mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +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);
|
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 () => {
|
it("resolves simple CSS var() font aliases when injecting deterministic fonts", async () => {
|
||||||
const html = `<!doctype html><html><head><style>
|
const html = `<!doctype html><html><head><style>
|
||||||
:root { --ui-font: "Inter"; --vowel-font: "Montserrat"; }
|
: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
|
* Whitespace and surrounding `"…"` / `'…'` quotes are stripped; case is
|
||||||
* preserved. Pass each name through `normalizeFamilyName` for case-
|
* preserved. Pass each name through `normalizeFamilyName` for case-
|
||||||
* insensitive comparisons.
|
* 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[] {
|
export function parseFontFamilyValue(value: string): string[] {
|
||||||
return value
|
const pieces: string[] = [];
|
||||||
.split(",")
|
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())
|
.map((piece) => piece.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim())
|
||||||
.filter((piece) => piece.length > 0);
|
.filter((piece) => piece.length > 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,26 @@ describe("parseFontFamilyValue", () => {
|
|||||||
expect(parseFontFamilyValue(`'My Custom Font', serif`)).toEqual(["My Custom Font", "serif"]);
|
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)", () => {
|
it("ignores empty entries (trailing commas)", () => {
|
||||||
expect(parseFontFamilyValue(`Inter,,sans-serif`)).toEqual(["Inter", "sans-serif"]);
|
expect(parseFontFamilyValue(`Inter,,sans-serif`)).toEqual(["Inter", "sans-serif"]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
top: 30%;
|
top: 30%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
font-family: var(--display-font), sans-serif;
|
font-family: var(--display-font, "Montserrat"), sans-serif;
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
|
|||||||
Reference in New Issue
Block a user