mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(compiler): split font-family only on top-level commas (#3067)
parseFontFamilyValue() split the family stack on every comma, so `font-family: var(--brand-font, inherit)` became two tokens: `var(--brand-font` and `inherit)`. The var() guard from #1655 only skips tokens starting with `var(`, so the orphan fragment was treated as a requested family, failed every resolution path, and aborted fail-closed distributed renders with: FontFetchError: [Compiler] Unresolved fonts in fail-closed mode: inherit). Distributed renders require all fonts to be resolvable. Split on top-level commas only, so a var() expression (including a nested one) stays a single token. Quotes are tracked as well, both so parentheses inside a quoted family name cannot skew the depth counter and so a legal quoted comma no longer splits. Closes #3066
This commit is contained in:
@@ -253,6 +253,17 @@ describe("injectDeterministicFontFaces — failClosedFontFetch: true", () => {
|
||||
expect(result).toBe(html);
|
||||
});
|
||||
|
||||
it("does NOT throw when font-family uses a CSS var() reference with a fallback", async () => {
|
||||
const html = `<!doctype html><html><head><style>
|
||||
.title { font-family: var(--brand-font, inherit); }
|
||||
</style></head><body><h1 class="title">hello</h1></body></html>`;
|
||||
const result = await injectDeterministicFontFaces(html, {
|
||||
failClosedFontFetch: true,
|
||||
fetchImpl: makeFailingFetch(),
|
||||
});
|
||||
expect(result).toBe(html);
|
||||
});
|
||||
|
||||
it("resolves simple CSS var() font aliases when injecting deterministic fonts", async () => {
|
||||
const html = `<!doctype html><html><head><style>
|
||||
:root { --ui-font: "Inter"; --vowel-font: "Montserrat"; }
|
||||
|
||||
@@ -54,10 +54,44 @@ export const GENERIC_FAMILIES: ReadonlySet<string> = new Set([
|
||||
* Whitespace and surrounding `"…"` / `'…'` quotes are stripped; case is
|
||||
* preserved. Pass each name through `normalizeFamilyName` for case-
|
||||
* insensitive comparisons.
|
||||
*
|
||||
* Only top-level commas split: a `var(--x, fallback)` expression stays one
|
||||
* token, as does a comma inside a quoted family name.
|
||||
*/
|
||||
export function parseFontFamilyValue(value: string): string[] {
|
||||
return value
|
||||
.split(",")
|
||||
const pieces: string[] = [];
|
||||
let start = 0;
|
||||
let depth = 0;
|
||||
let quote: "'" | '"' | null = null;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const char = value[index];
|
||||
if (char === "\\") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
if (char === quote) quote = null;
|
||||
continue;
|
||||
}
|
||||
if (char === "'" || char === '"') {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === "(") {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === ")") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
continue;
|
||||
}
|
||||
if (char !== "," || depth !== 0) continue;
|
||||
pieces.push(value.slice(start, index));
|
||||
start = index + 1;
|
||||
}
|
||||
pieces.push(value.slice(start));
|
||||
|
||||
return pieces
|
||||
.map((piece) => piece.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim())
|
||||
.filter((piece) => piece.length > 0);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,26 @@ describe("parseFontFamilyValue", () => {
|
||||
expect(parseFontFamilyValue(`'My Custom Font', serif`)).toEqual(["My Custom Font", "serif"]);
|
||||
});
|
||||
|
||||
it("keeps a comma inside a quoted family name", () => {
|
||||
expect(parseFontFamilyValue(`"Display, Condensed", serif`)).toEqual([
|
||||
"Display, Condensed",
|
||||
"serif",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a var() fallback in a single token", () => {
|
||||
expect(parseFontFamilyValue(`var(--brand-font, inherit), sans-serif`)).toEqual([
|
||||
"var(--brand-font, inherit)",
|
||||
"sans-serif",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a nested var() fallback in a single token", () => {
|
||||
expect(
|
||||
parseFontFamilyValue(`var(--brand-font, var(--fallback-font, "Inter")), sans-serif`),
|
||||
).toEqual([`var(--brand-font, var(--fallback-font, "Inter"))`, "sans-serif"]);
|
||||
});
|
||||
|
||||
it("ignores empty entries (trailing commas)", () => {
|
||||
expect(parseFontFamilyValue(`Inter,,sans-serif`)).toEqual(["Inter", "sans-serif"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user