From 3866e5593b71b6478a73473a538b576441f7b9a1 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 6 Nov 2026 01:50:49 -0500 Subject: [PATCH 1/2] fix(core): accented Latin letters draw in a bundled family at its bundled weights --- .../deterministicFonts-bundledSubset.test.ts | 35 +++++++++++++++++++ packages/core/src/fonts/deterministicFonts.ts | 23 ++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts b/packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts index 181bd733b33..28f4df578dd 100644 --- a/packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts +++ b/packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts @@ -128,3 +128,38 @@ describe("bundled Latin precedence", () => { expect(bundled).toContain(`unicode-range: ${LATIN_RANGE};`); }); }); + +describe("variable supplement at bundled weights", () => { + it("also emits a variable face's Latin Extended subset at each bundled weight it spans", async () => { + const { injectDeterministicFontFaces } = await import("./deterministicFonts.js"); + const EXT = "U+0100-02BA, U+02BD-02C5, U+1E00-1EFF"; + const fetchImpl = (async (input: unknown) => { + const url = String(input); + if (url.startsWith("https://fonts.googleapis.com/")) { + return new Response(`@font-face { + font-family: 'Outfit'; font-style: normal; font-weight: 100 900; + src: url(https://fonts.gstatic.com/s/outfit/ext.woff2) format('woff2'); + unicode-range: ${EXT}; + }`); + } + return new Response("OUTFIT_LATIN_EXT_BYTES"); + }) as unknown as typeof fetch; + const html = `

čšćžđ ąęłńśźż řěůď ğışİ őű ășț ạảấ

`; + const result = await injectDeterministicFontFaces(html, { + fetchImpl, + allowSystemFontCapture: false, + }); + const faces = (result.match(/@font-face \{[\s\S]*?\}/g) ?? []).filter((face) => + face.includes(b64("OUTFIT_LATIN_EXT_BYTES")), + ); + const weights = faces.map((face) => /font-weight: ([\d ]+);/.exec(face)?.[1]); + expect(weights).toEqual(expect.arrayContaining(["400", "700", "900", "100 900"])); + for (const face of faces) expect(face).toContain(`unicode-range: ${EXT};`); + // Croatian, Polish, Czech, Turkish, Hungarian, Romanian and Vietnamese letters beyond Latin-1 are all in its range. + const inExt = (ch: string) => { + const cp = ch.codePointAt(0) ?? 0; + return (cp >= 0x100 && cp <= 0x2ba) || (cp >= 0x1e00 && cp <= 0x1eff); + }; + for (const ch of "čšćžđąęłńśźżřěůďğışİőűășțạảấ") expect(inExt(ch)).toBe(true); + }); +}); diff --git a/packages/core/src/fonts/deterministicFonts.ts b/packages/core/src/fonts/deterministicFonts.ts index ac42839ea80..239d4f213b6 100644 --- a/packages/core/src/fonts/deterministicFonts.ts +++ b/packages/core/src/fonts/deterministicFonts.ts @@ -688,6 +688,23 @@ function partitionWeightRuns( return runs; } +/** The bundled weights a variable face's range ("100 900") spans, as exact weights. */ +function bundledWeightsWithin( + weight: string, + style: string, + covered: ReadonlySet, +): string[] { + const range = /^(\d+)\s+(\d+)$/.exec(weight.trim()); + if (!range) return []; + const [low, high] = [Number(range[1]), Number(range[2])]; + return [...covered].flatMap((key) => { + const [value, coveredStyle] = key.split(":"); + return coveredStyle === style && Number(value) >= low && Number(value) <= high + ? [value ?? ""] + : []; + }); +} + // Path 1: pre-bundled fonts via FONT_ALIASES — emit embedded faces, // then fetch from Google Fonts to fill missing weights and character subsets. async function bundledFamilyFaceRules( @@ -744,6 +761,12 @@ async function bundledFamilyFaceRules( const last = run[run.length - 1]; if (!first || !last) continue; const weight = run.length > 1 ? `${first.weight} ${last.weight}` : first.weight; + // The browser picks the exact-weight bundled face (Latin only) before a range face, so a variable face that + // spans a bundled weight must also exist at that weight or its Latin Extended glyphs never draw there. + for (const exact of bundledWeightsWithin(weight, first.style, coveredWeights)) + rules.push( + buildFontFaceRule(emitFamily, first.dataUri, exact, first.style, first.unicodeRange), + ); rules.push( buildFontFaceRule(emitFamily, first.dataUri, weight, first.style, first.unicodeRange), ); From c8c021ff71b6a2018ce7e35d8d3290ced607606b Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 6 Nov 2026 01:56:31 -0500 Subject: [PATCH 2/2] fix(core): trim the comment added with the bundled-weight faces --- packages/core/src/fonts/deterministicFonts.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/fonts/deterministicFonts.ts b/packages/core/src/fonts/deterministicFonts.ts index 239d4f213b6..f33973d91cb 100644 --- a/packages/core/src/fonts/deterministicFonts.ts +++ b/packages/core/src/fonts/deterministicFonts.ts @@ -688,7 +688,6 @@ function partitionWeightRuns( return runs; } -/** The bundled weights a variable face's range ("100 900") spans, as exact weights. */ function bundledWeightsWithin( weight: string, style: string, @@ -761,8 +760,7 @@ async function bundledFamilyFaceRules( const last = run[run.length - 1]; if (!first || !last) continue; const weight = run.length > 1 ? `${first.weight} ${last.weight}` : first.weight; - // The browser picks the exact-weight bundled face (Latin only) before a range face, so a variable face that - // spans a bundled weight must also exist at that weight or its Latin Extended glyphs never draw there. + // The exact-weight bundled face (Latin only) wins over a range face: emit the range at each bundled weight too. for (const exact of bundledWeightsWithin(weight, first.style, coveredWeights)) rules.push( buildFontFaceRule(emitFamily, first.dataUri, exact, first.style, first.unicodeRange),