diff --git a/src/css.ts b/src/css.ts index fc12f65f..374fe760 100644 --- a/src/css.ts +++ b/src/css.ts @@ -1,6 +1,6 @@ import type { Font } from '@capsizecss/unpack' import type { CssNode } from 'css-tree' -import { parse, walk } from 'css-tree' +import { generate, parse, walk } from 'css-tree' import { charIn, createRegExp } from 'magic-regexp' // See: https://github.com/seek-oss/capsize/blob/master/packages/core/src/round.ts @@ -39,14 +39,22 @@ const genericCSSFamilies = new Set([ 'fangsong', ]) +const fontProperties = new Set(['font-weight', 'font-style', 'font-stretch']) + +interface FontProperties { + 'font-weight'?: string + 'font-style'?: string + 'font-stretch'?: string +} + /** * Extracts font family and source information from a CSS @font-face rule using css-tree. * * @param {string} css - The CSS containing @font-face rules * @returns Array<{ family?: string, source?: string }> - Array of objects with font family and source information */ -export function parseFontFace(css: string | CssNode): Array<{ index: number, family: string, source?: string }> { - const families: Array<{ index: number, family: string, source?: string }> = [] +export function parseFontFace(css: string | CssNode): Array<{ index: number, family: string, source?: string, properties: FontProperties }> { + const families: Array<{ index: number, family: string, source?: string, properties: FontProperties }> = [] const ast = typeof css === 'string' ? parse(css, { positions: true }) : css walk(ast, { @@ -57,6 +65,7 @@ export function parseFontFace(css: string | CssNode): Array<{ index: number, fam let family: string | undefined const sources: string[] = [] + const properties: FontProperties = {} if (node.block) { walk(node.block, { @@ -75,6 +84,16 @@ export function parseFontFace(css: string | CssNode): Array<{ index: number, fam } } + if (fontProperties.has(declaration.property)) { + if (declaration.value.type === 'Value') { + for (const child of declaration.value.children) { + const hasValue = !!properties[declaration.property as keyof FontProperties] + properties[declaration.property as keyof FontProperties] ||= '' + properties[declaration.property as keyof FontProperties] += (hasValue ? ' ' : '') + generate(child) + } + } + } + if (declaration.property === 'src') { walk(declaration.value, { visit: 'Url', @@ -92,10 +111,10 @@ export function parseFontFace(css: string | CssNode): Array<{ index: number, fam if (family) { for (const source of sources) { - families.push({ index: node.loc!.start.offset, family, source }) + families.push({ index: node.loc!.start.offset, family, source, properties }) } if (!sources.length) { - families.push({ index: node.loc!.start.offset, family }) + families.push({ index: node.loc!.start.offset, family, properties }) } } }, diff --git a/src/transform.ts b/src/transform.ts index d03c2089..8edfbb7f 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -101,7 +101,7 @@ export const FontaineTransform = createUnplugin((options: FontaineTransformOptio const ast = parse(code, { positions: true }) - for (const { family, source, index } of parseFontFace(ast)) { + for (const { family, source, index, properties } of parseFontFace(ast)) { if (!supportedExtensions.some(e => source?.endsWith(e))) continue if (skipFontFaceGeneration(fallbackName(family))) @@ -125,6 +125,7 @@ export const FontaineTransform = createUnplugin((options: FontaineTransformOptio name: fallbackName(family), font: fallback, metrics: fallbackMetrics, + ...properties, }) cssContext.value += fontFace s.appendLeft(index, fontFace) diff --git a/test/index.spec.ts b/test/index.spec.ts index 5bb1d3a3..9ed973b4 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -175,6 +175,53 @@ describe('parseFontFace', () => { { "family": "Roboto", "index": 0, + "properties": {}, + "source": "/fonts/OpenSans-Regular-webfont.woff2", + } + `) + }) + + 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; + }`, + ) + + expect(result).toMatchInlineSnapshot(` + { + "family": "Roboto", + "index": 0, + "properties": { + "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; + }`, + ) + + expect(result).toMatchInlineSnapshot(` + { + "family": "Roboto", + "index": 0, + "properties": {}, "source": "/fonts/OpenSans-Regular-webfont.woff2", } `) @@ -204,6 +251,7 @@ describe('parseFontFace', () => { { "family": "Inter", "index": 0, + "properties": {}, "source": "./node_modules/inter-ui/Inter (web)/Inter-Regular.woff2", }, ] @@ -224,6 +272,7 @@ describe('parseFontFace', () => { { "family": "Arial", "index": 0, + "properties": {}, }, ] `) @@ -240,6 +289,7 @@ describe('parseFontFace', () => { { "family": "Arial", "index": 0, + "properties": {}, }, ] `) diff --git a/test/transform.spec.ts b/test/transform.spec.ts index 18a4d0f5..1f10e3ef 100644 --- a/test/transform.spec.ts +++ b/test/transform.spec.ts @@ -69,6 +69,47 @@ describe('fontaine transform', () => { `) }) + it('should add additional font properties to declarations', async () => { + expect(await transform(` + @font-face { + font-family: Poppins; + src: url('poppins.ttf'); + font-weight: 700; + font-style: oblique 10deg; + font-stretch: 75%; + }`)).toMatchInlineSnapshot(` + "@font-face { + font-family: "Poppins fallback"; + src: local("Segoe UI"); + size-adjust: 112.7753%; + ascent-override: 93.1055%; + descent-override: 31.0352%; + line-gap-override: 8.8672%; + font-weight: 700; + font-style: oblique 10deg; + font-stretch: 75%; + } + @font-face { + font-family: "Poppins fallback"; + src: local("Arial"); + size-adjust: 112.1577%; + ascent-override: 93.6182%; + descent-override: 31.2061%; + line-gap-override: 8.916%; + font-weight: 700; + font-style: oblique 10deg; + font-stretch: 75%; + } + @font-face { + font-family: Poppins; + src: url('poppins.ttf'); + font-weight: 700; + font-style: oblique 10deg; + font-stretch: 75%; + }" + `) + }) + it('should read metrics from URLs', async () => { await transform(` @font-face {