ソースコード / 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 = '1.0.0';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);}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));}const notices = notice + '\n書体\n\n' + fonts.map(font => font.family).join('・') + ':SIL Open Font License 1.1(' + fonts.map(font => 'fonts/' + font.id + '/OFL.txt').join('、') + ')\n';fs.writeFileSync(path.join(out, 'notices.txt'), notices);// The same text is shown in the app's 「作品と書体の出典」 dialog.const escapeHtml = text => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');fs.writeFileSync(path.join(out, 'editor.html'), fill(fill(read('editor.html'), '{{VERSION}}', extensionVersion), '{{NOTICES}}', escapeHtml(notices)));fs.writeFileSync(path.join(out, 'editor.css'), read('editor.css'));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', // Shown as the summary on the Chrome Web Store (132 characters at most). description: '四百字詰めの原稿用紙に、縦書きでそのまま書けます。ふりがな・禁則・印刷に対応。無料で、原稿はパソコンの中だけに保存します。', 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); // The store wants the 128px icon as 96px artwork with 16px of transparent padding. const pad = size === 128 ? 16 : 0, art = size - pad * 2; for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { if (x < pad || y < pad || x >= pad + art || y >= pad + art) continue; // left transparent const xx = (x - pad) / art, yy = (y - pad) / art; 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('拡張機能を生成: ' + out + '(ブラウザの起動・操作なし)');