下書き(未公開)
縦書き原稿用紙

ソースコード / 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');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 + '(ブラウザの起動・操作なし)');