85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
170 lines
5.8 KiB
JavaScript
170 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* inject-fonts.cjs — inline the @font-face blocks each HTML actually uses.
|
|
*
|
|
* node inject-fonts.cjs <project-dir> [file.html ...]
|
|
*
|
|
* hyperframes' renderer only auto-supplies its ~18 CANONICAL_FONTS. Every other
|
|
* family (Anton, Bangers, VT323, Press Start 2P, …) falls back to a generic font
|
|
* unless the HTML ships a local @font-face — even when it "looks fine" locally,
|
|
* because that only works when the font happens to be installed as a system font.
|
|
* On a clean/offline/CI machine it silently degrades. (hyperframes' own font
|
|
* linter flags exactly this: font_family_without_font_face.)
|
|
*
|
|
* This step reads the bundled font library (modes/standard/fonts/fonts.css —
|
|
* base64 woff2, no sub-resources), finds which of those families the HTML uses,
|
|
* and inlines just those @font-face blocks into a <style id="hf-embedded-fonts">.
|
|
* Idempotent (re-running replaces the block). Families already declared in the
|
|
* HTML, hyperframes-canonical families, and CSS generics are left untouched.
|
|
*
|
|
* Runs BEFORE the layout gates + render so measure-layout and the capture both
|
|
* see the real glyph metrics, never a fallback.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const SKILL_ROOT = path.resolve(__dirname, "..");
|
|
const FONTS_CSS = path.join(SKILL_ROOT, "modes", "standard", "fonts", "fonts.css");
|
|
const MARKER = "hf-embedded-fonts";
|
|
|
|
const GENERIC = new Set([
|
|
"serif",
|
|
"sans-serif",
|
|
"monospace",
|
|
"cursive",
|
|
"fantasy",
|
|
"system-ui",
|
|
"ui-serif",
|
|
"ui-sans-serif",
|
|
"ui-monospace",
|
|
"ui-rounded",
|
|
"math",
|
|
"emoji",
|
|
"fangsong",
|
|
"inherit",
|
|
"initial",
|
|
"unset",
|
|
"revert",
|
|
"revert-layer",
|
|
]);
|
|
|
|
// Parse fonts.css → Map<lowercased family, [ @font-face block strings ]>.
|
|
function loadFontLibrary() {
|
|
let css;
|
|
try {
|
|
css = fs.readFileSync(FONTS_CSS, "utf8");
|
|
} catch {
|
|
console.error(
|
|
`[inject-fonts] missing ${FONTS_CSS} — run modes/standard/fonts/build-fonts-css.cjs`,
|
|
);
|
|
process.exit(2);
|
|
}
|
|
const lib = new Map();
|
|
// base64 data-URIs contain no "}", so [^}]* safely bounds a block.
|
|
const blockRe = /@font-face\s*\{[^}]*\}/gi;
|
|
const famRe = /font-family\s*:\s*(['"]?)([^;'"]+)\1/i;
|
|
let m;
|
|
while ((m = blockRe.exec(css)) !== null) {
|
|
const fam = m[0].match(famRe);
|
|
if (!fam) continue;
|
|
const key = fam[2].trim().toLowerCase();
|
|
if (!lib.has(key)) lib.set(key, []);
|
|
lib.get(key).push(m[0]);
|
|
}
|
|
return lib;
|
|
}
|
|
|
|
// Families referenced anywhere in the HTML: CSS `font-family:` (in <style> or
|
|
// inline style="") and the data-font-family attribute hyperframes also honors.
|
|
function usedFamilies(html) {
|
|
const out = new Set();
|
|
const add = (stack) => {
|
|
for (const part of String(stack).split(",")) {
|
|
const name = part
|
|
.trim()
|
|
.replace(/^['"]|['"]$/g, "")
|
|
.trim()
|
|
.toLowerCase();
|
|
if (name && !GENERIC.has(name) && !/^var\(/.test(name)) out.add(name);
|
|
}
|
|
};
|
|
// strip existing @font-face so we don't re-list the family it declares
|
|
const body = html.replace(/@font-face\s*\{[^}]*\}/gi, "");
|
|
for (const mm of body.matchAll(/font-family\s*:\s*([^;}{]+)/gi)) add(mm[1]);
|
|
for (const mm of html.matchAll(/data-font-family\s*=\s*["']([^"']+)["']/gi)) add(mm[1]);
|
|
return out;
|
|
}
|
|
|
|
// Families the HTML ALREADY declares via @font-face — leave those alone.
|
|
function declaredFamilies(html) {
|
|
const out = new Set();
|
|
const famRe = /font-family\s*:\s*(['"]?)([^;'"]+)\1/i;
|
|
for (const mm of html.matchAll(/@font-face\s*\{[^}]*\}/gi)) {
|
|
const f = mm[0].match(famRe);
|
|
if (f) out.add(f[2].trim().toLowerCase());
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function injectInto(file, lib) {
|
|
let html;
|
|
try {
|
|
html = fs.readFileSync(file, "utf8");
|
|
} catch {
|
|
return null;
|
|
}
|
|
// drop any previous injection so this is idempotent
|
|
html = html.replace(new RegExp(`\\s*<style id="${MARKER}">[\\s\\S]*?</style>`, "i"), "");
|
|
|
|
const used = usedFamilies(html);
|
|
const already = declaredFamilies(html);
|
|
const blocks = [];
|
|
const inlined = [];
|
|
for (const fam of used) {
|
|
if (already.has(fam)) continue;
|
|
const lb = lib.get(fam);
|
|
if (!lb) continue; // canonical / system / unknown — not ours to supply
|
|
blocks.push(...lb);
|
|
inlined.push(fam);
|
|
}
|
|
if (!blocks.length) {
|
|
fs.writeFileSync(file, html);
|
|
return { file: path.basename(file), inlined: [] };
|
|
}
|
|
const style = `<style id="${MARKER}">\n/* auto-embedded by inject-fonts.cjs — deterministic template fonts */\n${blocks.join("\n")}\n</style>`;
|
|
// Prefer right after <head...>; fall back to before </head>, then <html>, then prepend.
|
|
if (/<head[^>]*>/i.test(html)) html = html.replace(/<head[^>]*>/i, (m0) => `${m0}\n${style}`);
|
|
else if (/<\/head>/i.test(html)) html = html.replace(/<\/head>/i, `${style}\n</head>`);
|
|
else if (/<html[^>]*>/i.test(html))
|
|
html = html.replace(/<html[^>]*>/i, (m0) => `${m0}\n${style}`);
|
|
else html = style + "\n" + html;
|
|
|
|
fs.writeFileSync(file, html);
|
|
return { file: path.basename(file), inlined };
|
|
}
|
|
|
|
function main() {
|
|
const project = path.resolve(process.argv[2] || "");
|
|
if (!process.argv[2]) {
|
|
console.error("usage: inject-fonts.cjs <project-dir> [file.html ...]");
|
|
process.exit(1);
|
|
}
|
|
const lib = loadFontLibrary();
|
|
let files = process.argv.slice(3);
|
|
if (!files.length) files = ["index.html", "rail.html", "index_fg.html"];
|
|
files = files.map((f) => (path.isAbsolute(f) ? f : path.join(project, f)));
|
|
|
|
let touched = 0;
|
|
for (const f of files) {
|
|
const r = injectInto(f, lib);
|
|
if (r == null) continue;
|
|
if (r.inlined.length) {
|
|
console.log(`[inject-fonts] ${r.file}: embedded ${r.inlined.join(", ")}`);
|
|
touched++;
|
|
} else console.log(`[inject-fonts] ${r.file}: no non-canonical fonts to embed`);
|
|
}
|
|
if (!touched)
|
|
console.log(`[inject-fonts] nothing embedded (all fonts canonical/system or already declared)`);
|
|
}
|
|
main();
|