// 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`);