Files
heygen-com--hyperframes/skills/embedded-captions/scripts/inject-fonts.cjs
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

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();