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 {