Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support font-weight, font-style and font-stretch @font-face properties #264

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Binary file added playground/fonts/font-bold-italic.ttf
Binary file not shown.
Binary file added playground/fonts/font-bold.ttf
Binary file not shown.
Binary file added playground/fonts/font-italic.ttf
Binary file not shown.
40 changes: 40 additions & 0 deletions playground/index.css
Original file line number Diff line number Diff line change
@@ -1,23 +1,63 @@
@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;
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2')
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;
src: url('https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2')
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';
Expand Down
6 changes: 3 additions & 3 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
<h1>A headline</h1>
<h2 class="roboto">A subheading</h2>
<p>
Id occaecat labore et adipisicing excepteur consequat et culpa pariatur quis qui officia non
Id occaecat labore et <em>adipisicing excepteur consequat</em> 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.
</p>
<p class="inter">
Consequat elit anim ex mollit cillum eiusmod voluptate. Sunt dolor Lorem proident esse amet
Consequat elit anim ex <strong>mollit cillum eiusmod voluptate</strong>. 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.
</p>
<div>
Reprehenderit fugiat sit proident id laboris amet nulla quis est dolor consequat ad eiusmod.
Reprehenderit fugiat sit <strong><em>proident id laboris amet nulla quis</em></strong> 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.
</div>
Expand Down
44 changes: 39 additions & 5 deletions src/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 }
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@

faceRanges.push([match.index, match.index + matchContent.length])

for (const { family, source } of parseFontFace(matchContent)) {
for (const { family, source, ...properties } of parseFontFace(matchContent)) {

Check warning on line 126 in src/transform.ts

View check run for this annotation

Codecov / codecov/patch

src/transform.ts#L126

Added line #L126 was not covered by tests
if (!family)
continue
if (!supportedExtensions.some(e => source?.endsWith(e)))
Expand All @@ -148,6 +148,7 @@
name: fallbackName(family),
font: fallback,
metrics: fallbackMetrics,
...properties,

Check warning on line 151 in src/transform.ts

View check run for this annotation

Codecov / codecov/patch

src/transform.ts#L151

Added line #L151 was not covered by tests
})
cssContext.value += fontFace
s.appendLeft(match.index, fontFace)
Expand Down
2 changes: 1 addition & 1 deletion test/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
"
`)
})
Expand Down
39 changes: 39 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down