Skip to content

Commit a3796d1

Browse files
Togeticclaude
andcommitted
fix(fontless): render valid format() values in @font-face src
`parseFont` always sets the `format` key, so `key in src` stayed true even when `formatMap` had no entry for the extension, rendering the literal `format(undefined)` for cache-busted and extensionless URLs. Non-keyword values were also emitted unquoted. Per css-fonts-4 only the seven defined `<font-format>` keywords may appear bare — anything else, such as the legacy `woff2-variations` that unifont unquotes when parsing provider CSS, has to be a string. Both are silent font failures in Safari, which drops the whole `src` descriptor rather than just the offending component. `tech()` is now guarded on its value too, but stays unquoted — `<font-tech>` accepts keywords only, with no string alternative. Fixes #772 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 55a180c commit a3796d1

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

packages/fontless/src/css/render.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,21 @@ export function parseFont(font: string): RemoteFontSource | { name: string } {
6767
return { name: font }
6868
}
6969

70+
// https://drafts.csswg.org/css-fonts-4/#font-format-values
71+
const fontFormatKeywords = new Set(['collection', 'embedded-opentype', 'opentype', 'svg', 'truetype', 'woff', 'woff2'])
72+
7073
function renderFontSrc(sources: Exclude<FontSource, string>[]) {
7174
return sources.map((src) => {
7275
if ('url' in src) {
7376
let rendered = `url("${src.url}")`
74-
for (const key of ['format', 'tech'] as const) {
75-
if (key in src) {
76-
rendered += ` ${key}(${src[key]})`
77-
}
77+
// `format()` takes a `<font-format>` keyword, or a string for values outside
78+
// that set (such as the legacy `woff2-variations`).
79+
if (src.format) {
80+
rendered += ` format(${fontFormatKeywords.has(src.format) ? src.format : `"${src.format}"`})`
81+
}
82+
// `tech()` takes `<font-tech>` keywords only — never a string.
83+
if (src.tech) {
84+
rendered += ` tech(${src.tech})`
7885
}
7986
return rendered
8087
}

packages/fontless/test/render.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { generateFontFace } from '../src/css/render'
3+
import { generateFontFace, parseFont } from '../src/css/render'
44

55
describe('rendering @font-face', () => {
66
it('should add declarations for `font-family`', () => {
@@ -36,4 +36,42 @@ describe('rendering @font-face', () => {
3636
}"
3737
`)
3838
})
39+
it('should omit `format()` when the format is unknown', () => {
40+
// `parseFont` leaves `format` undefined when the extension is not recognised,
41+
// which covers cache-busted and extensionless provider URLs
42+
const css = generateFontFace('Inter', {
43+
src: [parseFont('/inter.woff2?v=3.19') as never, parseFont('https://fonts.example.com/l/font?kit=abc') as never],
44+
})
45+
expect(css).toMatchInlineSnapshot(`
46+
"@font-face {
47+
font-family: 'Inter';
48+
src: url("/inter.woff2?v=3.19"), url("https://fonts.example.com/l/font?kit=abc");
49+
font-display: swap;
50+
}"
51+
`)
52+
})
53+
it('should quote `format()` values that are not keywords', () => {
54+
const css = generateFontFace('Inter', {
55+
src: [{ url: '/inter.woff2', format: 'woff2-variations' }],
56+
})
57+
expect(css).toMatchInlineSnapshot(`
58+
"@font-face {
59+
font-family: 'Inter';
60+
src: url("/inter.woff2") format("woff2-variations");
61+
font-display: swap;
62+
}"
63+
`)
64+
})
65+
it('should render `tech()` as an unquoted keyword', () => {
66+
const css = generateFontFace('Trickster', {
67+
src: [{ url: '/trickster.otf', format: 'opentype', tech: 'color-COLRv1' }],
68+
})
69+
expect(css).toMatchInlineSnapshot(`
70+
"@font-face {
71+
font-family: 'Trickster';
72+
src: url("/trickster.otf") format(opentype) tech(color-COLRv1);
73+
font-display: swap;
74+
}"
75+
`)
76+
})
3977
})

0 commit comments

Comments
 (0)