diff --git a/playground/fonts/font-bold-italic.ttf b/playground/fonts/font-bold-italic.ttf new file mode 100644 index 00000000..c22ad3a6 Binary files /dev/null and b/playground/fonts/font-bold-italic.ttf differ diff --git a/playground/fonts/font-bold.ttf b/playground/fonts/font-bold.ttf new file mode 100644 index 00000000..89b46e7b Binary files /dev/null and b/playground/fonts/font-bold.ttf differ diff --git a/playground/fonts/font-italic.ttf b/playground/fonts/font-italic.ttf new file mode 100644 index 00000000..2fc7d3e3 Binary files /dev/null and b/playground/fonts/font-italic.ttf differ diff --git a/playground/index.css b/playground/index.css index ad9f5905..68e96073 100644 --- a/playground/index.css +++ b/playground/index.css @@ -1,9 +1,33 @@ @font-face { font-family: 'Poppins variant'; font-display: swap; + font-weight: normal; src: url('/fonts/font.ttf') format('truetype'); } +@font-face { + font-family: 'Poppins variant'; + font-display: swap; + font-weight: 700; + src: url('/fonts/font-bold.ttf') format('truetype'); +} + +@font-face { + font-family: 'Poppins variant'; + font-display: swap; + font-weight: normal; + font-style: italic; + src: url('/fonts/font-italic.ttf') format('truetype'); +} + +@font-face { + font-family: 'Poppins variant'; + font-display: swap; + font-weight: 700; + font-style: italic; + src: url('/fonts/font-bold-italic.ttf') format('truetype'); +} + @font-face { font-family: 'Roboto'; font-display: swap; @@ -11,6 +35,14 @@ format('woff2'); } +@font-face { + font-family: 'Roboto'; + font-weight: 700; + font-display: swap; + src: url('https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2') + format('woff2'); +} + @font-face { font-family: 'Inter'; font-display: swap; @@ -18,6 +50,14 @@ format('woff2'); } +@font-face { + font-family: 'Inter'; + font-weight: 700; + font-display: swap; + src: url('https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuFuYAZ9hiJ-Ek-_EeA.woff2') + format('woff2'); +} + :root { /* Adding this manually for now */ --someFont: 'Poppins variant', 'Poppins variant fallback'; diff --git a/playground/index.html b/playground/index.html index f78af6c0..625aaa24 100644 --- a/playground/index.html +++ b/playground/index.html @@ -13,19 +13,19 @@

A headline

A subheading

- Id occaecat labore et adipisicing excepteur consequat et culpa pariatur quis qui officia non + Id occaecat labore et adipisicing excepteur consequat et culpa pariatur quis qui officia non cillum. Adipisicing aliquip occaecat non est minim nulla esse. Mollit in ex esse Lorem consectetur elit consequat quis adipisicing enim et culpa. Irure nostrud laboris consequat veniam dolor quis ullamco sint.

- Consequat elit anim ex mollit cillum eiusmod voluptate. Sunt dolor Lorem proident esse amet + Consequat elit anim ex mollit cillum eiusmod voluptate. Sunt dolor Lorem proident esse amet duis velit amet consectetur qui voluptate sint adipisicing. Voluptate nostrud non quis laborum veniam commodo duis laboris dolore veniam commodo amet. Officia cillum est sunt anim ullamco tempor ipsum dolore nisi dolore ut. Velit eu minim minim non laborum exercitation.

- Reprehenderit fugiat sit proident id laboris amet nulla quis est dolor consequat ad eiusmod. + Reprehenderit fugiat sit proident id laboris amet nulla quis est dolor consequat ad eiusmod. Mollit laborum cupidatat nisi commodo enim eiusmod sit. Est dolor ipsum nulla pariatur pariatur esse ea est labore fugiat eu velit. Minim ex sunt Lorem nisi non officia.
diff --git a/src/css.ts b/src/css.ts index fc99bac9..a31c165d 100644 --- a/src/css.ts +++ b/src/css.ts @@ -18,6 +18,36 @@ const QUOTES_RE = createRegExp( ['g'], ) +const PROPERTIES_WHITELIST = ['font-weight', 'font-style', 'font-stretch'] + +interface FontProperties { + 'font-weight'?: string + 'font-style'?: string + 'font-stretch'?: string +} + +function parseFontProperties(css: string): FontProperties { + return PROPERTIES_WHITELIST.reduce( + (properties: FontProperties, property: string) => { + const value = css.match(createPropertyRE(property))?.groups.value + if (value) { + properties[property as keyof FontProperties] = value + } + + return properties + }, + {}, + ) +} + +function createPropertyRE(property: string) { + return createRegExp( + exactly(`${property}:`) + .and(whitespace.optionally()) + .and(charNotIn(';}').times.any().as('value')), + ) +} + const FAMILY_RE = createRegExp( exactly('font-family:') .and(whitespace.optionally()) @@ -38,19 +68,23 @@ const URL_RE = createRegExp( export const withoutQuotes = (str: string) => str.trim().replace(QUOTES_RE, '') -export function* parseFontFace( - css: string, -): Generator<{ family?: string, source?: string }> { +export function* parseFontFace(css: string): Generator<{ + family?: string + source?: string + properties?: FontProperties +}> { const fontFamily = css.match(FAMILY_RE)?.groups.fontFamily const family = withoutQuotes(fontFamily?.split(',')[0] || '') + const properties = parseFontProperties(css) for (const match of css.matchAll(SOURCE_RE)) { const sources = match.groups.src?.split(',') for (const entry of sources /* c8 ignore next */ || []) { for (const url of entry.matchAll(URL_RE)) { const source = withoutQuotes(url.groups?.url || '') - if (source) - yield { family, source } + if (source) { + yield { family, source, ...properties } + } } } } diff --git a/src/transform.ts b/src/transform.ts index b8397a19..a513448a 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -123,7 +123,7 @@ export const FontaineTransform = createUnplugin( faceRanges.push([match.index, match.index + matchContent.length]) - for (const { family, source } of parseFontFace(matchContent)) { + for (const { family, source, ...properties } of parseFontFace(matchContent)) { if (!family) continue if (!supportedExtensions.some(e => source?.endsWith(e))) @@ -148,6 +148,7 @@ export const FontaineTransform = createUnplugin( name: fallbackName(family), font: fallback, metrics: fallbackMetrics, + ...properties, }) cssContext.value += fontFace s.appendLeft(match.index, fontFace) diff --git a/test/e2e.spec.ts b/test/e2e.spec.ts index 793e5bb3..ab8f4727 100644 --- a/test/e2e.spec.ts +++ b/test/e2e.spec.ts @@ -16,7 +16,7 @@ describe('fontaine', () => { // @ts-expect-error there must be a file or we _want_ a test failure const css = await readFile(join(assetsDir, cssFile), 'utf-8') expect(css.replace(/\.\w+\.woff2/g, '.woff2')).toMatchInlineSnapshot(` - "@font-face{font-family:Poppins variant fallback;src:local("Segoe UI");size-adjust:112.7753%;ascent-override:93.1055%;descent-override:31.0352%;line-gap-override:8.8672%}@font-face{font-family:Poppins variant fallback;src:local("Arial");size-adjust:112.1577%;ascent-override:93.6182%;descent-override:31.2061%;line-gap-override:8.916%}@font-face{font-family:Poppins variant;font-display:swap;src:url(/assets/font-CTKNfV9P.ttf) format("truetype")}@font-face{font-family:Roboto fallback;src:local("Segoe UI");size-adjust:100.3304%;ascent-override:92.4679%;descent-override:24.3337%;line-gap-override:0%}@font-face{font-family:Roboto fallback;src:local("Arial");size-adjust:99.7809%;ascent-override:92.9771%;descent-override:24.4677%;line-gap-override:0%}@font-face{font-family:Roboto;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format("woff2")}@font-face{font-family:Inter fallback;src:local("Segoe UI");size-adjust:107.7093%;ascent-override:89.9412%;descent-override:22.3946%;line-gap-override:0%}@font-face{font-family:Inter fallback;src:local("Arial");size-adjust:107.1194%;ascent-override:90.4365%;descent-override:22.518%;line-gap-override:0%}@font-face{font-family:Inter;font-display:swap;src:url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format("woff2")}:root{--someFont: "Poppins variant", "Poppins variant fallback"}h1{font-family:Poppins variant,Poppins variant fallback,sans-serif}.roboto{font-family:Roboto,Roboto fallback,Arial,Helvetica,sans-serif}p{font-family:Poppins variant,Poppins variant fallback}div{font-family:var(--someFont)}.inter{font-family:Inter,Inter fallback} + "@font-face{font-family:Poppins variant fallback;src:local("Segoe UI");size-adjust:112.7753%;ascent-override:93.1055%;descent-override:31.0352%;line-gap-override:8.8672%;font-weight:400}@font-face{font-family:Poppins variant fallback;src:local("Arial");size-adjust:112.1577%;ascent-override:93.6182%;descent-override:31.2061%;line-gap-override:8.916%;font-weight:400}@font-face{font-family:Poppins variant;font-display:swap;font-weight:400;src:url(/assets/font-CTKNfV9P.ttf) format("truetype")}@font-face{font-family:Poppins variant fallback;src:local("Segoe UI");size-adjust:116.1586%;ascent-override:90.3937%;descent-override:30.1312%;line-gap-override:8.6089%;font-weight:700}@font-face{font-family:Poppins variant fallback;src:local("Arial");size-adjust:115.5225%;ascent-override:90.8914%;descent-override:30.2971%;line-gap-override:8.6563%;font-weight:700}@font-face{font-family:Poppins variant;font-display:swap;font-weight:700;src:url(/assets/font-bold-CNzhNbUJ.ttf) format("truetype")}@font-face{font-family:Poppins variant fallback;src:local("Segoe UI");size-adjust:113.9031%;ascent-override:92.1836%;descent-override:30.7279%;line-gap-override:8.7794%;font-weight:400;font-style:italic}@font-face{font-family:Poppins variant fallback;src:local("Arial");size-adjust:113.2793%;ascent-override:92.6913%;descent-override:30.8971%;line-gap-override:8.8277%;font-weight:400;font-style:italic}@font-face{font-family:Poppins variant;font-display:swap;font-weight:400;font-style:italic;src:url(/assets/font-italic-CYgqeeDB.ttf) format("truetype")}@font-face{font-family:Poppins variant fallback;src:local("Segoe UI");size-adjust:116.8352%;ascent-override:89.8701%;descent-override:29.9567%;line-gap-override:8.5591%;font-weight:700;font-style:italic}@font-face{font-family:Poppins variant fallback;src:local("Arial");size-adjust:116.1954%;ascent-override:90.365%;descent-override:30.1217%;line-gap-override:8.6062%;font-weight:700;font-style:italic}@font-face{font-family:Poppins variant;font-display:swap;font-weight:700;font-style:italic;src:url(/assets/font-bold-italic-BV883OaJ.ttf) format("truetype")}@font-face{font-family:Roboto fallback;src:local("Segoe UI");size-adjust:100.3304%;ascent-override:92.4679%;descent-override:24.3337%;line-gap-override:0%}@font-face{font-family:Roboto fallback;src:local("Arial");size-adjust:99.7809%;ascent-override:92.9771%;descent-override:24.4677%;line-gap-override:0%}@font-face{font-family:Roboto;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format("woff2")}@font-face{font-family:Roboto fallback;src:local("Segoe UI");size-adjust:100.3304%;ascent-override:92.4679%;descent-override:24.3337%;line-gap-override:0%;font-weight:700}@font-face{font-family:Roboto fallback;src:local("Arial");size-adjust:99.7809%;ascent-override:92.9771%;descent-override:24.4677%;line-gap-override:0%;font-weight:700}@font-face{font-family:Roboto;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2) format("woff2")}@font-face{font-family:Inter fallback;src:local("Segoe UI");size-adjust:107.7093%;ascent-override:89.9412%;descent-override:22.3946%;line-gap-override:0%}@font-face{font-family:Inter fallback;src:local("Arial");size-adjust:107.1194%;ascent-override:90.4365%;descent-override:22.518%;line-gap-override:0%}@font-face{font-family:Inter;font-display:swap;src:url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format("woff2")}@font-face{font-family:Inter fallback;src:local("Segoe UI");size-adjust:107.7093%;ascent-override:89.9412%;descent-override:22.3946%;line-gap-override:0%;font-weight:700}@font-face{font-family:Inter fallback;src:local("Arial");size-adjust:107.1194%;ascent-override:90.4365%;descent-override:22.518%;line-gap-override:0%;font-weight:700}@font-face{font-family:Inter;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuFuYAZ9hiJ-Ek-_EeA.woff2) format("woff2")}:root{--someFont: "Poppins variant", "Poppins variant fallback"}h1{font-family:Poppins variant,Poppins variant fallback,sans-serif}.roboto{font-family:Roboto,Roboto fallback,Arial,Helvetica,sans-serif}p{font-family:Poppins variant,Poppins variant fallback}div{font-family:var(--someFont)}.inter{font-family:Inter,Inter fallback} " `) }) diff --git a/test/index.spec.ts b/test/index.spec.ts index 7b3b5286..8c8ca0d0 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -181,6 +181,45 @@ describe('parseFontFace', () => { } `) }) + it('should extract weight/style/stretch', () => { + const result = parseFontFace( + `@font-face { + font-family: Roboto; + font-weight: 700; + font-style: italic; + src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"); + font-stretch: condensed; + }`, + ).next().value + + expect(result).toMatchInlineSnapshot(` + { + "family": "Roboto", + "font-stretch": "condensed", + "font-style": "italic", + "font-weight": "700", + "source": "/fonts/OpenSans-Regular-webfont.woff2", + } + `) + }) + it('should handle invalid weight/style/stretch', () => { + const result = parseFontFace( + `@font-face { + font-family: Roboto; + font-weight; + font-style; + src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"); + font-stretch; + }`, + ).next().value + + expect(result).toMatchInlineSnapshot(` + { + "family": "Roboto", + "source": "/fonts/OpenSans-Regular-webfont.woff2", + } + `) + }) it('should handle incomplete font-faces', () => { for (const result of parseFontFace( `@font-face {