プログラム / scripts/build-chrome-extension.cjs
scripts/build-chrome-extension.cjs
// Build chrome-extension/ from the Chrome edition's own source in src/chrome.// The build only fills in data and writes the manifest, notices, fonts and// icons; edit src/chrome directly. Google versions and sample_20 are separate.const fs = require('node:fs');const path = require('node:path');const vm = require('node:vm');const zlib = require('node:zlib');const root = path.resolve(__dirname, '..');const extensionVersion = '0.2.24';const src = path.join(root, 'src', 'chrome');const out = path.join(root, 'chrome-extension');fs.mkdirSync(path.join(out, 'icons'), { recursive: true });const read = file => fs.readFileSync(path.join(src, file), 'utf8');function fill(text, marker, value) { if (text.split(marker).length !== 2) throw Error('Build marker missing or repeated: ' + marker); return text.replace(marker, () => value);}// Bundled handwriting fonts: local WOFF2 files and their licences.const fonts = [ { id: 'klee-one', family: 'Klee One', stem: 'KleeOne-Regular' }, { id: 'yomogi', family: 'Yomogi', stem: 'Yomogi-Regular' }];const { samples, notice } = require('./manuscript-samples.cjs');const legacySamples = JSON.parse(fs.readFileSync(path.join(root, 'data', 'chrome', 'legacy-samples.json'), 'utf8'));// Generated by scripts/measure-vertical-alternates.py from the font files.const verticalAlternates = Object.fromEntries(Object.entries(require('./vertical-alternates.json')) .map(([font, entry]) => [font, entry.marks]));let editor = read('editor.js');editor = fill(editor, '/* build: SAMPLE_CATALOG */[]', JSON.stringify(samples));editor = fill(editor, '/* build: LEGACY_SAMPLES */[]', JSON.stringify(legacySamples));editor = fill(editor, '/* build: VERTICAL_ALTERNATES */{}', JSON.stringify(verticalAlternates));const scripts = { 'startup.js': read('startup.js'), 'editor.js': editor, 'background.js': read('background.js') };for (const [file, source] of Object.entries(scripts)) { new vm.Script(source, { filename: file }); fs.writeFileSync(path.join(out, file), source);}fs.writeFileSync(path.join(out, 'editor.html'), fill(read('editor.html'), '{{VERSION}}', extensionVersion));fs.writeFileSync(path.join(out, 'editor.css'), read('editor.css'));fs.writeFileSync(path.join(out, 'notices.txt'), notice);for (const font of fonts) { const target = path.join(out, 'fonts', font.id); fs.mkdirSync(target, { recursive: true }); for (const name of [font.stem + '.woff2', 'OFL.txt']) fs.copyFileSync(path.join(root, 'assets', 'fonts', font.id, name), path.join(target, name)); fs.appendFileSync(path.join(out, 'notices.txt'), '\n' + font.family + ': SIL Open Font License 1.1\n' + '著作権表示と全文ライセンス:fonts/' + font.id + '/OFL.txt\nこのフォントにはアプリ独自の再配布禁止条件を付しません。\n');}const icons = Object.fromEntries([16, 32, 48, 128].map(size => [size, `icons/genko-${size}.png`]));fs.writeFileSync(path.join(out, 'manifest.json'), JSON.stringify({ manifest_version: 3, name: '縦書き原稿(試作版)', version: extensionVersion, minimum_chrome_version: '116', description: 'アイコンから縦書き原稿用紙を開きます。ルビ・編集履歴・JSON保存に対応。原稿は端末内に保存します。', action: { default_title: '縦書き原稿を開く', default_icon: icons }, icons, background: { service_worker: 'background.js' }, content_security_policy: { extension_pages: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'; connect-src 'none'; img-src 'self' data: blob:; frame-src 'none'; form-action 'none';" }}, null, 2) + '\n');// Geometric manuscript icon, encoded locally as PNG without dependencies.function crc32(bytes) { let crc = 0xffffffff; for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0); } return (crc ^ 0xffffffff) >>> 0;}function chunk(type, data) { const name = Buffer.from(type), length = Buffer.alloc(4), crc = Buffer.alloc(4); length.writeUInt32BE(data.length); crc.writeUInt32BE(crc32(Buffer.concat([name, data]))); return Buffer.concat([length, name, data, crc]);}for (const size of [16, 32, 48, 128]) { const bytes = Buffer.alloc((size * 4 + 1) * size); for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { const xx = x / size, yy = y / size; const paper = xx >= .16 && xx <= .84 && yy >= .1 && yy <= .9; const vertical = [.23, .41, .59, .77].some(line => Math.abs(xx - line) < .012); const horizontal = [.2, .35, .5, .65, .8].some(line => Math.abs(yy - line) < .01); const grid = paper && xx >= .22 && xx <= .78 && yy >= .19 && yy <= .81 && (vertical || horizontal); const color = !paper ? [38, 83, 91] : grid ? [194, 123, 103] : [255, 251, 239]; const at = y * (size * 4 + 1) + 1 + x * 4; bytes.set([...color, 255], at); } const header = Buffer.alloc(13); header.writeUInt32BE(size); header.writeUInt32BE(size, 4); header[8] = 8; header[9] = 6; fs.writeFileSync(path.join(out, icons[size]), Buffer.concat([ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', header), chunk('IDAT', zlib.deflateSync(bytes)), chunk('IEND', Buffer.alloc(0)) ]));}console.log('Chrome試作版を生成: ' + out + '(ブラウザの起動・操作なし)');