fix(fonts): cover rendered case variants in subsets

This commit is contained in:
Miguel Ángel
2026-08-26 01:11:38 +00:00
parent 3202f3fb87
commit 744146eb6e
2 changed files with 58 additions and 7 deletions
@@ -39,4 +39,47 @@ describe("Google Fonts text subsetting", () => {
expect(new URL(requestedUrl).searchParams.get("text")).toContain("旅行");
});
it("includes case variants for transformed supplemental alias weights", async () => {
let requestedUrl = "";
const fetchImpl = (async (input: unknown) => {
requestedUrl = String(input);
return new Response("", { status: 400 });
}) as unknown as typeof fetch;
await injectDeterministicFontFaces(
`<!doctype html><html><head><style>
h1 { font-family: "Inter", sans-serif; font-weight: 800; text-transform: uppercase; }
</style></head><body><h1>Your Kidney Transplant:<br/>What Happens Next</h1></body></html>`,
{ fetchImpl, allowSystemFontCapture: false },
);
const text = new URL(requestedUrl).searchParams.get("text") ?? "";
for (const character of new Set("YOUR KIDNEY TRANSPLANT:WHAT HAPPENS NEXT")) {
expect(text).toContain(character);
}
});
it("falls back to the full font when case closure exceeds the text URL budget", async () => {
let requestedUrl = "";
const fetchImpl = (async (input: unknown) => {
requestedUrl = String(input);
return new Response("", { status: 400 });
}) as unknown as typeof fetch;
const caseChangingCharacters = Array.from({ length: 0x500 }, (_, index) =>
String.fromCodePoint(index),
)
.filter((character) => character.toUpperCase() !== character.toLowerCase())
.slice(0, 300)
.join("");
await injectDeterministicFontFaces(
`<!doctype html><html><head><style>
p { font-family: "Inter", sans-serif; font-weight: 800; text-transform: uppercase; }
</style></head><body><p>${caseChangingCharacters}</p></body></html>`,
{ fetchImpl, allowSystemFontCapture: false },
);
expect(new URL(requestedUrl).searchParams.has("text")).toBe(false);
});
});
@@ -1193,18 +1193,26 @@ export interface InjectDeterministicFontFacesOptions {
}
// Keep the complete CSS request under the broadly supported ~2 KB URL limit.
// Using unique source characters covers static text plus strings authored in
// scripts, while collapsing repeated prose and base64 assets to a tiny set.
// Using unique source/decoded characters plus deterministic case variants covers
// static text, strings authored in scripts, and CSS case transforms while
// collapsing repeated prose and base64 assets to a tiny set.
const GOOGLE_FONTS_TEXT_MAX_ENCODED_LENGTH = 1_700;
function extractGoogleFontsText(html: string): string | undefined {
const { document } = parseHTML(html);
const decodedBodyText = document.body?.textContent ?? "";
const uniqueCharacters = [...new Set([...Array.from(html), ...Array.from(decodedBodyText)])].join(
"",
);
return encodeURIComponent(uniqueCharacters).length <= GOOGLE_FONTS_TEXT_MAX_ENCODED_LENGTH
? uniqueCharacters
const characters = [...Array.from(html), ...Array.from(decodedBodyText)];
const uniqueCharacters = new Set<string>();
for (const character of characters) {
uniqueCharacters.add(character);
// CSS text-transform can render glyphs absent from the authored source.
for (const variant of `${character.toUpperCase()}${character.toLowerCase()}`) {
uniqueCharacters.add(variant);
}
}
const fontText = [...uniqueCharacters].join("");
return encodeURIComponent(fontText).length <= GOOGLE_FONTS_TEXT_MAX_ENCODED_LENGTH
? fontText
: undefined;
}