fix(producer): embed the Google font the page linked instead of a wider cut (#4569)

* fix(producer): embed the Google font the page linked

The weight-only download drops axes the page's link asked for.
That file is written after the link, so the film draws a wider cut than the preview.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(producer): keep every weight the page's font link asked for

Google answers a 400..900 link as font-weight: 400 900, and only 400 was kept.
Heavier text then waited on the network. Large families are limited to the
characters on the page, @import is followed, and a failed link is not replaced
by a font found on this machine.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(producer): preserve italic supplements for bundled fonts

* fix(producer): preserve active authored font resources

* style(producer): condense font fetch comments

* fix(producer): preserve authored oblique font faces

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xuanru Li
2026-09-28 01:45:43 +00:00
committed by GitHub
co-authored by Cursor
parent e8313b2b39
commit 918e39e785
7 changed files with 773 additions and 26 deletions
@@ -0,0 +1,45 @@
import { expect, it } from "bun:test";
import { authoredGoogleFontStylesheets, withPageText } from "./authoredGoogleFonts.js";
const url = "https://fonts.googleapis.com/css2?family=Fraunces:wght@400";
it("decodes every family parameter and retains active screen imports in document order", () => {
const second = "https://fonts.googleapis.com/css2?family=Fraunces:wght@700";
const html = `<link rel="stylesheet" media="screen" href="${url}&amp;family=Test+Family:wght@700">
<style media="all">@import '${second}' screen;</style>`;
const resources = authoredGoogleFontStylesheets(html);
expect(resources.get("fraunces")).toEqual([`${url}&family=Test+Family:wght@700`, second]);
expect(resources.get("test family")).toEqual([`${url}&family=Test+Family:wght@700`]);
});
it.each([
`<link rel="preload" href="${url}">`,
`<link rel="stylesheet">`,
`<link rel="stylesheet" type="text/plain" href="${url}">`,
`<link rel="stylesheet" href="not-a-url">`,
`<link rel="stylesheet" href="https://example.com/css2?family=Fraunces">`,
`<link rel="stylesheet" href="http://fonts.googleapis.com/css2?family=Fraunces">`,
`<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=">`,
`<style>@media print { @import '${url}'; }</style>`,
`<style>/* @import '${url}'; */</style>`,
`<style>@import '${url}'; p {</style>`,
`<style type="text/plain">@import '${url}';</style>`,
])("does not collect an inactive or invalid resource: %s", (html) => {
expect(authoredGoogleFontStylesheets(html).size).toBe(0);
});
it("recognizes both families in a legacy Google Fonts URL", () => {
const legacy = "https://fonts.googleapis.com/css?family=Fraunces:400|Other:700";
expect([
...authoredGoogleFontStylesheets(`<link rel=stylesheet href="${legacy}">`).keys(),
]).toEqual(["fraunces", "other"]);
});
it("adds a text subset before the URL fragment without replacing authored text", () => {
expect(withPageText(`${url}#anchor`, "Hi")).toBe(`${url}&text=Hi#anchor`);
expect(withPageText(`${url}&text=Original#anchor`, "Hi")).toBe(`${url}&text=Original#anchor`);
expect(withPageText("https://fonts.googleapis.com/css2", "Hi")).toBe(
"https://fonts.googleapis.com/css2?text=Hi",
);
expect(withPageText(url, undefined)).toBe(url);
});
@@ -0,0 +1,86 @@
import { parseHTML } from "linkedom";
import postcss from "postcss";
function screenStylesheet(media: string | null): boolean {
return !media?.trim() || media.split(",").some((query) => /^(all|screen)$/i.test(query.trim()));
}
function googleStylesheetFamilies(urlText: string): string[] {
let parsed: URL;
try {
parsed = new URL(urlText);
} catch {
return [];
}
if (parsed.protocol !== "https:" || parsed.hostname !== "fonts.googleapis.com") return [];
return parsed.searchParams
.getAll("family")
.flatMap((family) => family.split("|"))
.map((family) => family.split(":")[0]?.trim().toLowerCase() ?? "")
.filter(Boolean);
}
function importUrls(css: string): string[] {
let root: postcss.Root;
try {
root = postcss.parse(css);
} catch {
return [];
}
const urls: string[] = [];
for (const node of root.nodes) {
if (node.type !== "atrule" || node.name.toLowerCase() !== "import") continue;
const url = unconditionalImportUrl(node.params);
if (url) urls.push(url);
}
return urls;
}
function unconditionalImportUrl(params: string): string | undefined {
const match = /^(?:url\(\s*['"]?([^'")\s]+)['"]?\s*\)|['"]([^'"]+)['"])\s*(.*)$/i.exec(params);
if (!match || !screenStylesheet(match[3] ?? null)) return undefined;
return match[1] ?? match[2];
}
type StylesheetElement = Pick<
Element,
"getAttribute" | "hasAttribute" | "localName" | "textContent"
>;
function activeStylesheet(element: StylesheetElement): boolean {
if (element.hasAttribute("disabled") || !screenStylesheet(element.getAttribute("media")))
return false;
const type = element.getAttribute("type")?.trim().toLowerCase();
return !type || type === "text/css";
}
function stylesheetUrls(element: StylesheetElement): string[] {
if (!activeStylesheet(element)) return [];
if (element.localName !== "link") return importUrls(element.textContent ?? "");
const rel = (element.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
if (!rel.includes("stylesheet") || rel.includes("alternate")) return [];
const href = element.getAttribute("href");
return href ? [href] : [];
}
export function authoredGoogleFontStylesheets(html: string): Map<string, string[]> {
const byFamily = new Map<string, string[]>();
const { document } = parseHTML(html);
for (const element of document.querySelectorAll("link, style")) {
for (const url of stylesheetUrls(element)) {
for (const family of googleStylesheetFamilies(url)) {
const previous = byFamily.get(family) ?? [];
byFamily.set(family, [...previous.filter((existing) => existing !== url), url]);
}
}
}
return byFamily;
}
export function withPageText(url: string, fontText: string | undefined): string {
const parsed = new URL(url);
if (!fontText || parsed.searchParams.has("text")) return url;
const withoutFragment = url.split("#")[0] ?? url;
const joiner = parsed.search ? "&" : "?";
return `${withoutFragment}${joiner}text=${encodeURIComponent(fontText)}${parsed.hash}`;
}
@@ -0,0 +1,83 @@
import { afterEach, beforeEach, expect, it, spyOn } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
_clearGoogleFontCssCacheForTests,
injectDeterministicFontFaces,
} from "./deterministicFonts.js";
let cacheDir: string;
let previousCacheDir: string | undefined;
const requests: string[] = [];
let restoreFetch: () => void;
beforeEach(() => {
previousCacheDir = process.env.HYPERFRAMES_FONT_CACHE_DIR;
cacheDir = mkdtempSync(join(tmpdir(), "hf-authored-resources-"));
process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir;
requests.length = 0;
_clearGoogleFontCssCacheForTests();
const mock = spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url = new URL(String(input));
requests.push(url.href);
if (url.hostname === "fonts.googleapis.com") {
const weight = url.searchParams.get("family")?.endsWith("700") ? "700" : "400";
return new Response(`@font-face {
font-family: 'Fraunces'; font-style: normal; font-weight: ${weight};
src: url(https://fonts.gstatic.com/fraunces-${weight}.woff2) format('woff2');
}`);
}
return new Response(url.pathname);
});
restoreFetch = () => mock.mockRestore();
});
afterEach(() => {
restoreFetch();
_clearGoogleFontCssCacheForTests();
if (previousCacheDir === undefined) delete process.env.HYPERFRAMES_FONT_CACHE_DIR;
else process.env.HYPERFRAMES_FONT_CACHE_DIR = previousCacheDir;
rmSync(cacheDir, { recursive: true, force: true });
});
const normal = "https://fonts.googleapis.com/css2?family=Fraunces:wght@400";
const bold = "https://fonts.googleapis.com/css2?family=Fraunces:wght@700";
const page = (head: string) => `<!doctype html><html><head>${head}
<style>p { font-family: Fraunces; }</style></head><body><p>Hi</p></body></html>`;
function requestedFamilies(): (string | null)[] {
return requests
.filter((url) => url.startsWith("https://fonts.googleapis.com/"))
.map((url) => new URL(url).searchParams.get("family"));
}
it("decodes HTML entities and preserves an authored text subset", async () => {
await injectDeterministicFontFaces(page(`<link rel=stylesheet href="${normal}&amp;text=Hi">`));
expect(requests[0]).toBe(`${normal}&text=Hi`);
});
it("embeds every separate weight resource once in document order", async () => {
const result = await injectDeterministicFontFaces(
page(
`<link rel=stylesheet href="${normal}"><link rel=stylesheet href="${bold}">` +
`<link rel=stylesheet href="${normal}">`,
),
);
expect(requestedFamilies()).toEqual(["Fraunces:wght@700", "Fraunces:wght@400"]);
expect(result).toContain(Buffer.from("/fraunces-400.woff2").toString("base64"));
expect(result).toContain(Buffer.from("/fraunces-700.woff2").toString("base64"));
});
it.each([
`<link rel="stylesheet" media="print" href="${bold}">`,
`<link rel="stylesheet" disabled href="${bold}">`,
`<link rel="alternate stylesheet" title="other" href="${bold}">`,
`<style media="print">@import url("${bold}");</style>`,
`<style>@import url("${bold}") print;</style>`,
`<style>@import url("${bold}") supports(display: grid);</style>`,
`<!-- <link rel="stylesheet" href="${bold}"> -->`,
])("does not promote an inactive font source: %s", async (inactive) => {
await injectDeterministicFontFaces(page(`${inactive}<link rel="stylesheet" href="${normal}">`));
expect(requestedFamilies()).toEqual(["Fraunces:wght@400"]);
});
@@ -0,0 +1,53 @@
import { afterEach, expect, it, spyOn } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
_clearGoogleFontCssCacheForTests,
injectDeterministicFontFaces,
} from "./deterministicFonts.js";
const previousCacheDir = process.env.HYPERFRAMES_FONT_CACHE_DIR;
const cacheDir = mkdtempSync(join(tmpdir(), "hf-bundled-stylesheet-"));
const fetchMock = spyOn(globalThis, "fetch");
afterEach(() => {
fetchMock.mockRestore();
_clearGoogleFontCssCacheForTests();
if (previousCacheDir === undefined) delete process.env.HYPERFRAMES_FONT_CACHE_DIR;
else process.env.HYPERFRAMES_FONT_CACHE_DIR = previousCacheDir;
rmSync(cacheDir, { recursive: true, force: true });
});
it("supplements bundled fonts with real italics even when the page imports only normal", async () => {
process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir;
_clearGoogleFontCssCacheForTests();
const italicUrl = "https://fonts.gstatic.com/s/ebgaramond/italic.woff2";
const normalUrl = "https://fonts.gstatic.com/s/ebgaramond/normal.woff2";
fetchMock.mockImplementation(async (input) => {
const url = String(input);
if (url.startsWith("https://fonts.googleapis.com/")) {
const italic = url.includes("ital,wght@");
return new Response(`@font-face {
font-family: 'EB Garamond';
font-style: ${italic ? "italic" : "normal"};
font-weight: 400;
src: url(${italic ? italicUrl : normalUrl}) format('woff2');
unicode-range: U+0000-00FF;
}`);
}
if (url === italicUrl) return new Response("REAL_ITALIC_FONT");
if (url === normalUrl) return new Response("NORMAL_FONT");
return new Response("", { status: 404 });
});
const result = await injectDeterministicFontFaces(`<!doctype html><html><head><style>
@import url("https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400&display=swap");
p { font-family: "EB Garamond", serif; font-style: italic; }
</style></head><body><p>lack technical skills</p></body></html>`);
const italicFace = result.match(/@font-face\s*\{[^}]*font-style: italic[^}]*\}/)?.[0];
expect(italicFace).toBeDefined();
expect(italicFace).toContain('font-family: "EB Garamond"');
expect(italicFace).toContain(Buffer.from("REAL_ITALIC_FONT").toString("base64"));
});
@@ -19,24 +19,33 @@
* so they are hermetic.
*/
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { _clearGoogleFontCssCacheForTests } from "./deterministicFonts.js";
beforeEach(() => _clearGoogleFontCssCacheForTests());
let cacheDir: string;
let prevCacheEnv: string | undefined;
const LOCAL_FONT_DIR = join(homedir(), ".local", "share", "fonts");
const LOCAL_FONT_FILE = join(LOCAL_FONT_DIR, "hf-authored-fail-test.woff2");
const LOCAL_FONT_BYTES = "LOCAL_ONLY_BYTES";
beforeAll(() => {
prevCacheEnv = process.env.HYPERFRAMES_FONT_CACHE_DIR;
cacheDir = mkdtempSync(join(tmpdir(), "hf-font-cache-"));
process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir;
mkdirSync(LOCAL_FONT_DIR, { recursive: true });
writeFileSync(LOCAL_FONT_FILE, LOCAL_FONT_BYTES);
});
afterAll(() => {
if (prevCacheEnv === undefined) delete process.env.HYPERFRAMES_FONT_CACHE_DIR;
else process.env.HYPERFRAMES_FONT_CACHE_DIR = prevCacheEnv;
rmSync(cacheDir, { recursive: true, force: true });
rmSync(LOCAL_FONT_FILE, { force: true });
});
const VIET_RANGE = "U+0102-0103, U+1EA0-1EF9, U+20AB";
@@ -108,3 +117,351 @@ describe("Google Fonts multi-subset embedding", () => {
expect(latinFace).toContain(LATIN_RANGE);
});
});
const AUTHORED_HREF =
"https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Nunito+Sans:wght@400;600;700&display=swap";
const FRAUNCES_FILE = "https://fonts.gstatic.com/s/fraunces/authored.woff2";
const NUNITO_FILE = "https://fonts.gstatic.com/s/nunitosans/authored.woff2";
const WIDE_FILE = "https://fonts.gstatic.com/s/fraunces/wide.woff2";
const INTER_FILE = "https://fonts.gstatic.com/s/inter/v1/inter-supplement.woff2";
const AUTHORED_CSS = `@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(${FRAUNCES_FILE}) format('woff2');
unicode-range: U+0000-00FF;
}
@font-face {
font-family: 'Nunito Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(${NUNITO_FILE}) format('woff2');
unicode-range: U+0000-00FF;
}`;
function authoredPage(head: string): string {
return `<!doctype html><html><head>${head}</head><body><h1>Seconds</h1></body></html>`;
}
function isAuthoredRequest(url: string, href: string): boolean {
return url === href || url.startsWith(`${href}&text=`);
}
function authoredFetch(cssStatus: number): { fetchImpl: typeof fetch; urls: string[] } {
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
const url = String(input);
urls.push(url);
if (isAuthoredRequest(url, AUTHORED_HREF))
return new Response(cssStatus === 200 ? AUTHORED_CSS : "", { status: cssStatus });
if (url.includes("family=Fraunces:ital,wght@")) {
return new Response(
`@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 500; src: url(${WIDE_FILE}) format('woff2'); unicode-range: U+0000-00FF; }`,
{ status: 200 },
);
}
if (url.includes("family=Inter:")) {
return new Response(
`@font-face { font-family: 'Inter'; font-style: normal; font-weight: 300; src: url(${INTER_FILE}) format('woff2'); }`,
{ status: 200 },
);
}
if (url === FRAUNCES_FILE) return new Response("FRAUNCES_LINKED", { status: 200 });
if (url === NUNITO_FILE) return new Response("NUNITO_LINKED", { status: 200 });
if (url === WIDE_FILE) return new Response("FRAUNCES_WIDE", { status: 200 });
if (url === INTER_FILE) return new Response("INTER_BYTES", { status: 200 });
return new Response("", { status: 404 });
}) as unknown as typeof fetch;
return { fetchImpl, urls };
}
describe("authored Google font stylesheet", () => {
it("embeds the linked file and leaves it after the link", async () => {
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const { fetchImpl, urls } = authoredFetch(200);
const result = await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${AUTHORED_HREF}">` +
`<style>h1 { font-family: "Fraunces", serif; } p { font-family: "Nunito Sans", sans-serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
const cssUrls = urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"));
expect(cssUrls).toHaveLength(1);
expect(cssUrls[0]?.startsWith(`${AUTHORED_HREF}&text=`)).toBe(true);
expect(new URL(cssUrls[0] ?? "").searchParams.get("text") ?? "").toContain("S");
expect(result).toContain(b64("FRAUNCES_LINKED"));
expect(result).toContain(b64("NUNITO_LINKED"));
expect(result).not.toContain(b64("FRAUNCES_WIDE"));
const frauncesFace = result
.split("@font-face")
.find((block) => block.includes('font-family: "Fraunces"'));
expect(frauncesFace).toBeDefined();
expect(frauncesFace).not.toContain(b64("NUNITO_LINKED"));
expect(result.indexOf("data-hyperframes-deterministic-fonts")).toBeGreaterThan(
result.indexOf(AUTHORED_HREF),
);
});
it("does not replace a failed link with the weight-only file", async () => {
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const { fetchImpl, urls } = authoredFetch(400);
const result = await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${AUTHORED_HREF}"><style>h1 { font-family: "Fraunces", serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
expect(urls.some((url) => url.includes("ital,wght@"))).toBe(false);
expect(result).not.toContain(b64("FRAUNCES_WIDE"));
expect(result).not.toContain("data-hyperframes-deterministic-fonts");
});
it("still requests by family name when the page has no Google link", async () => {
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const { fetchImpl, urls } = authoredFetch(200);
const result = await injectDeterministicFontFaces(
authoredPage(`<style>h1 { font-family: "Fraunces", serif; }</style>`),
{ fetchImpl, allowSystemFontCapture: false },
);
expect(urls.some((url) => url.includes("family=Fraunces:ital,wght@"))).toBe(true);
expect(result).toContain(b64("FRAUNCES_WIDE"));
});
it("keeps a 400 900 weight span on every embedded subset", async () => {
const href =
"https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@0,6..96,400..900&display=swap";
const latin = "https://fonts.gstatic.com/s/bodonimoda/latin.woff2";
const latinExt = "https://fonts.gstatic.com/s/bodonimoda/latin-ext.woff2";
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
const url = String(input);
urls.push(url);
if (isAuthoredRequest(url, href)) {
return new Response(
`@font-face { font-family: 'Bodoni Moda'; font-style: normal; font-weight: 400 900; src: url(${latinExt}) format('woff2'); unicode-range: U+0100-02BA; }
@font-face { font-family: 'Bodoni Moda'; font-style: normal; font-weight: 400 900; src: url(${latin}) format('woff2'); unicode-range: U+0000-00FF; }`,
{ status: 200 },
);
}
if (url === latin) return new Response("BODONI_LATIN", { status: 200 });
if (url === latinExt) return new Response("BODONI_LATIN_EXT", { status: 200 });
return new Response("", { status: 404 });
}) as unknown as typeof fetch;
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const result = await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${href}"><style>h1 { font-family: "Bodoni Moda", serif; font-weight: 700; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
const cssUrls = urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"));
expect(cssUrls).toHaveLength(1);
expect(cssUrls[0]?.startsWith(`${href}&text=`)).toBe(true);
const faces = result
.split("@font-face")
.slice(1)
.filter((block) => block.includes('font-family: "Bodoni Moda"'));
expect(faces).toHaveLength(2);
for (const face of faces) {
expect(face).toContain("font-weight: 400 900;");
}
expect(result).toContain(b64("BODONI_LATIN"));
expect(result).toContain(b64("BODONI_LATIN_EXT"));
});
it("still embeds Inter for Arial when the page links Arial", async () => {
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const { fetchImpl, urls } = authoredFetch(200);
await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Arial">` +
`<style>h1 { font-family: Arial, sans-serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
expect(urls.some((url) => url.includes("family=Arial"))).toBe(false);
expect(urls.some((url) => url.includes("family=Inter:"))).toBe(true);
});
it("embeds the Google file named by an import, not the weight-only file", async () => {
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const { fetchImpl, urls } = authoredFetch(200);
const result = await injectDeterministicFontFaces(
authoredPage(
`<style>@import url("${AUTHORED_HREF}"); h1 { font-family: "Fraunces", serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
const cssUrls = urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"));
expect(cssUrls.some((url) => url.startsWith(`${AUTHORED_HREF}&text=`))).toBe(true);
expect(cssUrls.some((url) => url.includes("ital,wght@"))).toBe(false);
expect(result).toContain(b64("FRAUNCES_LINKED"));
expect(result).not.toContain(b64("FRAUNCES_WIDE"));
});
it("preserves a later weight-range link alongside the first static link", async () => {
const narrow = "https://fonts.googleapis.com/css2?family=Bodoni+Moda:wght@400&display=swap";
const wide =
"https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@0,6..96,400..900&display=swap";
const file = "https://fonts.gstatic.com/s/bodonimoda/range.woff2";
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
const url = String(input);
urls.push(url);
if (isAuthoredRequest(url, wide)) {
return new Response(
`@font-face { font-family: 'Bodoni Moda'; font-style: normal; font-weight: 400 900; src: url(${file}) format('woff2'); unicode-range: U+0000-00FF; }`,
{ status: 200 },
);
}
if (url === file) return new Response("BODONI_RANGE", { status: 200 });
return new Response("", { status: 404 });
}) as unknown as typeof fetch;
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const result = await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${narrow}">` +
`<link rel="stylesheet" href="${wide}">` +
`<style>h1 { font-family: "Bodoni Moda", serif; font-weight: 700; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
const cssUrls = urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"));
expect(cssUrls).toHaveLength(2);
expect(cssUrls[0]?.startsWith(`${narrow}&text=`)).toBe(true);
expect(cssUrls[1]?.startsWith(`${wide}&text=`)).toBe(true);
expect(result).toContain("font-weight: 400 900;");
expect(result).toContain(b64("BODONI_RANGE"));
});
it("preserves the first weight-range link alongside a later static link", async () => {
const wide =
"https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@0,6..96,400..900&display=swap";
const narrow = "https://fonts.googleapis.com/css2?family=Bodoni+Moda:wght@400&display=swap";
const file = "https://fonts.gstatic.com/s/bodonimoda/first-range.woff2";
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
const url = String(input);
urls.push(url);
if (isAuthoredRequest(url, wide)) {
return new Response(
`@font-face { font-family: 'Bodoni Moda'; font-style: normal; font-weight: 400 900; src: url(${file}) format('woff2'); unicode-range: U+0000-00FF; }`,
{ status: 200 },
);
}
return new Response("", { status: 404 });
}) as unknown as typeof fetch;
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${wide}">` +
`<link rel="stylesheet" href="${narrow}">` +
`<style>h1 { font-family: "Bodoni Moda", serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
const cssUrls = urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"));
expect(cssUrls).toHaveLength(2);
expect(cssUrls[0]?.startsWith(`${wide}&text=`)).toBe(true);
expect(cssUrls[1]?.startsWith(`${narrow}&text=`)).toBe(true);
});
it("leaves a link's own text list unchanged", async () => {
const href = "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500&text=Hi";
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
const url = String(input);
urls.push(url);
if (url === href) {
return new Response(
`@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 500; src: url(${FRAUNCES_FILE}) format('woff2'); unicode-range: U+0000-00FF; }`,
{ status: 200 },
);
}
if (url === FRAUNCES_FILE) return new Response("FRAUNCES_LINKED", { status: 200 });
return new Response("", { status: 404 });
}) as unknown as typeof fetch;
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${href}"><style>h1 { font-family: "Fraunces", serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: false },
);
expect(urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"))).toEqual([href]);
});
it("does not append page text when the character set does not fit on the font URL", async () => {
const href = "https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@400";
const file = "https://fonts.gstatic.com/s/notoserifjp/full.woff2";
const many = Array.from({ length: 600 }, (_, i) => String.fromCodePoint(0x4e00 + i)).join("");
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
const url = String(input);
urls.push(url);
if (url === href) {
return new Response(
`@font-face { font-family: 'Noto Serif JP'; font-style: normal; font-weight: 400; src: url(${file}) format('woff2'); unicode-range: U+3000-30FF; }`,
{ status: 200 },
);
}
if (url === file) return new Response("NOTO_FULL", { status: 200 });
return new Response("", { status: 404 });
}) as unknown as typeof fetch;
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
await injectDeterministicFontFaces(
`<!doctype html><html><head><link rel="stylesheet" href="${href}"><style>p { font-family: "Noto Serif JP", serif; }</style></head><body>${many}</body></html>`,
{ fetchImpl, allowSystemFontCapture: false },
);
expect(urls.filter((url) => url.startsWith("https://fonts.googleapis.com/"))).toEqual([href]);
});
it("does not embed a local file when the page's link failed", async () => {
const href = "https://fonts.googleapis.com/css2?family=Hf+Authored+Fail+Test";
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const urls: string[] = [];
const fetchImpl = (async (input: unknown) => {
urls.push(String(input));
return new Response("", { status: 400 });
}) as unknown as typeof fetch;
const result = await injectDeterministicFontFaces(
authoredPage(
`<link rel="stylesheet" href="${href}"><style>h1 { font-family: "Hf Authored Fail Test", serif; }</style>`,
),
{ fetchImpl, allowSystemFontCapture: true },
);
expect(urls.some((url) => url.includes("ital,wght@"))).toBe(false);
expect(result).not.toContain(b64(LOCAL_FONT_BYTES));
});
it("still embeds a local file when the page has no Google link", async () => {
const { injectDeterministicFontFaces } = await import("./deterministicFonts.js");
const fetchImpl = (async () => new Response("", { status: 400 })) as unknown as typeof fetch;
const result = await injectDeterministicFontFaces(
authoredPage(`<style>h1 { font-family: "Hf Authored Fail Test", serif; }</style>`),
{ fetchImpl, allowSystemFontCapture: true },
);
expect(result).toContain(b64(LOCAL_FONT_BYTES));
});
});
@@ -0,0 +1,66 @@
import { afterEach, beforeEach, expect, it, spyOn } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
_clearGoogleFontCssCacheForTests,
injectDeterministicFontFaces,
} from "./deterministicFonts.js";
let cacheDir: string;
let previousCacheDir: string | undefined;
let restoreFetch: (() => void) | undefined;
beforeEach(() => {
previousCacheDir = process.env.HYPERFRAMES_FONT_CACHE_DIR;
cacheDir = mkdtempSync(join(tmpdir(), "hf-oblique-"));
process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir;
_clearGoogleFontCssCacheForTests();
});
afterEach(() => {
restoreFetch?.();
_clearGoogleFontCssCacheForTests();
if (previousCacheDir === undefined) delete process.env.HYPERFRAMES_FONT_CACHE_DIR;
else process.env.HYPERFRAMES_FONT_CACHE_DIR = previousCacheDir;
rmSync(cacheDir, { recursive: true, force: true });
});
it.each(["oblique", "oblique 10deg", "oblique 0deg 10deg", "oblique -12.5deg 2.5deg"])(
"embeds every subset of an authored %s face in fail-closed mode",
async (style) => {
const subsets = ["U+0000-00FF", "U+0100-017F"];
const fetchMock = spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url = String(input);
if (url.startsWith("https://fonts.googleapis.com/")) {
return new Response(
subsets
.map(
(unicodeRange, index) => `@font-face {
font-family: 'Roboto Flex'; font-style: ${style}; font-weight: 100 1000;
src: url(https://fonts.gstatic.com/oblique-${index}.woff2) format('woff2');
unicode-range: ${unicodeRange};
}`,
)
.join("\n"),
);
}
return new Response(url);
});
restoreFetch = () => fetchMock.mockRestore();
const html = `<html><head><link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto+Flex:slnt,wght@-10..0,100..1000">
<style>p { font-family: 'Roboto Flex'; }</style></head><body><p>hello Ā</p></body></html>`;
const result = await injectDeterministicFontFaces(html, { failClosedFontFetch: true });
const faces = result.match(/@font-face\s*\{[^}]+\}/g) ?? [];
expect(faces).toHaveLength(2);
for (const [index, face] of faces.entries()) {
expect(face).toContain(`font-style: ${style};`);
expect(face).toContain("font-weight: 100 1000;");
expect(face).toContain(`unicode-range: ${subsets[index]};`);
expect(face).toContain(
Buffer.from(`https://fonts.gstatic.com/oblique-${index}.woff2`).toString("base64"),
);
}
},
);
@@ -13,6 +13,7 @@ import { parseHTML } from "linkedom";
import postcss, { type AtRule, type Declaration, type Rule } from "postcss";
import { EMBEDDED_FONT_DATA } from "./fontData.generated.js";
import { fontToDataUri } from "./fontCompression.js";
import { authoredGoogleFontStylesheets, withPageText } from "./authoredGoogleFonts.js";
type FontFaceSpec = {
weight: string;
@@ -700,8 +701,9 @@ async function buildFontFaceCss(
continue;
}
// Path 3: locate font on the local filesystem, compress, and embed.
if (options.allowSystemFontCapture) {
// The page already named the file. A font found on this machine is a
// different file, and the render machines do not have it.
if (options.allowSystemFontCapture && !pageNamedThisFile(originalCaseFamily, options)) {
const variants = locateSystemFontVariants(originalCaseFamily);
if (variants.length > 0) {
let totalBytes = 0;
@@ -904,6 +906,8 @@ interface InternalFontFetchOptions {
abortSignal?: AbortSignal;
retryPolicy: FontFetchRetryPolicy;
retryDeadlineMs: number;
/** Google stylesheet URL the page already wrote, keyed by normalized family name. */
authoredStylesheets: ReadonlyMap<string, readonly string[]>;
}
/**
@@ -1169,12 +1173,20 @@ function fetchGoogleFontCss(
return raceAgainstAbort(shared, options.abortSignal);
}
function pageNamedThisFile(familyName: string, options: InternalFontFetchOptions): boolean {
return options.authoredStylesheets.has(normalizeFamilyName(familyName.replace(/\+/g, " ")));
}
function declaredFaceFamily(block: string): string | undefined {
const found = /font-family:\s*['"]([^'"]+)['"]/i.exec(block);
return found?.[1];
}
async function fetchGoogleFont(
familyName: string,
options: InternalFontFetchOptions,
fontText?: string,
): Promise<GoogleFontFace[]> {
const slug = fontSlug(familyName);
// Agents sometimes copy the `family=` value from a Google Fonts URL into
// CSS, where `+` remains a literal character instead of being decoded as a
// space. Resolve that URL-style spelling through the canonical Google family
@@ -1183,23 +1195,40 @@ async function fetchGoogleFont(
const googleFamilyName = familyName.replace(/\+/g, " ");
const encodedFamily = encodeURIComponent(googleFamilyName);
const textParam = fontText ? `&text=${encodeURIComponent(fontText)}` : "";
const url = `https://fonts.googleapis.com/css2?family=${encodedFamily}:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700${textParam}`;
// Bundled families need the full supplement, including italics a page's
// link may omit. Other families need the authored URL to preserve axes
// such as optical size instead of replacing them with a weight-only face.
const normalizedFamily = normalizeFamilyName(googleFamilyName);
const authoredStylesheet = FONT_ALIASES[normalizedFamily]
? undefined
: options.authoredStylesheets.get(normalizedFamily);
// `text=` asks Google for only the characters on the page. A CJK family
// without it is a hundred files, and the compile's font budget is 20s.
const urls = authoredStylesheet
? authoredStylesheet.map((url) => withPageText(url, fontText))
: [
`https://fonts.googleapis.com/css2?family=${encodedFamily}:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700${textParam}`,
];
const faces: GoogleFontFace[] = [];
for (const url of urls) {
faces.push(...(await fetchGoogleFontStylesheet(googleFamilyName, url, options)));
}
return faces;
}
let cssText: string;
async function readGoogleFontStylesheet(
familyName: string,
url: string,
options: InternalFontFetchOptions,
): Promise<string | null> {
try {
const cssResult = await fetchGoogleFontCss(url, familyName, options);
if (!cssResult.ok) {
// 4xx is a *deterministic* answer from Google Fonts that this
// family is not served (e.g. HTTP 400 for "Segoe UI", "Arial",
// "Futura" — names absent from Google's catalog) or is misnamed.
// The render falls back to embedded faces / the composition's
// font-family chain; we return [] in both modes. 5xx (and other
// transient upstream failures) could return faces on retry, which
// would break the byte-identical-retry contract distributed
// renders rely on — those still fail closed when requested.
return [];
// Missing families are deterministic; transient failures follow the
// caller's retry and fail-closed policy.
return null;
}
cssText = cssResult.body;
return cssResult.body;
} catch (err) {
// Rethrow typed error untouched. Network / DNS / fetch-throws are
// non-deterministic infrastructure failures — wrapped when failClosed
@@ -1209,25 +1238,52 @@ async function fetchGoogleFont(
if (options.failClosedFontFetch) {
throw fontFetchError(familyName, url, "Google Fonts CSS", { error: err });
}
return [];
return null;
}
}
type GoogleFontSource = Omit<GoogleFontFace, "dataUri"> & { url: string };
function parseGoogleFontSource(
match: RegExpMatchArray,
familyName: string,
): GoogleFontSource | null {
const declared = declaredFaceFamily(match[0]);
if (declared && normalizeFamilyName(declared) !== normalizeFamilyName(familyName)) return null;
const url = match[3];
if (!url) return null;
return {
style: match[1] || "normal",
weight: (match[2] || "400").replace(/\s+/g, " "),
url,
unicodeRange: match[4]?.trim() || undefined,
};
}
async function fetchGoogleFontStylesheet(
familyName: string,
url: string,
options: InternalFontFetchOptions,
): Promise<GoogleFontFace[]> {
const slug = fontSlug(familyName);
const cssText = await readGoogleFontStylesheet(familyName, url, options);
if (!cssText) return [];
// Parse @font-face blocks from the CSS response. The optional trailing
// capture grabs each face's `unicode-range` (Google emits it after `src`)
// so the injected face only claims the codepoints the subset actually
// covers — without it the face would advertise full coverage it lacks.
// A variable face is `font-weight: 400 900`: one file for every weight in
// the span. Keeping only the first number leaves 600/700/800 on the network.
const faceRegex =
/@font-face\s*\{[^}]*font-style:\s*(normal|italic)[^}]*font-weight:\s*(\d+)[^}]*src:\s*url\(([^)]+)\)\s*format\(['"]woff2['"]\)(?:[^}]*?unicode-range:\s*([^;}]+))?[^}]*\}/gi;
/@font-face\s*\{[^}]*font-style:\s*(normal|italic|oblique(?:\s+-?\d+(?:\.\d+)?deg){0,2})\s*;[^}]*font-weight:\s*(\d+(?:\s+\d+)?)[^}]*src:\s*url\(([^)]+)\)\s*format\(['"]woff2['"]\)(?:[^}]*?unicode-range:\s*([^;}]+))?[^}]*\}/gi;
const faces: GoogleFontFace[] = [];
for (const match of cssText.matchAll(faceRegex)) {
const style = match[1] || "normal";
const weight = match[2] || "400";
const woff2Url = match[3] || "";
const unicodeRange = match[4]?.trim() || undefined;
if (!woff2Url) continue;
const source = parseGoogleFontSource(match, familyName);
if (!source) continue;
const { weight, style, unicodeRange, url: woff2Url } = source;
const cachePath = cachedWoff2Path(slug, weight, style, subsetToken(woff2Url));
const dataUri = await ensureWoff2DataUri(
@@ -1474,6 +1530,7 @@ export async function injectDeterministicFontFaces(
abortSignal: options.abortSignal,
retryPolicy,
retryDeadlineMs: Date.now() + retryPolicy.maxElapsedMs,
authoredStylesheets: authoredGoogleFontStylesheets(html),
};
const existingFaces = extractExistingFontFaces(html);