theseus/addon-build/docx-editor/fetch-fonts.mjs

86 lines
3.7 KiB
JavaScript
Raw Normal View History

feat(docx-editor): the page fills the window, and two fonts ship with it The page sat marooned in the middle of a wide window with dark space either side of it. It is now drawn at the size the document actually claims — A4 stays A4, margins come from its own sectPr — and CSS `zoom` scales that to fit, defaulting to Fit width with a control in the footer and Ctrl +/-/0. Scaling rather than widening is deliberate. A page stretched to the window would break every line somewhere different from where the printed page breaks it, and an editor whose whole claim is that it shows you the document should not lie about where the lines end. `zoom` also beats a transform here: it affects layout, so the board scrolls correctly and ProseMirror's coordinate maths keeps working. Ubuntu and Fraunces now ship in fonts/, because Windows has neither and a font offered in the ribbon that the machine lacks is a font the user picks and then cannot see. Fetched once by `npm run fonts` and committed, never at runtime: an extension in a browser built around not phoning home should not ask a font CDN what a document looks like every time one is opened. Two things had to be worked around. On file:// Chromium registers @font-face rules and then refuses to fetch the files — the family appears in document.fonts and every glyph still renders in the fallback — so the add-on reads the woff2 and hands the page a stylesheet with them inlined as data URLs. The PDF export needed the same treatment for a different reason: its print window runs from a temp folder, where a relative url() resolves to nothing, which would have quietly undone the one-stylesheet-for-both promise that lib/doc-css.js exists to keep. If either path fails, the ribbon labels those families "(not available)" rather than implying otherwise. About 700 KB, most of it Ubuntu's Cyrillic and Greek — kept because the documents this is used on are not all English. Licences ship alongside.
2026-09-22 19:12:56 +02:00
// Vendor the editor's bundled webfonts.
//
// npm run fonts (from addon-build/docx-editor/)
//
// The editor offers fonts in its ribbon, and a font the machine doesn't have
// is a font the user picks and then cannot see. Ubuntu and Fraunces are not
// on Windows, so they ship with the add-on.
//
// Fetched from Google Fonts once, here, and committed — never at runtime. An
// extension in a browser built around not phoning home should not ask
// fonts.gstatic.com what a document looks like every time one is opened.
//
// Subsets: Latin plus Cyrillic and Greek where the family has them, because
// the documents this editor is actually used on are not all English.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.resolve(here, "../../bundled-addons/docx-editor/fonts");
// A browser UA, or Google serves ttf instead of woff2.
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
const FAMILIES = [
{
name: "Ubuntu",
css: "https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,400;0,700;1,400;1,700&display=block",
// No cyrillic-ext or greek-ext: 148 KB for historic and liturgical
// ranges that a word processor's font menu will not miss.
keep: ["latin", "latin-ext", "cyrillic", "greek"],
},
{
// The variable file, which is smaller than the four static instances it
// replaces (280 KB against 305 KB) and carries every weight between them.
name: "Fraunces",
css: "https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=block",
keep: ["latin", "latin-ext"],
},
];
async function text(url) {
const r = await fetch(url, { headers: { "User-Agent": UA } });
if (!r.ok) throw new Error(`${r.status} for ${url}`);
return r.text();
}
fs.mkdirSync(OUT, { recursive: true });
let css = `/* Bundled with the Word editor — see addon-build/docx-editor/fetch-fonts.mjs.
Regenerate with \`npm run fonts\`; do not edit by hand. */\n`;
let total = 0;
for (const fam of FAMILIES) {
const sheet = await text(fam.css);
// Google's stylesheet is a run of /* subset */ + @font-face blocks.
const blocks = sheet.split("/*").slice(1);
let n = 0;
for (const raw of blocks) {
const subset = raw.slice(0, raw.indexOf("*/")).trim();
if (!fam.keep.includes(subset)) continue;
const block = raw.slice(raw.indexOf("*/") + 2);
const url = /src:\s*url\(([^)]+)\)/.exec(block)?.[1];
if (!url) continue;
const style = /font-style:\s*(\w+)/.exec(block)?.[1] || "normal";
const weight = /font-weight:\s*([\d\s]+)/.exec(block)?.[1].trim() || "400";
const range = /unicode-range:\s*([^;]+);/.exec(block)?.[1].trim();
const file = `${fam.name.toLowerCase()}-${subset}-${style}-${weight.replace(/\s+/g, "_")}.woff2`;
const bytes = Buffer.from(await (await fetch(url, { headers: { "User-Agent": UA } })).arrayBuffer());
fs.writeFileSync(path.join(OUT, file), bytes);
total += bytes.length;
n++;
css += `@font-face {\n font-family: '${fam.name}';\n font-style: ${style};\n` +
` font-weight: ${weight};\n font-display: block;\n` +
` src: url('fonts/${file}') format('woff2');\n` +
(range ? ` unicode-range: ${range};\n` : "") + `}\n`;
}
console.log(`${fam.name}: ${n} file(s)`);
}
fs.writeFileSync(path.resolve(OUT, "../fonts.css"), css);
console.log(`\n${(total / 1024).toFixed(0)} KB of woff2 in ${path.relative(path.resolve(here, "../.."), OUT)}`);
console.log(`stylesheet: bundled-addons/docx-editor/fonts.css`);