From 59bce04f2ec844ffcd89d14db6ed3e685288eaa1 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Thu, 17 Oct 2024 15:49:46 +0800 Subject: [PATCH] feat(google): allow configuring variable axes (#35) --- src/providers/google.ts | 50 ++++++++++++++++++++++++++++++++--- test/providers/google.test.ts | 29 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 4b2737f..c4c1078 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -5,7 +5,20 @@ import { extractFontFaceData } from '../css/parse' import { $fetch } from '../fetch' import { defineFontProvider } from '../utils' -export default defineFontProvider('google', async (_options, ctx) => { +type VariableAxis = 'opsz' | 'slnt' | 'wdth' | (string & {}) + +interface ProviderOption { + experimental?: { + /** + * Experimental: Setting variable axis configuration on a per-font basis. + */ + variableAxis?: { + [key: string]: Partial> + } + } +} + +export default defineFontProvider('google', async (_options = {}, ctx) => { const googleFonts = await ctx.storage.getItem('google:meta.json', () => $fetch<{ familyMetadataList: FontIndexMeta[] }>('https://fonts.google.com/metadata/fonts', { responseType: 'json' }).then(r => r.familyMetadataList)) const styleMap = { @@ -34,7 +47,23 @@ export default defineFontProvider('google', async (_options, ctx) => { if (weights.length === 0 || styles.length === 0) return [] - const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)).sort() + const resolvedAxes = [] + let resolvedVariants: string[] = [] + + for (const axis of ['wght', 'ital', ...Object.keys(_options?.experimental?.variableAxis?.[family] ?? {})].sort(googleFlavoredSorting)) { + const axisValue = ({ + wght: weights, + ital: styles, + })[axis] ?? _options!.experimental!.variableAxis![family]![axis]!.map(v => Array.isArray(v) ? `${v[0]}..${v[1]}` : v) + + if (resolvedVariants.length === 0) { + resolvedVariants = axisValue + } + else { + resolvedVariants = resolvedVariants.flatMap(v => [...axisValue].map(o => [v, o].join(','))).sort() + } + resolvedAxes.push(axis) + } let css = '' @@ -43,7 +72,7 @@ export default defineFontProvider('google', async (_options, ctx) => { baseURL: 'https://fonts.googleapis.com', headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] }, query: { - family: `${family}:` + `ital,wght@${resolvedVariants.join(';')}`, + family: `${family}:${resolvedAxes.join(',')}@${resolvedVariants.join(';')}`, }, }) } @@ -64,6 +93,8 @@ export default defineFontProvider('google', async (_options, ctx) => { } }) +/** internal */ + interface FontIndexMeta { family: string subsets: string[] @@ -80,3 +111,16 @@ interface FontIndexMeta { defaultValue: number }> } + +// 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 Number(isBLowercase) - Number(isALowercase) + } + else { + return a.localeCompare(b) + } +} diff --git a/test/providers/google.test.ts b/test/providers/google.test.ts index 9708e5b..e8cc77f 100644 --- a/test/providers/google.test.ts +++ b/test/providers/google.test.ts @@ -36,4 +36,33 @@ describe('google', () => { expect(resolvedStyles).toMatchObject(styles) expect(resolvedWeights).toMatchObject(weights) }) + + it('supports variable axes', async () => { + const unifont = await createUnifont([providers.google({ + experimental: { + variableAxis: { + Recursive: { + slnt: [['-15', '0']], + CASL: [['0', '1']], + CRSV: ['1'], + MONO: [['0', '1']], + }, + }, + }, + })]) + + const { fonts } = await unifont.resolveFont('Recursive') + + const resolvedStyles = pickUniqueBy(fonts, fnt => fnt.style) + const resolvedWeights = pickUniqueBy(fonts, fnt => String(fnt.weight)) + + const styles = ['oblique 0deg 15deg', 'normal'] as ResolveFontOptions['styles'] + + // Variable wght and separate weights from 300 to 1000 + const weights = ['300,1000', ...([...Array.from({ length: 7 }).keys()].map(i => String(i * 100 + 300)))] + + expect(fonts).toHaveLength(11) + expect(resolvedStyles).toMatchObject(styles) + expect(resolvedWeights).toMatchObject(weights) + }) })