下書き(未公開)。〔 〕と「要決定」の箇所は未確定です。
縦書き原稿仮称

プログラム / 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 + '(ブラウザの起動・操作なし)');