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

feat: support variable axis #141

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
15 changes: 11 additions & 4 deletions playground/pages/providers/google.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
<template>
<div>
<div>
<div class="poppins">
Poppins
</div>
<p>
<p class="press-start-2p">
Press Start 2P
</p>
<div class="resursive">
Recursive
</div>
</div>
</template>

<style scoped>
div {
.poppins {
font-family: 'Poppins', Raleway, sans-serif;
}

p {
.press-start-2p {
font-family: 'Press Start 2P', sans-serif;
}

.resursive {
font-family: 'Recursive';
}
</style>
20 changes: 19 additions & 1 deletion src/css/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,18 @@ export default defineNuxtModule<ModuleOptions>({
}

// Respect custom weights, styles and subsets options
const defaults = { ...normalizedDefaults, fallbacks }
const defaults: Omit<typeof normalizedDefaults, 'fallbacks'> & { 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) {
Expand Down
45 changes: 43 additions & 2 deletions src/providers/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 = ''

Expand All @@ -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(';'),
},
})
}
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FontProviderOptions = Record<string, unknown>> {
Expand Down
17 changes: 17 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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 () => {
Expand Down
Loading