diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 63bbe7c..51e96e6 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -19,6 +19,7 @@ export default defineNuxtConfig({ { name: 'Barlow Semi Condensed', provider: 'adobe' }, { name: 'Barlow', preload: true }, { name: 'Roboto Mono', provider: 'fontsource' }, + { name: 'Recursive', provider: 'google', variableAxis: { slnt: ['-15..0'], CASL: ['0..1'], CRSV: ['0..1'], MONO: ['0..1'] } }, ], adobe: { id: ['sij5ufr', 'grx7wdj'], diff --git a/playground/pages/providers/google.vue b/playground/pages/providers/google.vue index 996f318..5ac4292 100644 --- a/playground/pages/providers/google.vue +++ b/playground/pages/providers/google.vue @@ -1,20 +1,27 @@ diff --git a/src/css/parse.ts b/src/css/parse.ts index 685f61f..c446205 100644 --- a/src/css/parse.ts +++ b/src/css/parse.ts @@ -67,9 +67,24 @@ export function extractFontFaceData(css: string, family?: string): NormalizedFon if (child.type === 'Declaration' && child.property in extractableKeyMap) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const value = extractCSSValue(child) as any - data[extractableKeyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value + if (child.property === 'src' && !Array.isArray(value)) { + // @ts-expect-error Type mismatch caused by any + data[extractableKeyMap[child.property]!] = [value] + } + else if (child.property === 'font-style' && Array.isArray(value)) { + // looks like css-tree like to process dimension values first. we have to manually move the last element to front. + // @ts-expect-error Type mismatch caused by any + data[extractableKeyMap[child.property]!] = [ + value.pop(), + ...value, + ].join(' ') + } + else { + data[extractableKeyMap[child.property]!] = value + } } } + console.log(data) fontFaces.push(data as NormalizedFontFaceData) } @@ -118,6 +133,9 @@ function extractCSSValue(node: Declaration) { if (child.type === 'Number') { values.push(Number(child.value)) } + if (child.type === 'Dimension') { + values.push(child.value + child.unit) + } } if (buffer) { diff --git a/src/module.ts b/src/module.ts index 855c450..e5b41f7 100644 --- a/src/module.ts +++ b/src/module.ts @@ -178,13 +178,18 @@ export default defineNuxtModule({ } // Respect custom weights, styles and subsets options - const defaults = { ...normalizedDefaults, fallbacks } + const defaults: Omit & { variableAxis?: { [key: string]: string[] }, fallbacks: string[] } = { ...normalizedDefaults, fallbacks } for (const key of ['weights', 'styles', 'subsets'] as const) { if (override?.[key]) { defaults[key as 'weights'] = override[key]!.map(v => String(v)) } } + // Respect custom variable axis options + if (override?.variableAxis) { + defaults.variableAxis = override.variableAxis + } + // Handle explicit provider if (override?.provider) { if (override.provider in providers) { diff --git a/src/providers/google.ts b/src/providers/google.ts index c13ef57..550cf1c 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -70,6 +70,23 @@ const styleMap = { oblique: '1', normal: '0', } + +// Google wants lowercase letters to be in front of uppercase letters. +function googleFlavoredSorting(a: string, b: string) { + const isALowercase = a.charAt(0) === a.charAt(0).toLowerCase() + const isBLowercase = b.charAt(0) === b.charAt(0).toLowerCase() + + if (isALowercase && !isBLowercase) { + return -1 + } + else if (!isALowercase && isBLowercase) { + return 1 + } + else { + return a.localeCompare(b) + } +} + async function getFontDetails(family: string, variants: ResolveFontFacesOptions) { const font = fonts.find(font => font.family === family)! const styles = [...new Set(variants.styles.map(i => styleMap[i]))].sort() @@ -81,7 +98,31 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) if (weights.length === 0 || styles.length === 0) return [] - const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)).sort() + const resolvedAxis = [] + let resolvedVariants: string[] = [] + + for (const axis of ['wght', 'ital', ...Object.keys(variants.variableAxis ?? {})].sort(googleFlavoredSorting)) { + let axisValue: string[] | undefined + if (axis === 'wght') { + axisValue = weights + } + else if (axis === 'ital') { + axisValue = styles + } + else { + axisValue = variants.variableAxis![axis as keyof typeof variants] + } + + if (axisValue) { + if (resolvedVariants.length === 0) { + resolvedVariants = axisValue + } + else { + resolvedVariants = resolvedVariants.flatMap(v => [...axisValue!].map(o => [v, o].join(','))).sort() + } + resolvedAxis.push(axis) + } + } let css = '' @@ -90,7 +131,7 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) baseURL: 'https://fonts.googleapis.com', headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] }, query: { - family: family + ':' + 'ital,wght@' + resolvedVariants.join(';'), + family: family + ':' + resolvedAxis.join(',') + '@' + resolvedVariants.join(';'), }, }) } diff --git a/src/types.ts b/src/types.ts index 8fb0264..9949932 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,6 +62,8 @@ export interface ResolveFontFacesOptions { // TODO: improve support and support unicode range subsets: string[] fallbacks: string[] + // Variable axis + variableAxis?: { [key: string]: string[] } } export interface FontProvider> { diff --git a/test/basic.test.ts b/test/basic.test.ts index 994b7ec..7203de6 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -91,6 +91,7 @@ describe('providers', async () => { const poppins = extractFontFaces('Poppins', html) const raleway = extractFontFaces('Raleway', html) const press = extractFontFaces('Press Start 2P', html) + const recursive = extractFontFaces('Recursive', html) expect(poppins.length).toMatchInlineSnapshot(`6`) // No `@font-face` is generated for second/fallback fonts expect(raleway.length).toMatchInlineSnapshot(`0`) @@ -105,6 +106,22 @@ describe('providers', async () => { "@font-face{font-family:"Press Start 2P";src:local("Press Start 2P Regular"),local("Press Start 2P"),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:400;font-style:normal}", ] `) + expect(recursive).toMatchInlineSnapshot(` + [ + "@font-face{font-family:Recursive;src:local("Recursive Variable"),url(/_fonts/file.woff2) format(woff2);font-display:swap;unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-weight:300 1000;font-style:oblique 0deg 15deg}", + "@font-face{font-family:Recursive;src:local("Recursive Variable"),url(/_fonts/file.woff2) format(woff2);font-display:swap;unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-weight:300 1000;font-style:oblique 0deg 15deg}", + "@font-face{font-family:Recursive;src:local("Recursive Variable"),url(/_fonts/file.woff2) format(woff2);font-display:swap;unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-weight:300 1000;font-style:oblique 0deg 15deg}", + "@font-face{font-family:Recursive;src:local("Recursive Variable"),url(/_fonts/file.woff2) format(woff2);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight:300 1000;font-style:oblique 0deg 15deg}", + "@font-face{font-family:Recursive;src:local("Recursive Light"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:300;font-style:normal}", + "@font-face{font-family:Recursive;src:local("Recursive Regular"),local("Recursive"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:400;font-style:normal}", + "@font-face{font-family:Recursive;src:local("Recursive Medium"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:500;font-style:normal}", + "@font-face{font-family:Recursive;src:local("Recursive SemiBold"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:600;font-style:normal}", + "@font-face{font-family:Recursive;src:local("Recursive Bold"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:700;font-style:normal}", + "@font-face{font-family:Recursive;src:local("Recursive ExtraBold"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:800;font-style:normal}", + "@font-face{font-family:Recursive;src:local("Recursive Black"),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff),url(/_fonts/file.woff) format(woff);font-display:swap;font-weight:900;font-style:normal}", + ] + `, + ) }) it('should allow overriding providers with `none`', async () => {