From 396c561003b8d5af0728abde7ce0286ea67a6c30 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 19 Feb 2024 23:24:35 +0000 Subject: [PATCH 01/37] feat: add `local` and `google` providers --- src/providers/google.ts | 68 ++++++++++++++++++++ src/providers/local.ts | 137 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 src/providers/google.ts create mode 100644 src/providers/local.ts diff --git a/src/providers/google.ts b/src/providers/google.ts new file mode 100644 index 0000000..0c2febb --- /dev/null +++ b/src/providers/google.ts @@ -0,0 +1,68 @@ +import { $fetch } from 'ofetch' + +import type { FontProvider } from '../types' + +interface FontIndexMeta { + category: string + defSubset: string + defVariant: string + family: string + id: string + lastModified: string + popularity: number + subsets: string[] + variants: string[] + version: string +} + +interface FontDetail extends Omit { + variants: Array<{ + id: string + fontFamily: string + fontStyle: string + fontWeight: string + // + eot: string + woff: string + ttf: string + svg: string + woff2: string + local: string[] + }> + subsetMap: Record + storeID: string +} + + +let fonts: Array +export default { + async setup () { + // TODO: Fetch and cache possible Google fonts + fonts = await $fetch('https://gwfh.mranftl.com/api/fonts') + }, + async resolveFontFaces (fontFamily, defaults) { + const font = fonts.find(font => font.family === fontFamily) + if (!font) { return } + + const subsets = defaults.subsets.filter(subset => font.subsets.includes(subset)) + + const details = await $fetch(`https://gwfh.mranftl.com/api/fonts/${font.id}?subsets=${subsets.join(',')}`) + + return { + fonts: details.variants.map(variant => ({ + style: variant.fontStyle, + weight: variant.fontWeight, + // TODO: handle subset unicode ranges + // TODO: download/proxy URLs locally + src: [ + ...variant.local?.map(name => ({ name })) || [fontFamily], + ...variant.woff2 ? [{ url: variant.woff2, format: 'woff2' }] : [], + ...variant.woff ? [{ url: variant.woff, format: 'woff' }] : [], + ...variant.ttf ? [{ url: variant.ttf, format: 'truetype' }] : [], + ...variant.eot ? [{ url: variant.eot, format: 'embedded-opentype' }] : [], + ...variant.svg ? [{ url: variant.svg, format: 'svg' }] : [], + ] + })) + } + }, +} satisfies FontProvider diff --git a/src/providers/local.ts b/src/providers/local.ts new file mode 100644 index 0000000..03d1bf6 --- /dev/null +++ b/src/providers/local.ts @@ -0,0 +1,137 @@ +import { globby } from 'globby' +import { join, relative, resolve } from 'pathe' +import { filename } from 'pathe/utils' + +import type { FontFaceData, FontProvider } from '../types' +import { withLeadingSlash, withTrailingSlash } from 'ufo' + +const providerContext = { + rootPaths: [] as string[], + registry: {} as Record, +} + +export default { + async setup (nuxt) { + // Scan for all font files in public directories + for (const layer of nuxt.options._layers) { + const publicDir = join(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public') + const possibleFontFiles = await globby('**/*.{ttf,woff,woff2,eot,otf}', { + absolute: true, + cwd: publicDir + }) + providerContext.rootPaths.push(withTrailingSlash(publicDir)) + for (const file of possibleFontFiles) { + registerFont(file) + } + } + + // Sort rootPaths so we resolve to most specific path first + providerContext.rootPaths = providerContext.rootPaths.sort((a, b) => b.length - a.length) + + // Update registry when files change + nuxt.hook('builder:watch', (event, relativePath) => { + const path = resolve(nuxt.options.srcDir, relativePath) + if (event === 'add' && isFontFile(path)) { + registerFont(path) + } + if (event === 'unlink' && isFontFile(path)) { + unregisterFont(path) + } + }) + }, + resolveFontFaces (fontFamily, defaults) { + const fonts: FontFaceData[] = [] + + // Generate all possible permutations of font family names + // and resolve the first one that exists + for (const weight of defaults.weights) { + const isDefaultWeight = weight === 'normal' || weight === 400 + for (const style of defaults.styles) { + const isDefaultStyle = style === 'normal' + for (const subset of defaults.subsets) { + const isDefaultSubset = subset === 'latin' + const options = [ + [weight, style, subset], + [weight, subset, style], + [style, weight, subset], + [style, subset, weight], + [subset, weight, style], + [subset, style, weight], + ...isDefaultWeight ? [[style, subset], [subset, style]] : [], + ...isDefaultStyle ? [[weight, subset], [subset, weight]] : [], + ...isDefaultSubset ? [[weight, style], [style, weight]] : [], + ...(isDefaultStyle && isDefaultWeight) ? [[subset]] : [], + ...(isDefaultStyle && isDefaultWeight && isDefaultSubset) ? [[]] : [] + ] + for (const option of options) { + const resolved = lookupFont([fontFamily, ...option].join('-')) + if (resolved) { + fonts.push({ + src: resolved, + weight, + style, + }) + break + } + } + } + } + } + + if (fonts.length > 0) { + return { + fonts, + } + } + }, +} satisfies FontProvider + +const FONT_RE = /\.(ttf|woff|woff2|eot|otf)(\?[^.]+)?$/ +export const isFontFile = (id: string) => FONT_RE.test(id) + +function generateSlugs (path: string) { + const name = filename(path) + return [...new Set([ + name.toLowerCase(), + // Barlow-das324jasdf => barlow + name.replace(/-[\w\d]+$/, '').toLowerCase(), + // Barlow.das324jasdf => barlow + name.replace(/\.[\w\d]+$/, '').toLowerCase(), + // Open+Sans => open-sans + name.replace(/[+ ]+/g, '-').toLowerCase(), + // Open+Sans => opensans + name.replace(/[+ ]+/g, '').toLowerCase(), + ])] +} + +function registerFont (path: string) { + const slugs = generateSlugs(path) + for (const slug of slugs) { + providerContext.registry[slug] ||= [] + providerContext.registry[slug]!.push(path) + } +} + +function unregisterFont (path: string) { + const slugs = generateSlugs(path) + for (const slug of slugs) { + providerContext.registry[slug] ||= [] + providerContext.registry[slug] = providerContext.registry[slug]!.filter(p => p !== path) + } +} + +function lookupFont (family: string): string[] | undefined { + const priority = ['woff2', 'woff', 'ttf', 'otf', 'eot'] + const slug = family.replace(/[+ ]+/g, '-').toLowerCase() + const scannedFiles = providerContext.registry[slug]?.map(path => { + const base = providerContext.rootPaths.find(root => path.startsWith(root)) + return base ? withLeadingSlash(relative(base, path)) : path + }) + + return scannedFiles?.sort((a, b) => { + const extA = filename(a).split('.').pop()! + const extB = filename(b).split('.').pop()! + + return priority.indexOf(extA) - priority.indexOf(extB) + }) +} From 0d613d47f8c3b2675072e8fe2aff4a6953be0b78 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 09:16:22 +0000 Subject: [PATCH 02/37] feat(local): support looking up pascal-case fonts --- src/providers/local.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index 03d1bf6..d4c5e0b 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -64,7 +64,7 @@ export default { ...(isDefaultStyle && isDefaultWeight && isDefaultSubset) ? [[]] : [] ] for (const option of options) { - const resolved = lookupFont([fontFamily, ...option].join('-')) + const resolved = lookupFont([fontFamily, ...option].join('-')) || lookupFont([fontFamily, ...option].join('')) if (resolved) { fonts.push({ src: resolved, From bfae890c8631a6f9bcfa5086c56c91434b27e595 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 10:29:38 +0000 Subject: [PATCH 03/37] test: add more granular tests in suite --- src/providers/local.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index d4c5e0b..173ccb0 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -63,15 +63,17 @@ export default { ...(isDefaultStyle && isDefaultWeight) ? [[subset]] : [], ...(isDefaultStyle && isDefaultWeight && isDefaultSubset) ? [[]] : [] ] - for (const option of options) { - const resolved = lookupFont([fontFamily, ...option].join('-')) || lookupFont([fontFamily, ...option].join('')) - if (resolved) { - fonts.push({ - src: resolved, - weight, - style, - }) - break + for (const family of [fontFamily, fontFamily.replace(NON_WORD_RE, '-'), fontFamily.replace(NON_WORD_RE, '')]) { + for (const option of options) { + const resolved = lookupFont([family, ...option].join('-')) || lookupFont([family, ...option].join('')) + if (resolved) { + fonts.push({ + src: resolved, + weight, + style, + }) + break + } } } } @@ -87,9 +89,11 @@ export default { } satisfies FontProvider const FONT_RE = /\.(ttf|woff|woff2|eot|otf)(\?[^.]+)?$/ +const NON_WORD_RE = /[^\w\d]+/g + export const isFontFile = (id: string) => FONT_RE.test(id) -function generateSlugs (path: string) { +function generateSlugs (path: string) { const name = filename(path) return [...new Set([ name.toLowerCase(), @@ -98,9 +102,9 @@ function generateSlugs (path: string) { // Barlow.das324jasdf => barlow name.replace(/\.[\w\d]+$/, '').toLowerCase(), // Open+Sans => open-sans - name.replace(/[+ ]+/g, '-').toLowerCase(), + name.replace(NON_WORD_RE, '-').toLowerCase(), // Open+Sans => opensans - name.replace(/[+ ]+/g, '').toLowerCase(), + name.replace(NON_WORD_RE, '').toLowerCase(), ])] } @@ -122,7 +126,7 @@ function unregisterFont (path: string) { function lookupFont (family: string): string[] | undefined { const priority = ['woff2', 'woff', 'ttf', 'otf', 'eot'] - const slug = family.replace(/[+ ]+/g, '-').toLowerCase() + const slug = fontFamilyToSlug(family) const scannedFiles = providerContext.registry[slug]?.map(path => { const base = providerContext.rootPaths.find(root => path.startsWith(root)) return base ? withLeadingSlash(relative(base, path)) : path @@ -135,3 +139,7 @@ function lookupFont (family: string): string[] | undefined { return priority.indexOf(extA) - priority.indexOf(extB) }) } + +function fontFamilyToSlug (family: string) { + return family.toLowerCase().replace(NON_WORD_RE, '') +} From b358dc2b92cb4251250c10b41d78b9d58378c8ee Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 10:41:56 +0000 Subject: [PATCH 04/37] refactor(google): split out api client --- src/providers/google.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 0c2febb..0bfbbd4 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -21,7 +21,6 @@ interface FontDetail extends Omit { fontFamily: string fontStyle: string fontWeight: string - // eot: string woff: string ttf: string @@ -33,12 +32,15 @@ interface FontDetail extends Omit { storeID: string } +const fontAPI = $fetch.create({ + baseURL: 'https://gwfh.mranftl.com/api/fonts' +}) -let fonts: Array +let fonts: FontIndexMeta[] export default { async setup () { // TODO: Fetch and cache possible Google fonts - fonts = await $fetch('https://gwfh.mranftl.com/api/fonts') + fonts = await fontAPI('/') }, async resolveFontFaces (fontFamily, defaults) { const font = fonts.find(font => font.family === fontFamily) @@ -46,7 +48,9 @@ export default { const subsets = defaults.subsets.filter(subset => font.subsets.includes(subset)) - const details = await $fetch(`https://gwfh.mranftl.com/api/fonts/${font.id}?subsets=${subsets.join(',')}`) + const details = await fontAPI(font.id, { + query: subsets.length ? { subsets: subsets.join(',') } : {} + }) return { fonts: details.variants.map(variant => ({ @@ -66,3 +70,4 @@ export default { } }, } satisfies FontProvider + From 98feb4947e2af036682466c8e801ea98ff301fbe Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 13:09:04 +0000 Subject: [PATCH 05/37] feat: support passing options to font providers --- src/providers/google.ts | 1 + src/providers/local.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 0bfbbd4..8f10237 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -37,6 +37,7 @@ const fontAPI = $fetch.create({ }) let fonts: FontIndexMeta[] + export default { async setup () { // TODO: Fetch and cache possible Google fonts diff --git a/src/providers/local.ts b/src/providers/local.ts index 173ccb0..980c823 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -11,7 +11,7 @@ const providerContext = { } export default { - async setup (nuxt) { + async setup (_options, nuxt) { // Scan for all font files in public directories for (const layer of nuxt.options._layers) { const publicDir = join(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public') @@ -93,7 +93,7 @@ const NON_WORD_RE = /[^\w\d]+/g export const isFontFile = (id: string) => FONT_RE.test(id) -function generateSlugs (path: string) { +function generateSlugs (path: string) { const name = filename(path) return [...new Set([ name.toLowerCase(), From b25f0aa3e8b14fecf0816db5783fdd8b21b2728a Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 13:53:28 +0000 Subject: [PATCH 06/37] refactor(google): slight refactor --- src/providers/google.ts | 75 ++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 8f10237..53397bc 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -2,6 +2,36 @@ import { $fetch } from 'ofetch' import type { FontProvider } from '../types' +export default { + async setup () { + await initialiseFontMeta() + }, + async resolveFontFaces (fontFamily, defaults) { + if (!isGoogleFont(fontFamily)) { return } + + const details = await getFontDetails(fontFamily, defaults.subsets) + + return { + fonts: details.variants.map(variant => ({ + style: variant.fontStyle, + weight: variant.fontWeight, + // TODO: handle subset unicode ranges + // TODO: download/proxy URLs locally + src: [ + ...variant.local?.map(name => ({ name })) || [fontFamily], + ...variant.woff2 ? [{ url: variant.woff2, format: 'woff2' }] : [], + ...variant.woff ? [{ url: variant.woff, format: 'woff' }] : [], + ...variant.ttf ? [{ url: variant.ttf, format: 'truetype' }] : [], + ...variant.eot ? [{ url: variant.eot, format: 'embedded-opentype' }] : [], + ...variant.svg ? [{ url: variant.svg, format: 'svg' }] : [], + ] + })) + } + }, +} satisfies FontProvider + +// https://github.com/majodev/google-webfonts-helper + interface FontIndexMeta { category: string defSubset: string @@ -38,37 +68,20 @@ const fontAPI = $fetch.create({ let fonts: FontIndexMeta[] -export default { - async setup () { - // TODO: Fetch and cache possible Google fonts - fonts = await fontAPI('/') - }, - async resolveFontFaces (fontFamily, defaults) { - const font = fonts.find(font => font.family === fontFamily) - if (!font) { return } - - const subsets = defaults.subsets.filter(subset => font.subsets.includes(subset)) +// TODO: Fetch and cache possible Google fonts +async function initialiseFontMeta () { + fonts = await fontAPI('/') +} - const details = await fontAPI(font.id, { - query: subsets.length ? { subsets: subsets.join(',') } : {} - }) +function isGoogleFont (family: string) { + return fonts.some(font => font.family === family) +} - return { - fonts: details.variants.map(variant => ({ - style: variant.fontStyle, - weight: variant.fontWeight, - // TODO: handle subset unicode ranges - // TODO: download/proxy URLs locally - src: [ - ...variant.local?.map(name => ({ name })) || [fontFamily], - ...variant.woff2 ? [{ url: variant.woff2, format: 'woff2' }] : [], - ...variant.woff ? [{ url: variant.woff, format: 'woff' }] : [], - ...variant.ttf ? [{ url: variant.ttf, format: 'truetype' }] : [], - ...variant.eot ? [{ url: variant.eot, format: 'embedded-opentype' }] : [], - ...variant.svg ? [{ url: variant.svg, format: 'svg' }] : [], - ] - })) - } - }, -} satisfies FontProvider +async function getFontDetails (family: string, defaultSubsets: string[]) { + const font = fonts.find(font => font.family === family)! + const subsets = defaultSubsets.filter(subset => font.subsets.includes(subset)) + return await fontAPI(font.id, { + query: subsets.length ? { subsets: subsets.join(',') } : {} + }) +} From dbd66a321f7cd345f807c0ad40e945cb5dd36113 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 16:10:29 +0000 Subject: [PATCH 07/37] feat: download (prod) or proxy (dev) provider font urls --- src/providers/google.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 53397bc..b7b48e7 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -16,7 +16,6 @@ export default { style: variant.fontStyle, weight: variant.fontWeight, // TODO: handle subset unicode ranges - // TODO: download/proxy URLs locally src: [ ...variant.local?.map(name => ({ name })) || [fontFamily], ...variant.woff2 ? [{ url: variant.woff2, format: 'woff2' }] : [], From 0eca0a370ee2b64a27a097c6a4d9f509cb5da6ea Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 20 Feb 2024 23:58:58 +0000 Subject: [PATCH 08/37] feat: add `bunny` provider --- src/providers/bunny.ts | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/providers/bunny.ts diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts new file mode 100644 index 0000000..55e21e9 --- /dev/null +++ b/src/providers/bunny.ts @@ -0,0 +1,68 @@ +import { $fetch } from 'ofetch' +import type { FontProvider, ResolveFontFacesOptions } from '../types' +import { extractFontFaceData } from '../css/parse' + +export default { + async setup () { + await initialiseFontMeta() + }, + async resolveFontFaces (fontFamily, defaults) { + if (!isBunnyFont(fontFamily)) { return } + + return { + fonts: await getFontDetails(fontFamily, defaults) + } + }, +} satisfies FontProvider + +const fontAPI = $fetch.create({ + baseURL: 'https://fonts.bunny.net' +}) + +interface BunnyFontMeta { + [key: string]: { + category: string + defSubset: string + familyName: string + isVariable: boolean + styles: string[] + variants: Record + weights: number[] + } +} + +let fonts: BunnyFontMeta +const familyMap = new Map() + +// TODO: Fetch and cache +async function initialiseFontMeta () { + fonts = await fontAPI('/list', { responseType: 'json' }) + for (const id in fonts) { + familyMap.set(fonts[id]!.familyName!, id) + } +} + +function isBunnyFont (family: string) { + return familyMap.has(family) +} + +async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { + const id = familyMap.get(family) as keyof typeof fonts + const font = fonts[id]! + const weights = variants.weights?.filter(weight => font.weights.includes(Number(weight))) || font.weights + const styleMap = { + italic: 'i', + oblique: 'i', + normal: '' + } + const styles = new Set(variants.styles.map(i => styleMap[i])) + const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${w}${s}`)) + + const css = await fontAPI('/css', { + query: { + family: id + ':' + resolvedVariants.join(',') + } + }) + + return extractFontFaceData(css) +} From 43c250f8e532dc93058bdb4b6901255fb2d3028b Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 10:39:49 +0000 Subject: [PATCH 09/37] fix: prepend local font sources --- src/providers/bunny.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index 55e21e9..aaf92d5 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -1,6 +1,6 @@ import { $fetch } from 'ofetch' import type { FontProvider, ResolveFontFacesOptions } from '../types' -import { extractFontFaceData } from '../css/parse' +import { extractFontFaceData, addLocalFallbacks } from '../css/parse' export default { async setup () { @@ -49,7 +49,7 @@ function isBunnyFont (family: string) { async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { const id = familyMap.get(family) as keyof typeof fonts const font = fonts[id]! - const weights = variants.weights?.filter(weight => font.weights.includes(Number(weight))) || font.weights + const weights = variants.weights.filter(weight => font.weights.includes(Number(weight))) const styleMap = { italic: 'i', oblique: 'i', @@ -64,5 +64,6 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions } }) - return extractFontFaceData(css) + // TODO: support subsets + return addLocalFallbacks(family, extractFontFaceData(css)) } From 3efea712c7302b7de7562ab71fdf11a213a2059a Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 10:40:46 +0000 Subject: [PATCH 10/37] refactor(google): use native google apis --- src/providers/google.ts | 94 +++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index b7b48e7..5c1d0aa 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -1,6 +1,7 @@ import { $fetch } from 'ofetch' -import type { FontProvider } from '../types' +import type { FontProvider, ResolveFontFacesOptions } from '../types' +import { extractFontFaceData, addLocalFallbacks } from '../css/parse' export default { async setup () { @@ -9,22 +10,8 @@ export default { async resolveFontFaces (fontFamily, defaults) { if (!isGoogleFont(fontFamily)) { return } - const details = await getFontDetails(fontFamily, defaults.subsets) - return { - fonts: details.variants.map(variant => ({ - style: variant.fontStyle, - weight: variant.fontWeight, - // TODO: handle subset unicode ranges - src: [ - ...variant.local?.map(name => ({ name })) || [fontFamily], - ...variant.woff2 ? [{ url: variant.woff2, format: 'woff2' }] : [], - ...variant.woff ? [{ url: variant.woff, format: 'woff' }] : [], - ...variant.ttf ? [{ url: variant.ttf, format: 'truetype' }] : [], - ...variant.eot ? [{ url: variant.eot, format: 'embedded-opentype' }] : [], - ...variant.svg ? [{ url: variant.svg, format: 'svg' }] : [], - ] - })) + fonts: await getFontDetails(fontFamily, defaults) } }, } satisfies FontProvider @@ -32,55 +19,62 @@ export default { // https://github.com/majodev/google-webfonts-helper interface FontIndexMeta { - category: string - defSubset: string - defVariant: string family: string - id: string - lastModified: string - popularity: number subsets: string[] - variants: string[] - version: string -} - -interface FontDetail extends Omit { - variants: Array<{ - id: string - fontFamily: string - fontStyle: string - fontWeight: string - eot: string - woff: string - ttf: string - svg: string - woff2: string - local: string[] + fonts: Record - subsetMap: Record - storeID: string } -const fontAPI = $fetch.create({ - baseURL: 'https://gwfh.mranftl.com/api/fonts' -}) - let fonts: FontIndexMeta[] // TODO: Fetch and cache possible Google fonts async function initialiseFontMeta () { - fonts = await fontAPI('/') + const { familyMetadataList } = await $fetch<{ familyMetadataList: FontIndexMeta[] }>('/metadata/fonts', { + baseURL: 'https://fonts.google.com' + }) + fonts = familyMetadataList } function isGoogleFont (family: string) { return fonts.some(font => font.family === family) } -async function getFontDetails (family: string, defaultSubsets: string[]) { +async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { const font = fonts.find(font => font.family === family)! - const subsets = defaultSubsets.filter(subset => font.subsets.includes(subset)) + const weights = variants.weights.filter(weight => String(weight) in font.fonts) + const styleMap = { + italic: '1', + oblique: '1', + normal: '0' + } - return await fontAPI(font.id, { - query: subsets.length ? { subsets: subsets.join(',') } : {} - }) + const styles = new Set(variants.styles.map(i => styleMap[i])) + const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)) + + let css = '' + + for (const extension in userAgents) { + css += await $fetch('/css2', { + baseURL: 'https://fonts.googleapis.com', + headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] }, + query: { + family: family.replace(/ /g, '+') + ':' + 'ital,wght@' + resolvedVariants.join(';') + } + }) + } + + // TODO: support subsets + return addLocalFallbacks(family, extractFontFaceData(css)) +} + +const userAgents = { + woff2: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + ttf: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16' + // eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)', + // woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0', + // svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3', } From 6c0e864fd76343fc63016d4f0579078e28eb4c23 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 11:14:03 +0000 Subject: [PATCH 11/37] chore: remove old attribution --- src/providers/google.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 5c1d0aa..621451a 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -16,8 +16,6 @@ export default { }, } satisfies FontProvider -// https://github.com/majodev/google-webfonts-helper - interface FontIndexMeta { family: string subsets: string[] From 6826605e68e34f0493cb3b095ebad07623bfc777 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 11:33:17 +0000 Subject: [PATCH 12/37] perf: cache google/bunny font metadata --- src/providers/bunny.ts | 10 +++++++--- src/providers/google.ts | 17 +++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index aaf92d5..5502604 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -1,6 +1,9 @@ import { $fetch } from 'ofetch' +import { hash } from 'ohash' + import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { cachedData } from '../cache' export default { async setup () { @@ -10,11 +13,13 @@ export default { if (!isBunnyFont(fontFamily)) { return } return { - fonts: await getFontDetails(fontFamily, defaults) + fonts: await cachedData(`bunny:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults)) } }, } satisfies FontProvider +/** internal */ + const fontAPI = $fetch.create({ baseURL: 'https://fonts.bunny.net' }) @@ -34,9 +39,8 @@ interface BunnyFontMeta { let fonts: BunnyFontMeta const familyMap = new Map() -// TODO: Fetch and cache async function initialiseFontMeta () { - fonts = await fontAPI('/list', { responseType: 'json' }) + fonts = await cachedData('bunny:meta.json', () => fontAPI('/list', { responseType: 'json' })) for (const id in fonts) { familyMap.set(fonts[id]!.familyName!, id) } diff --git a/src/providers/google.ts b/src/providers/google.ts index 621451a..62577cf 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -1,7 +1,9 @@ import { $fetch } from 'ofetch' +import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { cachedData } from '../cache' export default { async setup () { @@ -11,7 +13,7 @@ export default { if (!isGoogleFont(fontFamily)) { return } return { - fonts: await getFontDetails(fontFamily, defaults) + fonts: await cachedData(`google:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults)) } }, } satisfies FontProvider @@ -27,14 +29,17 @@ interface FontIndexMeta { }> } +/** internal */ + let fonts: FontIndexMeta[] -// TODO: Fetch and cache possible Google fonts +async function fetchFontMetadata () { + return await $fetch<{ familyMetadataList: FontIndexMeta[] }>('https://fonts.google.com/metadata/fonts') + .then(r => r.familyMetadataList) +} + async function initialiseFontMeta () { - const { familyMetadataList } = await $fetch<{ familyMetadataList: FontIndexMeta[] }>('/metadata/fonts', { - baseURL: 'https://fonts.google.com' - }) - fonts = familyMetadataList + fonts = await cachedData('google:meta.json', fetchFontMetadata) } function isGoogleFont (family: string) { From 40f3fcc8f814f9c600e1be90f2d540b5559ccdb6 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 11:48:38 +0000 Subject: [PATCH 13/37] fix: handle network errors fetching font metadata --- src/providers/bunny.ts | 15 +++++++++++++-- src/providers/google.ts | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index 5502604..39b941e 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -4,6 +4,7 @@ import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { logger } from '../logger' export default { async setup () { @@ -13,7 +14,12 @@ export default { if (!isBunnyFont(fontFamily)) { return } return { - fonts: await cachedData(`bunny:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults)) + fonts: await cachedData(`bunny:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { + onError (err) { + logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`bunny\`.`, err) + return [] + } + }) } }, } satisfies FontProvider @@ -40,7 +46,12 @@ let fonts: BunnyFontMeta const familyMap = new Map() async function initialiseFontMeta () { - fonts = await cachedData('bunny:meta.json', () => fontAPI('/list', { responseType: 'json' })) + fonts = await cachedData('bunny:meta.json', () => fontAPI('/list', { responseType: 'json' }), { + onError () { + logger.error('Could not download `bunny` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for bunny.') + return {} + } + }) for (const id in fonts) { familyMap.set(fonts[id]!.familyName!, id) } diff --git a/src/providers/google.ts b/src/providers/google.ts index 62577cf..1a3e687 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -4,6 +4,7 @@ import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { logger } from '../logger' export default { async setup () { @@ -13,7 +14,12 @@ export default { if (!isGoogleFont(fontFamily)) { return } return { - fonts: await cachedData(`google:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults)) + fonts: await cachedData(`google:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { + onError (err) { + logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`google\`.`, err) + return [] + } + }) } }, } satisfies FontProvider @@ -39,7 +45,12 @@ async function fetchFontMetadata () { } async function initialiseFontMeta () { - fonts = await cachedData('google:meta.json', fetchFontMetadata) + fonts = await cachedData('google:meta.json', fetchFontMetadata, { + onError () { + logger.error('Could not download `google` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for google.') + return [] + } + }) } function isGoogleFont (family: string) { From f81ab59624393cc268d7d14549fe9b68eee71faf Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 16:12:37 +0000 Subject: [PATCH 14/37] feat: add automatic font metric optimisation --- src/providers/google.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 1a3e687..10663f0 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -76,7 +76,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.replace(/ /g, '+') + ':' + 'ital,wght@' + resolvedVariants.join(';') + family: family + ':' + 'ital,wght@' + resolvedVariants.join(';') } }) } From f604f482868a234c12c0b876268787ffba933568 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 20:18:24 +0000 Subject: [PATCH 15/37] feat: add `fontshare` provider (#4) --- src/providers/fontshare.ts | 110 +++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/providers/fontshare.ts diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts new file mode 100644 index 0000000..7ea416c --- /dev/null +++ b/src/providers/fontshare.ts @@ -0,0 +1,110 @@ +import { $fetch } from 'ofetch' +import { hash } from 'ohash' + +import type { FontProvider, ResolveFontFacesOptions } from '../types' +import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { cachedData } from '../cache' +import { logger } from '../logger' + +export default { + async setup () { + await initialiseFontMeta() + }, + async resolveFontFaces (fontFamily, defaults) { + if (!isFontshareFont(fontFamily)) { return } + + return { + fonts: await cachedData(`fontshare:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { + onError (err) { + logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`fontshare\`.`, err) + return [] + } + }) + } + }, +} satisfies FontProvider + +/** internal */ + +const fontAPI = $fetch.create({ + baseURL: 'https://api.fontshare.com/v2' +}) + +interface FontshareFontMeta { + slug: string + name: string + styles: Array<{ + default: boolean + file: string + id: string + is_italic: boolean + is_variable: boolean + properties: { + ascending_leading: number + body_height: null + cap_height: number + descending_leading: number + max_char_width: number + x_height: number + y_max: number + y_min: number + } + weight: { + label: string + name: string + native_name: null + number: number + weight: number + } + }> +} + +let fonts: FontshareFontMeta[] +const families = new Set() + +async function initialiseFontMeta () { + fonts = await cachedData('fontshare:meta.json', async () => { + const fonts: FontshareFontMeta[] = [] + let offset = 0 + let chunk + do { + chunk = await fontAPI<{ fonts: FontshareFontMeta[], has_more: boolean }>('/fonts', { + query: { + offset, + limit: 100 + } + }) + fonts.push(...chunk.fonts) + offset++ + } while (chunk.has_more) + return fonts + }, { + onError () { + logger.error('Could not download `fontshare` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontshare.') + return {} + } + }) + for (const font of fonts) { + families.add(font.name) + } +} + +function isFontshareFont (family: string) { + return families.has(family) +} + +async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { + // https://api.fontshare.com/v2/css?f[]=alpino@300 + const font = fonts.find(f => f.name === family)! + const numbers: number[] = [] + for (const style of font.styles) { + if (style.is_italic && !variants.styles.includes('italic')) { continue } + if (!variants.weights.includes(style.weight.number)) { continue } + numbers.push(style.weight.number) + } + + const css = await fontAPI(`/css?f[]=${font.slug + '@' + numbers.join(',')}`) + + // TODO: support subsets and axes + return addLocalFallbacks(family, extractFontFaceData(css)) +} From 7c90e5529665fb87f468f452e5179bfd2bba24bd Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Feb 2024 20:24:14 +0000 Subject: [PATCH 16/37] fix(fontshare): return empty array when there's an error --- src/providers/fontshare.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts index 7ea416c..395f986 100644 --- a/src/providers/fontshare.ts +++ b/src/providers/fontshare.ts @@ -81,7 +81,7 @@ async function initialiseFontMeta () { }, { onError () { logger.error('Could not download `fontshare` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontshare.') - return {} + return [] } }) for (const font of fonts) { From 65a80b164dba71eaf5bd4b7f22eafc0a06e7bc1e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 22 Feb 2024 00:48:57 +0000 Subject: [PATCH 17/37] fix(google): handle variable font weights --- src/providers/google.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 10663f0..6162e36 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -33,6 +33,12 @@ interface FontIndexMeta { width: number | null lineHeight: number | null }> + axes: Array<{ + tag: string + min: number + max: number + defaultValue: number + }> } /** internal */ @@ -57,16 +63,19 @@ function isGoogleFont (family: string) { return fonts.some(font => font.family === family) } +const styleMap = { + italic: '1', + oblique: '1', + normal: '0' +} async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { const font = fonts.find(font => font.family === family)! - const weights = variants.weights.filter(weight => String(weight) in font.fonts) - const styleMap = { - italic: '1', - oblique: '1', - normal: '0' - } + const styles = [...new Set(variants.styles.map(i => styleMap[i]))].sort() - const styles = new Set(variants.styles.map(i => styleMap[i])) + const variableWeight = font.axes.find(a => a.tag === 'wght') + const weights = variableWeight + ? [`${variableWeight.min}..${variableWeight.max}`] + : variants.weights.filter(weight => String(weight) in font.fonts) const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)) let css = '' From a2328bc58914aa4b0c77b30489995e4fc2f512c2 Mon Sep 17 00:00:00 2001 From: qwerzl <46770502+qwerzl@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:51:34 +0800 Subject: [PATCH 18/37] fix(google): sort resolved variants when fetching font css (#33) --- src/providers/google.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/google.ts b/src/providers/google.ts index 6162e36..c1c8268 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -76,7 +76,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const weights = variableWeight ? [`${variableWeight.min}..${variableWeight.max}`] : variants.weights.filter(weight => String(weight) in font.fonts) - const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)) + const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)).sort() let css = '' From f821d0a781e87a1c817745450a057c0747010bba Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Mar 2024 12:05:32 +0000 Subject: [PATCH 19/37] fix(local): deduplicate found fonts --- src/providers/local.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index 980c823..5800b6d 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -63,18 +63,13 @@ export default { ...(isDefaultStyle && isDefaultWeight) ? [[subset]] : [], ...(isDefaultStyle && isDefaultWeight && isDefaultSubset) ? [[]] : [] ] - for (const family of [fontFamily, fontFamily.replace(NON_WORD_RE, '-'), fontFamily.replace(NON_WORD_RE, '')]) { - for (const option of options) { - const resolved = lookupFont([family, ...option].join('-')) || lookupFont([family, ...option].join('')) - if (resolved) { - fonts.push({ - src: resolved, - weight, - style, - }) - break - } - } + const resolved = findFirst([fontFamily, fontFamily.replace(NON_WORD_RE, '-'), fontFamily.replace(NON_WORD_RE, '')], options) + if (resolved) { + fonts.push({ + src: [...new Set(resolved)], + weight, + style, + }) } } } @@ -93,6 +88,17 @@ const NON_WORD_RE = /[^\w\d]+/g export const isFontFile = (id: string) => FONT_RE.test(id) +function findFirst (families: string[], options: Array[]) { + for (const family of families) { + for (const option of options) { + const resolved = lookupFont([family, ...option].join('-')) || lookupFont([family, ...option].join('')) + if (resolved) { + return resolved + } + } + } +} + function generateSlugs (path: string) { const name = filename(path) return [...new Set([ From 18b7652f14e852ccb41f9a70a52b8e2f252c97e3 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Mar 2024 13:43:59 +0000 Subject: [PATCH 20/37] fix(local): refactor scanning/lookup mechanism (#41) resolves https://github.com/nuxt/fonts/issues/22 --- src/providers/local.ts | 116 +++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index 5800b6d..eb796f0 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -1,6 +1,7 @@ import { globby } from 'globby' import { join, relative, resolve } from 'pathe' import { filename } from 'pathe/utils' +import { anyOf, createRegExp, not, wordBoundary } from 'magic-regexp' import type { FontFaceData, FontProvider } from '../types' import { withLeadingSlash, withTrailingSlash } from 'ufo' @@ -42,31 +43,14 @@ export default { resolveFontFaces (fontFamily, defaults) { const fonts: FontFaceData[] = [] - // Generate all possible permutations of font family names - // and resolve the first one that exists + // Resolve font files for each combination of weight, style and subset for (const weight of defaults.weights) { - const isDefaultWeight = weight === 'normal' || weight === 400 for (const style of defaults.styles) { - const isDefaultStyle = style === 'normal' for (const subset of defaults.subsets) { - const isDefaultSubset = subset === 'latin' - const options = [ - [weight, style, subset], - [weight, subset, style], - [style, weight, subset], - [style, subset, weight], - [subset, weight, style], - [subset, style, weight], - ...isDefaultWeight ? [[style, subset], [subset, style]] : [], - ...isDefaultStyle ? [[weight, subset], [subset, weight]] : [], - ...isDefaultSubset ? [[weight, style], [style, weight]] : [], - ...(isDefaultStyle && isDefaultWeight) ? [[subset]] : [], - ...(isDefaultStyle && isDefaultWeight && isDefaultSubset) ? [[]] : [] - ] - const resolved = findFirst([fontFamily, fontFamily.replace(NON_WORD_RE, '-'), fontFamily.replace(NON_WORD_RE, '')], options) - if (resolved) { + const resolved = lookupFont(fontFamily, [weightMap[weight] || weight, style, subset]) + if (resolved.length > 0) { fonts.push({ - src: [...new Set(resolved)], + src: resolved, weight, style, }) @@ -88,30 +72,58 @@ const NON_WORD_RE = /[^\w\d]+/g export const isFontFile = (id: string) => FONT_RE.test(id) -function findFirst (families: string[], options: Array[]) { - for (const family of families) { - for (const option of options) { - const resolved = lookupFont([family, ...option].join('-')) || lookupFont([family, ...option].join('')) - if (resolved) { - return resolved - } - } - } +const weightMap: Record = { + '100': 'thin', + '200': 'extra-light', + '300': 'light', + '400': 'normal', + '500': 'medium', + '600': 'semi-bold', + '700': 'bold', + '800': 'extra-bold', + '900': 'black', } +const weights = Object.entries(weightMap).flatMap(e => e).filter(r => r !== 'normal') +const WEIGHT_RE = createRegExp(anyOf(...weights).groupedAs('weight').after(not.digit).before(not.digit.or(wordBoundary)), ['i']) + +const styles = ['italic', 'oblique'] as const +const STYLE_RE = createRegExp(anyOf(...styles).groupedAs('style').before(not.wordChar.or(wordBoundary)), ['i']) + +const subsets = [ + 'cyrillic-ext', + 'cyrillic', + 'greek-ext', + 'greek', + 'vietnamese', + 'latin-ext', + 'latin', +] as const +const SUBSET_RE = createRegExp(anyOf(...subsets).groupedAs('subset').before(not.wordChar.or(wordBoundary)), ['i']) + function generateSlugs (path: string) { - const name = filename(path) - return [...new Set([ - name.toLowerCase(), - // Barlow-das324jasdf => barlow - name.replace(/-[\w\d]+$/, '').toLowerCase(), - // Barlow.das324jasdf => barlow - name.replace(/\.[\w\d]+$/, '').toLowerCase(), - // Open+Sans => open-sans - name.replace(NON_WORD_RE, '-').toLowerCase(), - // Open+Sans => opensans - name.replace(NON_WORD_RE, '').toLowerCase(), - ])] + let name = filename(path) + + const weight = name.match(WEIGHT_RE)?.groups?.weight || 'normal' + const style = name.match(STYLE_RE)?.groups?.style || 'normal' + const subset = name.match(SUBSET_RE)?.groups?.subset || 'latin' + + for (const slug of [weight, style, subset]) { + name = name.replace(slug, '') + } + + const slugs = new Set() + + for (const slug of [name.replace(/[.][\w\d]*$/, ''), name.replace(/[._-][\w\d]*$/, '')]) { + slugs.add([ + fontFamilyToSlug(slug.replace(/[\W._-]+$/, '')), + weightMap[weight] || weight, + style, + subset + ].join('-').toLowerCase()) + } + + return [...slugs] } function registerFont (path: string) { @@ -130,19 +142,23 @@ function unregisterFont (path: string) { } } -function lookupFont (family: string): string[] | undefined { - const priority = ['woff2', 'woff', 'ttf', 'otf', 'eot'] - const slug = fontFamilyToSlug(family) - const scannedFiles = providerContext.registry[slug]?.map(path => { +const extensionPriority = ['woff2', 'woff', 'ttf', 'otf', 'eot'] +function lookupFont (family: string, suffixes: Array): string[] { + const slug = [fontFamilyToSlug(family), ...suffixes].join('-') + const paths = providerContext.registry[slug] + if (!paths || paths.length === 0) { return [] } + + const fonts = new Set() + for (const path of paths) { const base = providerContext.rootPaths.find(root => path.startsWith(root)) - return base ? withLeadingSlash(relative(base, path)) : path - }) + fonts.add(base ? withLeadingSlash(relative(base, path)) : path) + } - return scannedFiles?.sort((a, b) => { + return [...fonts].sort((a, b) => { const extA = filename(a).split('.').pop()! const extB = filename(b).split('.').pop()! - return priority.indexOf(extA) - priority.indexOf(extB) + return extensionPriority.indexOf(extA) - extensionPriority.indexOf(extB) }) } From 9250a8c67ec7cfbd18ecc7046aef6c75679fa14f Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Sun, 10 Mar 2024 03:23:32 +0800 Subject: [PATCH 21/37] fix: warn when fonts can't be resolved because of an override (#35) --- src/providers/bunny.ts | 2 ++ src/providers/fontshare.ts | 2 ++ src/providers/google.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index 39b941e..bdaf9f7 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -71,6 +71,8 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions normal: '' } const styles = new Set(variants.styles.map(i => styleMap[i])) + if (weights.length === 0 || styles.size === 0) return [] + const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${w}${s}`)) const css = await fontAPI('/css', { diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts index 395f986..ccc3681 100644 --- a/src/providers/fontshare.ts +++ b/src/providers/fontshare.ts @@ -103,6 +103,8 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions numbers.push(style.weight.number) } + if (numbers.length === 0) return [] + const css = await fontAPI(`/css?f[]=${font.slug + '@' + numbers.join(',')}`) // TODO: support subsets and axes diff --git a/src/providers/google.ts b/src/providers/google.ts index c1c8268..d0abc73 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -76,6 +76,9 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const weights = variableWeight ? [`${variableWeight.min}..${variableWeight.max}`] : variants.weights.filter(weight => String(weight) in font.fonts) + + if (weights.length === 0 || styles.length === 0) return [] + const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${s},${w}`)).sort() let css = '' From 81c92bed23946a637e91889de71669f14c921947 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Mon, 11 Mar 2024 01:18:51 +0800 Subject: [PATCH 22/37] fix: normalize weights before passing to `resolveFontFaces` (#47) --- src/providers/fontshare.ts | 2 +- src/providers/google.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts index ccc3681..aea04ee 100644 --- a/src/providers/fontshare.ts +++ b/src/providers/fontshare.ts @@ -99,7 +99,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const numbers: number[] = [] for (const style of font.styles) { if (style.is_italic && !variants.styles.includes('italic')) { continue } - if (!variants.weights.includes(style.weight.number)) { continue } + if (!variants.weights.includes(String(style.weight.number))) { continue } numbers.push(style.weight.number) } diff --git a/src/providers/google.ts b/src/providers/google.ts index d0abc73..d7f1d7b 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -75,7 +75,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const variableWeight = font.axes.find(a => a.tag === 'wght') const weights = variableWeight ? [`${variableWeight.min}..${variableWeight.max}`] - : variants.weights.filter(weight => String(weight) in font.fonts) + : variants.weights.filter(weight => weight in font.fonts) if (weights.length === 0 || styles.length === 0) return [] From 9c8a83a7022342fd389adc9cee89c77d3df3e7d3 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Wed, 13 Mar 2024 07:29:18 +0800 Subject: [PATCH 23/37] feat: add adobe fonts provider (#55) --- src/providers/adobe.ts | 118 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/providers/adobe.ts diff --git a/src/providers/adobe.ts b/src/providers/adobe.ts new file mode 100644 index 0000000..299b182 --- /dev/null +++ b/src/providers/adobe.ts @@ -0,0 +1,118 @@ +import { $fetch } from 'ofetch' +import { hash } from 'ohash' + +import type { FontProvider, ResolveFontFacesOptions } from '../types' +import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { cachedData } from '../cache' +import { logger } from '../logger' + +interface ProviderOption { + id?: string[] | string +} + +export default { + async setup (options: ProviderOption) { + if (!options.id) { return } + await initialiseFontMeta(typeof options.id === 'string' ? [options.id] : options.id) + }, + async resolveFontFaces (fontFamily, defaults) { + if (!isAdobeFont(fontFamily)) { return } + + return { + fonts: await cachedData(`adobe:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { + onError (err) { + logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`adobe\`.`, err) + return [] + } + }) + } + }, +} satisfies FontProvider + +const fontAPI = $fetch.create({ + baseURL: 'https://typekit.com' +}) + +const fontCSSAPI = $fetch.create({ + baseURL: 'https://use.typekit.net' +}) + +interface AdobeFontMeta { + kits: AdobeFontKit[] +} + +interface AdobeFontAPI { + kit: AdobeFontKit +} + +interface AdobeFontKit { + id: string + families: AdobeFontFamily[] +} + +interface AdobeFontFamily { + id: string + name: string + slug: string + css_names: string[] + css_stack: string + variations: string[] +} + +let fonts: AdobeFontMeta +const familyMap = new Map() + +async function getAdobeFontMeta (id: string): Promise { + const { kit } = await fontAPI(`/api/v1/json/kits/${id}/published`, { responseType: 'json' }) + return kit +} + +async function initialiseFontMeta (kits: string[]) { + fonts = { + kits: await Promise.all(kits.map(id => cachedData(`adobe:meta-${id}.json`, () => getAdobeFontMeta(id), { + onError () { + logger.error('Could not download `adobe` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for adobe.') + return null + } + }))).then(r => r.filter((meta): meta is AdobeFontKit => !!meta)) + } + for (const kit in fonts.kits) { + const families = fonts.kits[kit]!.families + for (const family in families) { + familyMap.set(families[family]!.name, families[family]!.id) + } + } +} + +function isAdobeFont (family: string) { + return familyMap.has(family) +} + +async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { + variants.weights = variants.weights.map(String) + + for (const kit in fonts.kits) { + const font = fonts.kits[kit]!.families.find(f => f.name === family)! + if (!font) { continue } + + const styles: string[] = [] + for (const style of font.variations) { + if (style.includes('i') && !variants.styles.includes('italic')) { + continue + } + if (!variants.weights.includes(String(style.slice(-1) + '00'))) { + continue + } + styles.push(style) + } + if (styles.length === 0) { continue } + const css = await fontCSSAPI(`${fonts.kits[kit]!.id}.css`) + + // Adobe uses slugs instead of names in its CSS to define its font faces, + // so we need to first transform names into slugs. + const slug = family.toLowerCase().split(' ').join('-') + return addLocalFallbacks(family, extractFontFaceData(css, slug)) + } + + return [] +} From 9335a18122f20596963238aa72aeb58a76cc2454 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Wed, 20 Mar 2024 21:12:11 +0800 Subject: [PATCH 24/37] feat: fontsource provider (#78) --- src/providers/fontsource.ts | 127 ++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/providers/fontsource.ts diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts new file mode 100644 index 0000000..80ba6dc --- /dev/null +++ b/src/providers/fontsource.ts @@ -0,0 +1,127 @@ +import { $fetch } from 'ofetch' +import { hash } from 'ohash' + +import type { FontProvider, NormalizedFontFaceData, ResolveFontFacesOptions } from '../types' +import { addLocalFallbacks } from '../css/parse' +import { cachedData } from '../cache' +import { logger } from '../logger' + +export default { + async setup () { + await initialiseFontMeta() + }, + async resolveFontFaces (fontFamily, defaults) { + if (!isFontsourceFont(fontFamily)) { return } + + return { + fonts: await cachedData(`fontsource:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { + onError (err) { + logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`fontsource\`.`, err) + return [] + } + }) + } + }, +} satisfies FontProvider + +/** internal */ + +const fontAPI = $fetch.create({ + baseURL: 'https://api.fontsource.org/v1' +}) + +export interface FontsourceFontMeta { + [key: string]: { + id: string + family: string + subsets: string[] + weights: number[] + styles: string[] + defSubset: string + variable: boolean + lastModified: string + category: string + version: string + type: string + } +} + +interface FontsourceFontFile { + url: { + woff2?: string + woff?: string + ttf?: string + } +} + +interface FontsourceFontVariant { + [key: string]: { + [key: string]: { + [key: string]: FontsourceFontFile + } + } +} + +interface FontsourceFontDetail { + id: string + family: string + subsets: string[] + weights: number[] + styles: string[] + unicodeRange: Record + defSubset: string + variable: boolean + lastModified: string + category: string + version: string + type: string + variants: FontsourceFontVariant +} + +let fonts: FontsourceFontMeta +const familyMap = new Map() + +async function initialiseFontMeta () { + fonts = await cachedData('fontsource:meta.json', () => fontAPI('/fonts', { responseType: 'json' }), { + onError () { + logger.error('Could not download `fontsource` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontsource.') + return {} + } + }) + for (const id in fonts) { + familyMap.set(fonts[id]!.family!, id) + } +} + +function isFontsourceFont (family: string) { + return familyMap.has(family) +} + + +async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { + const id = familyMap.get(family) as keyof typeof fonts + const font = fonts[id]! + const weights = variants.weights.filter(weight => font.weights.includes(Number(weight))) + const styles = variants.styles.filter(style => font.styles.includes(style)) + if (weights.length === 0 || styles.length === 0) return [] + + const fontDetail = await fontAPI(`/fonts/${font.id}`, { responseType: 'json' }) + const fontFaceData: NormalizedFontFaceData[] = [] + + // TODO: support subsets apart from default + const defaultSubset = fontDetail.defSubset + + for (const weight of weights) { + for (const style of styles) { + const variantUrl = fontDetail.variants[weight]![style]![defaultSubset]!.url + fontFaceData.push({ + style, + weight, + src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })), + unicodeRange: fontDetail.unicodeRange[defaultSubset]?.split(',') + }) + } + } + + return addLocalFallbacks(family, fontFaceData) +} From 3eda57c1fe9f13d239e27ad9f3336b6af584b181 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 20 Mar 2024 14:30:11 +0000 Subject: [PATCH 25/37] feat: support system proxy when fetching fonts/metadata (#82) --- src/providers/adobe.ts | 4 ++-- src/providers/bunny.ts | 4 ++-- src/providers/fontshare.ts | 5 +++-- src/providers/fontsource.ts | 2 +- src/providers/google.ts | 6 +++--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/providers/adobe.ts b/src/providers/adobe.ts index 299b182..ab052f9 100644 --- a/src/providers/adobe.ts +++ b/src/providers/adobe.ts @@ -1,9 +1,9 @@ -import { $fetch } from 'ofetch' import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { $fetch } from '../fetch' import { logger } from '../logger' interface ProviderOption { @@ -106,7 +106,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions styles.push(style) } if (styles.length === 0) { continue } - const css = await fontCSSAPI(`${fonts.kits[kit]!.id}.css`) + const css = await fontCSSAPI(`${fonts.kits[kit]!.id}.css`) // Adobe uses slugs instead of names in its CSS to define its font faces, // so we need to first transform names into slugs. diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index bdaf9f7..40fdcf9 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -1,9 +1,9 @@ -import { $fetch } from 'ofetch' import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { $fetch } from '../fetch' import { logger } from '../logger' export default { @@ -75,7 +75,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const resolvedVariants = weights.flatMap(w => [...styles].map(s => `${w}${s}`)) - const css = await fontAPI('/css', { + const css = await fontAPI('/css', { query: { family: id + ':' + resolvedVariants.join(',') } diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts index aea04ee..300d31b 100644 --- a/src/providers/fontshare.ts +++ b/src/providers/fontshare.ts @@ -1,9 +1,9 @@ -import { $fetch } from 'ofetch' import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { $fetch } from '../fetch' import { logger } from '../logger' export default { @@ -69,6 +69,7 @@ async function initialiseFontMeta () { let chunk do { chunk = await fontAPI<{ fonts: FontshareFontMeta[], has_more: boolean }>('/fonts', { + responseType: 'json', query: { offset, limit: 100 @@ -105,7 +106,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions if (numbers.length === 0) return [] - const css = await fontAPI(`/css?f[]=${font.slug + '@' + numbers.join(',')}`) + const css = await fontAPI(`/css?f[]=${font.slug + '@' + numbers.join(',')}`) // TODO: support subsets and axes return addLocalFallbacks(family, extractFontFaceData(css)) diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts index 80ba6dc..86fbae2 100644 --- a/src/providers/fontsource.ts +++ b/src/providers/fontsource.ts @@ -1,9 +1,9 @@ -import { $fetch } from 'ofetch' import { hash } from 'ohash' import type { FontProvider, NormalizedFontFaceData, ResolveFontFacesOptions } from '../types' import { addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { $fetch } from '../fetch' import { logger } from '../logger' export default { diff --git a/src/providers/google.ts b/src/providers/google.ts index d7f1d7b..8e1814d 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -1,9 +1,9 @@ -import { $fetch } from 'ofetch' import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' import { extractFontFaceData, addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' +import { $fetch } from '../fetch' import { logger } from '../logger' export default { @@ -46,7 +46,7 @@ interface FontIndexMeta { let fonts: FontIndexMeta[] async function fetchFontMetadata () { - return await $fetch<{ familyMetadataList: FontIndexMeta[] }>('https://fonts.google.com/metadata/fonts') + return await $fetch<{ familyMetadataList: FontIndexMeta[] }>('https://fonts.google.com/metadata/fonts', { responseType: 'json' }) .then(r => r.familyMetadataList) } @@ -84,7 +84,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions let css = '' for (const extension in userAgents) { - css += await $fetch('/css2', { + css += await $fetch('/css2', { baseURL: 'https://fonts.googleapis.com', headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] }, query: { From 82f34b1fa3fa47acfe18d0c4b765d0219223ea86 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Thu, 21 Mar 2024 06:32:13 +0800 Subject: [PATCH 26/37] feat(fontsource): support subsets (#84) --- src/providers/fontsource.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts index 86fbae2..d1a0e4e 100644 --- a/src/providers/fontsource.ts +++ b/src/providers/fontsource.ts @@ -24,8 +24,6 @@ export default { }, } satisfies FontProvider -/** internal */ - const fontAPI = $fetch.create({ baseURL: 'https://api.fontsource.org/v1' }) @@ -103,23 +101,23 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const font = fonts[id]! const weights = variants.weights.filter(weight => font.weights.includes(Number(weight))) const styles = variants.styles.filter(style => font.styles.includes(style)) + const subsets = variants.subsets ? variants.subsets.filter(subset => font.subsets.includes(subset)) : [font.defSubset] if (weights.length === 0 || styles.length === 0) return [] const fontDetail = await fontAPI(`/fonts/${font.id}`, { responseType: 'json' }) const fontFaceData: NormalizedFontFaceData[] = [] - // TODO: support subsets apart from default - const defaultSubset = fontDetail.defSubset - - for (const weight of weights) { - for (const style of styles) { - const variantUrl = fontDetail.variants[weight]![style]![defaultSubset]!.url - fontFaceData.push({ - style, - weight, - src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })), - unicodeRange: fontDetail.unicodeRange[defaultSubset]?.split(',') - }) + for (const subset of subsets) { + for (const weight of weights) { + for (const style of styles) { + const variantUrl = fontDetail.variants[weight]![style]![subset]!.url + fontFaceData.push({ + style, + weight, + src: Object.entries(variantUrl).map(([format, url]) => ({url, format})), + unicodeRange: fontDetail.unicodeRange[subset]?.split(',') + }) + } } } From 0bb90591dcab4f86c237ec42f28b63096667fdd8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 26 Mar 2024 17:58:58 +0000 Subject: [PATCH 27/37] fix: adopt forward-compatible approach to `builder:watch` (#101) --- src/providers/local.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/providers/local.ts b/src/providers/local.ts index eb796f0..2dbffd6 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -31,6 +31,7 @@ export default { // Update registry when files change nuxt.hook('builder:watch', (event, relativePath) => { + relativePath = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, relativePath)) const path = resolve(nuxt.options.srcDir, relativePath) if (event === 'add' && isFontFile(path)) { registerFont(path) From 3c15a3374522c5763e5ab2fd2573b8ebf360a6bb Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Mon, 1 Apr 2024 16:50:37 +0800 Subject: [PATCH 28/37] feat(fontsource): support variable fonts (#102) --- src/providers/fontsource.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts index d1a0e4e..e7684fc 100644 --- a/src/providers/fontsource.ts +++ b/src/providers/fontsource.ts @@ -108,8 +108,18 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const fontFaceData: NormalizedFontFaceData[] = [] for (const subset of subsets) { - for (const weight of weights) { - for (const style of styles) { + for (const style of styles) { + if (font.variable) { + fontFaceData.push({ + style, + weight: [font.weights[0]!, font.weights.slice(-1)[0]!], + src: [ + { url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: "woff2" } + ], + unicodeRange: fontDetail.unicodeRange[subset]?.split(',') + }) + } + for (const weight of weights) { const variantUrl = fontDetail.variants[weight]![style]![subset]!.url fontFaceData.push({ style, From c882509188e203c7a44ddc98622227b7f587e4dc Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 10 Apr 2024 21:07:21 +0200 Subject: [PATCH 29/37] chore: migrate to eslint v9 (#121) --- src/providers/adobe.ts | 44 ++++++++++++++++++++++--------------- src/providers/bunny.ts | 32 ++++++++++++++------------- src/providers/fontshare.ts | 38 ++++++++++++++++++-------------- src/providers/fontsource.ts | 35 +++++++++++++++-------------- src/providers/google.ts | 34 ++++++++++++++-------------- src/providers/local.ts | 42 ++++++++++++++++++----------------- 6 files changed, 123 insertions(+), 102 deletions(-) diff --git a/src/providers/adobe.ts b/src/providers/adobe.ts index ab052f9..18d0925 100644 --- a/src/providers/adobe.ts +++ b/src/providers/adobe.ts @@ -11,30 +11,34 @@ interface ProviderOption { } export default { - async setup (options: ProviderOption) { - if (!options.id) { return } + async setup(options: ProviderOption) { + if (!options.id) { + return + } await initialiseFontMeta(typeof options.id === 'string' ? [options.id] : options.id) }, - async resolveFontFaces (fontFamily, defaults) { - if (!isAdobeFont(fontFamily)) { return } + async resolveFontFaces(fontFamily, defaults) { + if (!isAdobeFont(fontFamily)) { + return + } return { fonts: await cachedData(`adobe:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { - onError (err) { + onError(err) { logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`adobe\`.`, err) return [] - } - }) + }, + }), } }, } satisfies FontProvider const fontAPI = $fetch.create({ - baseURL: 'https://typekit.com' + baseURL: 'https://typekit.com', }) const fontCSSAPI = $fetch.create({ - baseURL: 'https://use.typekit.net' + baseURL: 'https://use.typekit.net', }) interface AdobeFontMeta { @@ -62,19 +66,19 @@ interface AdobeFontFamily { let fonts: AdobeFontMeta const familyMap = new Map() -async function getAdobeFontMeta (id: string): Promise { +async function getAdobeFontMeta(id: string): Promise { const { kit } = await fontAPI(`/api/v1/json/kits/${id}/published`, { responseType: 'json' }) return kit } -async function initialiseFontMeta (kits: string[]) { +async function initialiseFontMeta(kits: string[]) { fonts = { kits: await Promise.all(kits.map(id => cachedData(`adobe:meta-${id}.json`, () => getAdobeFontMeta(id), { - onError () { + onError() { logger.error('Could not download `adobe` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for adobe.') return null - } - }))).then(r => r.filter((meta): meta is AdobeFontKit => !!meta)) + }, + }))).then(r => r.filter((meta): meta is AdobeFontKit => !!meta)), } for (const kit in fonts.kits) { const families = fonts.kits[kit]!.families @@ -84,16 +88,18 @@ async function initialiseFontMeta (kits: string[]) { } } -function isAdobeFont (family: string) { +function isAdobeFont(family: string) { return familyMap.has(family) } -async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { +async function getFontDetails(family: string, variants: ResolveFontFacesOptions) { variants.weights = variants.weights.map(String) for (const kit in fonts.kits) { const font = fonts.kits[kit]!.families.find(f => f.name === family)! - if (!font) { continue } + if (!font) { + continue + } const styles: string[] = [] for (const style of font.variations) { @@ -105,7 +111,9 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions } styles.push(style) } - if (styles.length === 0) { continue } + if (styles.length === 0) { + continue + } const css = await fontCSSAPI(`${fonts.kits[kit]!.id}.css`) // Adobe uses slugs instead of names in its CSS to define its font faces, diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index 40fdcf9..93adeaa 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -7,19 +7,21 @@ import { $fetch } from '../fetch' import { logger } from '../logger' export default { - async setup () { + async setup() { await initialiseFontMeta() }, - async resolveFontFaces (fontFamily, defaults) { - if (!isBunnyFont(fontFamily)) { return } + async resolveFontFaces(fontFamily, defaults) { + if (!isBunnyFont(fontFamily)) { + return + } return { fonts: await cachedData(`bunny:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { - onError (err) { + onError(err) { logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`bunny\`.`, err) return [] - } - }) + }, + }), } }, } satisfies FontProvider @@ -27,7 +29,7 @@ export default { /** internal */ const fontAPI = $fetch.create({ - baseURL: 'https://fonts.bunny.net' + baseURL: 'https://fonts.bunny.net', }) interface BunnyFontMeta { @@ -45,30 +47,30 @@ interface BunnyFontMeta { let fonts: BunnyFontMeta const familyMap = new Map() -async function initialiseFontMeta () { +async function initialiseFontMeta() { fonts = await cachedData('bunny:meta.json', () => fontAPI('/list', { responseType: 'json' }), { - onError () { + onError() { logger.error('Could not download `bunny` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for bunny.') return {} - } + }, }) for (const id in fonts) { familyMap.set(fonts[id]!.familyName!, id) } } -function isBunnyFont (family: string) { +function isBunnyFont(family: string) { return familyMap.has(family) } -async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { +async function getFontDetails(family: string, variants: ResolveFontFacesOptions) { const id = familyMap.get(family) as keyof typeof fonts const font = fonts[id]! const weights = variants.weights.filter(weight => font.weights.includes(Number(weight))) const styleMap = { italic: 'i', oblique: 'i', - normal: '' + normal: '', } const styles = new Set(variants.styles.map(i => styleMap[i])) if (weights.length === 0 || styles.size === 0) return [] @@ -77,8 +79,8 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const css = await fontAPI('/css', { query: { - family: id + ':' + resolvedVariants.join(',') - } + family: id + ':' + resolvedVariants.join(','), + }, }) // TODO: support subsets diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts index 300d31b..de00c69 100644 --- a/src/providers/fontshare.ts +++ b/src/providers/fontshare.ts @@ -7,19 +7,21 @@ import { $fetch } from '../fetch' import { logger } from '../logger' export default { - async setup () { + async setup() { await initialiseFontMeta() }, - async resolveFontFaces (fontFamily, defaults) { - if (!isFontshareFont(fontFamily)) { return } + async resolveFontFaces(fontFamily, defaults) { + if (!isFontshareFont(fontFamily)) { + return + } return { fonts: await cachedData(`fontshare:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { - onError (err) { + onError(err) { logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`fontshare\`.`, err) return [] - } - }) + }, + }), } }, } satisfies FontProvider @@ -27,7 +29,7 @@ export default { /** internal */ const fontAPI = $fetch.create({ - baseURL: 'https://api.fontshare.com/v2' + baseURL: 'https://api.fontshare.com/v2', }) interface FontshareFontMeta { @@ -62,7 +64,7 @@ interface FontshareFontMeta { let fonts: FontshareFontMeta[] const families = new Set() -async function initialiseFontMeta () { +async function initialiseFontMeta() { fonts = await cachedData('fontshare:meta.json', async () => { const fonts: FontshareFontMeta[] = [] let offset = 0 @@ -72,35 +74,39 @@ async function initialiseFontMeta () { responseType: 'json', query: { offset, - limit: 100 - } + limit: 100, + }, }) fonts.push(...chunk.fonts) offset++ } while (chunk.has_more) return fonts }, { - onError () { + onError() { logger.error('Could not download `fontshare` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontshare.') return [] - } + }, }) for (const font of fonts) { families.add(font.name) } } -function isFontshareFont (family: string) { +function isFontshareFont(family: string) { return families.has(family) } -async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { +async function getFontDetails(family: string, variants: ResolveFontFacesOptions) { // https://api.fontshare.com/v2/css?f[]=alpino@300 const font = fonts.find(f => f.name === family)! const numbers: number[] = [] for (const style of font.styles) { - if (style.is_italic && !variants.styles.includes('italic')) { continue } - if (!variants.weights.includes(String(style.weight.number))) { continue } + if (style.is_italic && !variants.styles.includes('italic')) { + continue + } + if (!variants.weights.includes(String(style.weight.number))) { + continue + } numbers.push(style.weight.number) } diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts index e7684fc..7bb9dbc 100644 --- a/src/providers/fontsource.ts +++ b/src/providers/fontsource.ts @@ -7,25 +7,27 @@ import { $fetch } from '../fetch' import { logger } from '../logger' export default { - async setup () { + async setup() { await initialiseFontMeta() }, - async resolveFontFaces (fontFamily, defaults) { - if (!isFontsourceFont(fontFamily)) { return } + async resolveFontFaces(fontFamily, defaults) { + if (!isFontsourceFont(fontFamily)) { + return + } return { fonts: await cachedData(`fontsource:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { - onError (err) { + onError(err) { logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`fontsource\`.`, err) return [] - } - }) + }, + }), } }, } satisfies FontProvider const fontAPI = $fetch.create({ - baseURL: 'https://api.fontsource.org/v1' + baseURL: 'https://api.fontsource.org/v1', }) export interface FontsourceFontMeta { @@ -79,24 +81,23 @@ interface FontsourceFontDetail { let fonts: FontsourceFontMeta const familyMap = new Map() -async function initialiseFontMeta () { +async function initialiseFontMeta() { fonts = await cachedData('fontsource:meta.json', () => fontAPI('/fonts', { responseType: 'json' }), { - onError () { + onError() { logger.error('Could not download `fontsource` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontsource.') return {} - } + }, }) for (const id in fonts) { familyMap.set(fonts[id]!.family!, id) } } -function isFontsourceFont (family: string) { +function isFontsourceFont(family: string) { return familyMap.has(family) } - -async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { +async function getFontDetails(family: string, variants: ResolveFontFacesOptions) { const id = familyMap.get(family) as keyof typeof fonts const font = fonts[id]! const weights = variants.weights.filter(weight => font.weights.includes(Number(weight))) @@ -114,9 +115,9 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions style, weight: [font.weights[0]!, font.weights.slice(-1)[0]!], src: [ - { url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: "woff2" } + { url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: 'woff2' }, ], - unicodeRange: fontDetail.unicodeRange[subset]?.split(',') + unicodeRange: fontDetail.unicodeRange[subset]?.split(','), }) } for (const weight of weights) { @@ -124,8 +125,8 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions fontFaceData.push({ style, weight, - src: Object.entries(variantUrl).map(([format, url]) => ({url, format})), - unicodeRange: fontDetail.unicodeRange[subset]?.split(',') + src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })), + unicodeRange: fontDetail.unicodeRange[subset]?.split(','), }) } } diff --git a/src/providers/google.ts b/src/providers/google.ts index 8e1814d..c13ef57 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -7,19 +7,21 @@ import { $fetch } from '../fetch' import { logger } from '../logger' export default { - async setup () { + async setup() { await initialiseFontMeta() }, - async resolveFontFaces (fontFamily, defaults) { - if (!isGoogleFont(fontFamily)) { return } + async resolveFontFaces(fontFamily, defaults) { + if (!isGoogleFont(fontFamily)) { + return + } return { fonts: await cachedData(`google:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), { - onError (err) { + onError(err) { logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`google\`.`, err) return [] - } - }) + }, + }), } }, } satisfies FontProvider @@ -45,30 +47,30 @@ interface FontIndexMeta { let fonts: FontIndexMeta[] -async function fetchFontMetadata () { +async function fetchFontMetadata() { return await $fetch<{ familyMetadataList: FontIndexMeta[] }>('https://fonts.google.com/metadata/fonts', { responseType: 'json' }) .then(r => r.familyMetadataList) } -async function initialiseFontMeta () { +async function initialiseFontMeta() { fonts = await cachedData('google:meta.json', fetchFontMetadata, { - onError () { + onError() { logger.error('Could not download `google` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for google.') return [] - } + }, }) } -function isGoogleFont (family: string) { +function isGoogleFont(family: string) { return fonts.some(font => font.family === family) } const styleMap = { italic: '1', oblique: '1', - normal: '0' + normal: '0', } -async function getFontDetails (family: string, variants: ResolveFontFacesOptions) { +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() @@ -88,8 +90,8 @@ 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 + ':' + 'ital,wght@' + resolvedVariants.join(';'), + }, }) } @@ -99,7 +101,7 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions const userAgents = { woff2: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', - ttf: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16' + ttf: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16', // eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)', // woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0', // svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3', diff --git a/src/providers/local.ts b/src/providers/local.ts index 2dbffd6..dcfc641 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -3,8 +3,8 @@ import { join, relative, resolve } from 'pathe' import { filename } from 'pathe/utils' import { anyOf, createRegExp, not, wordBoundary } from 'magic-regexp' -import type { FontFaceData, FontProvider } from '../types' import { withLeadingSlash, withTrailingSlash } from 'ufo' +import type { FontFaceData, FontProvider } from '../types' const providerContext = { rootPaths: [] as string[], @@ -12,13 +12,13 @@ const providerContext = { } export default { - async setup (_options, nuxt) { + async setup(_options, nuxt) { // Scan for all font files in public directories for (const layer of nuxt.options._layers) { const publicDir = join(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public') const possibleFontFiles = await globby('**/*.{ttf,woff,woff2,eot,otf}', { absolute: true, - cwd: publicDir + cwd: publicDir, }) providerContext.rootPaths.push(withTrailingSlash(publicDir)) for (const file of possibleFontFiles) { @@ -41,7 +41,7 @@ export default { } }) }, - resolveFontFaces (fontFamily, defaults) { + resolveFontFaces(fontFamily, defaults) { const fonts: FontFaceData[] = [] // Resolve font files for each combination of weight, style and subset @@ -74,15 +74,15 @@ const NON_WORD_RE = /[^\w\d]+/g export const isFontFile = (id: string) => FONT_RE.test(id) const weightMap: Record = { - '100': 'thin', - '200': 'extra-light', - '300': 'light', - '400': 'normal', - '500': 'medium', - '600': 'semi-bold', - '700': 'bold', - '800': 'extra-bold', - '900': 'black', + 100: 'thin', + 200: 'extra-light', + 300: 'light', + 400: 'normal', + 500: 'medium', + 600: 'semi-bold', + 700: 'bold', + 800: 'extra-bold', + 900: 'black', } const weights = Object.entries(weightMap).flatMap(e => e).filter(r => r !== 'normal') @@ -102,7 +102,7 @@ const subsets = [ ] as const const SUBSET_RE = createRegExp(anyOf(...subsets).groupedAs('subset').before(not.wordChar.or(wordBoundary)), ['i']) -function generateSlugs (path: string) { +function generateSlugs(path: string) { let name = filename(path) const weight = name.match(WEIGHT_RE)?.groups?.weight || 'normal' @@ -120,14 +120,14 @@ function generateSlugs (path: string) { fontFamilyToSlug(slug.replace(/[\W._-]+$/, '')), weightMap[weight] || weight, style, - subset + subset, ].join('-').toLowerCase()) } return [...slugs] } -function registerFont (path: string) { +function registerFont(path: string) { const slugs = generateSlugs(path) for (const slug of slugs) { providerContext.registry[slug] ||= [] @@ -135,7 +135,7 @@ function registerFont (path: string) { } } -function unregisterFont (path: string) { +function unregisterFont(path: string) { const slugs = generateSlugs(path) for (const slug of slugs) { providerContext.registry[slug] ||= [] @@ -144,10 +144,12 @@ function unregisterFont (path: string) { } const extensionPriority = ['woff2', 'woff', 'ttf', 'otf', 'eot'] -function lookupFont (family: string, suffixes: Array): string[] { +function lookupFont(family: string, suffixes: Array): string[] { const slug = [fontFamilyToSlug(family), ...suffixes].join('-') const paths = providerContext.registry[slug] - if (!paths || paths.length === 0) { return [] } + if (!paths || paths.length === 0) { + return [] + } const fonts = new Set() for (const path of paths) { @@ -163,6 +165,6 @@ function lookupFont (family: string, suffixes: Array): string[] }) } -function fontFamilyToSlug (family: string) { +function fontFamilyToSlug(family: string) { return family.toLowerCase().replace(NON_WORD_RE, '') } From 60ce3b44a5061af6827d6c5ecf7f61c4a7c0d1ca Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 20 May 2024 16:19:24 +0100 Subject: [PATCH 30/37] fix(local): scan for fonts in all public assets dirs --- src/providers/local.ts | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index dcfc641..2dc7b43 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -12,22 +12,24 @@ const providerContext = { } export default { - async setup(_options, nuxt) { - // Scan for all font files in public directories - for (const layer of nuxt.options._layers) { - const publicDir = join(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public') - const possibleFontFiles = await globby('**/*.{ttf,woff,woff2,eot,otf}', { - absolute: true, - cwd: publicDir, - }) - providerContext.rootPaths.push(withTrailingSlash(publicDir)) - for (const file of possibleFontFiles) { - registerFont(file) + setup(_options, nuxt) { + // TODO: rework when providers can respond with font metric data + // Scan for all font files in public asset directories + nuxt.hook('nitro:init', async (nitro) => { + for (const assetsDir of nitro.options.publicAssets) { + const possibleFontFiles = await globby('**/*.{ttf,woff,woff2,eot,otf}', { + absolute: true, + cwd: assetsDir.dir, + }) + providerContext.rootPaths.push(withTrailingSlash(assetsDir.dir)) + for (const file of possibleFontFiles) { + registerFont(file.replace(assetsDir.dir, join(assetsDir.dir, assetsDir.baseURL || '/'))) + } } - } - // Sort rootPaths so we resolve to most specific path first - providerContext.rootPaths = providerContext.rootPaths.sort((a, b) => b.length - a.length) + // Sort rootPaths so we resolve to most specific path first + providerContext.rootPaths = providerContext.rootPaths.sort((a, b) => b.length - a.length) + }) // Update registry when files change nuxt.hook('builder:watch', (event, relativePath) => { @@ -73,6 +75,8 @@ const NON_WORD_RE = /[^\w\d]+/g export const isFontFile = (id: string) => FONT_RE.test(id) +// TODO: support without hyphen +// TODO: support reading font metrics const weightMap: Record = { 100: 'thin', 200: 'extra-light', @@ -86,7 +90,7 @@ const weightMap: Record = { } const weights = Object.entries(weightMap).flatMap(e => e).filter(r => r !== 'normal') -const WEIGHT_RE = createRegExp(anyOf(...weights).groupedAs('weight').after(not.digit).before(not.digit.or(wordBoundary)), ['i']) +const WEIGHT_RE = createRegExp(anyOf(...new Set([...weights, ...weights.map(w => w.replace('-', ''))])).groupedAs('weight').after(not.digit).before(not.digit.or(wordBoundary)), ['i']) const styles = ['italic', 'oblique'] as const const STYLE_RE = createRegExp(anyOf(...styles).groupedAs('style').before(not.wordChar.or(wordBoundary)), ['i']) From 545a0968ba9e511671abd661b8ee5baf616c181b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 21:44:15 +0100 Subject: [PATCH 31/37] chore(deps): update all non-major dependencies (#157) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Roe --- src/providers/local.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index 2dc7b43..26c3ffd 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -70,8 +70,8 @@ export default { }, } satisfies FontProvider -const FONT_RE = /\.(ttf|woff|woff2|eot|otf)(\?[^.]+)?$/ -const NON_WORD_RE = /[^\w\d]+/g +const FONT_RE = /\.(?:ttf|woff|woff2|eot|otf)(?:\?[^.]+)?$/ +const NON_WORD_RE = /\W+/g export const isFontFile = (id: string) => FONT_RE.test(id) @@ -119,9 +119,9 @@ function generateSlugs(path: string) { const slugs = new Set() - for (const slug of [name.replace(/[.][\w\d]*$/, ''), name.replace(/[._-][\w\d]*$/, '')]) { + for (const slug of [name.replace(/\.\w*$/, ''), name.replace(/[._-]\w*$/, '')]) { slugs.add([ - fontFamilyToSlug(slug.replace(/[\W._-]+$/, '')), + fontFamilyToSlug(slug.replace(/[\W_]+$/, '')), weightMap[weight] || weight, style, subset, From 8ad40dc1e7b55fb4902beafdfdfc9c3472618a5c Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Thu, 4 Jul 2024 13:50:30 +0800 Subject: [PATCH 32/37] fix(adobe): use provided `css_names` from API (#192) --- src/providers/adobe.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/providers/adobe.ts b/src/providers/adobe.ts index 18d0925..e2ac33b 100644 --- a/src/providers/adobe.ts +++ b/src/providers/adobe.ts @@ -116,10 +116,10 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) } const css = await fontCSSAPI(`${fonts.kits[kit]!.id}.css`) - // Adobe uses slugs instead of names in its CSS to define its font faces, - // so we need to first transform names into slugs. - const slug = family.toLowerCase().split(' ').join('-') - return addLocalFallbacks(family, extractFontFaceData(css, slug)) + // TODO: Not sure whether this css_names array always has a single element. Still need to investigate. + const cssName = font.css_names[0] ?? family.toLowerCase().split(' ').join('-') + + return addLocalFallbacks(family, extractFontFaceData(css, cssName)) } return [] From 1001651306ca73133d4af55df1125c13311500a2 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Fri, 12 Jul 2024 15:39:16 +0800 Subject: [PATCH 33/37] fix(fontsource): use `/variable` endpoint for retrieving variable axes (#196) --- src/providers/fontsource.ts | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts index 7bb9dbc..0ed0cfe 100644 --- a/src/providers/fontsource.ts +++ b/src/providers/fontsource.ts @@ -78,6 +78,18 @@ interface FontsourceFontDetail { variants: FontsourceFontVariant } +interface FontsourceVariableAxesData { + default: string + min: string + max: string + step: string +} + +interface FontsourceVariableFontDetail { + axes: Record + family: string +} + let fonts: FontsourceFontMeta const familyMap = new Map() @@ -111,14 +123,22 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) for (const subset of subsets) { for (const style of styles) { if (font.variable) { - fontFaceData.push({ - style, - weight: [font.weights[0]!, font.weights.slice(-1)[0]!], - src: [ - { url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: 'woff2' }, - ], - unicodeRange: fontDetail.unicodeRange[subset]?.split(','), + const variableAxes = await cachedData(`fontsource:${font.family}-axes.json`, () => fontAPI(`/variable/${font.id}`, { responseType: 'json' }), { + onError() { + logger.error(`Could not download variable axes metadata for ${font.family} from \`fontsource\`. \`@nuxt/fonts\` will not be able to inject variable axes for ${font.family}.`) + return undefined + }, }) + if (variableAxes && variableAxes.axes['wght']) { + fontFaceData.push({ + style, + weight: [Number(variableAxes.axes['wght'].min), Number(variableAxes.axes['wght'].max)], + src: [ + { url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: 'woff2' }, + ], + unicodeRange: fontDetail.unicodeRange[subset]?.split(','), + }) + } } for (const weight of weights) { const variantUrl = fontDetail.variants[weight]![style]![subset]!.url From 6848c2100d129f3e72e03f0abdc71460aa024d1a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 7 Aug 2024 05:51:57 -0700 Subject: [PATCH 34/37] perf(local): use `tinyglobby` to scan font files (#205) --- src/providers/local.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index 26c3ffd..03ef9fb 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -1,4 +1,4 @@ -import { globby } from 'globby' +import { glob } from 'tinyglobby' import { join, relative, resolve } from 'pathe' import { filename } from 'pathe/utils' import { anyOf, createRegExp, not, wordBoundary } from 'magic-regexp' @@ -17,7 +17,7 @@ export default { // Scan for all font files in public asset directories nuxt.hook('nitro:init', async (nitro) => { for (const assetsDir of nitro.options.publicAssets) { - const possibleFontFiles = await globby('**/*.{ttf,woff,woff2,eot,otf}', { + const possibleFontFiles = await glob(['**/*.{ttf,woff,woff2,eot,otf}'], { absolute: true, cwd: assetsDir.dir, }) From bd7266f48acb3ed6c5b3987ee736e18d0b0ae6ab Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Thu, 12 Sep 2024 06:00:39 +0800 Subject: [PATCH 35/37] feat: `googleicons` provider (#133) --- src/providers/googleicons.ts | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/providers/googleicons.ts diff --git a/src/providers/googleicons.ts b/src/providers/googleicons.ts new file mode 100644 index 0000000..d511617 --- /dev/null +++ b/src/providers/googleicons.ts @@ -0,0 +1,98 @@ +import { hash } from 'ohash' + +import type { FontProvider } from '../types' +import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { cachedData } from '../cache' +import { $fetch } from '../fetch' +import { logger } from '../logger' + +export default { + async setup() { + await initialiseFontMeta() + }, + async resolveFontFaces(fontFamily, defaults) { + if (!isGoogleIcon(fontFamily)) { + return + } + + return { + fonts: await cachedData(`googleicons:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily), { + onError(err) { + logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`googleicons\`.`, err) + return [] + }, + }), + } + }, +} satisfies FontProvider + +/** internal */ + +let fonts: string[] + +async function fetchFontMetadata() { + const response: { families: string[] } = JSON.parse((await $fetch( + 'https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true', + )).split('\n').slice(1).join('\n')) // remove the first line which makes it an invalid JSON + + return response.families +} + +async function initialiseFontMeta() { + fonts = await cachedData('googleicons:meta.json', fetchFontMetadata, { + onError() { + logger.error('Could not download `googleicons` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for googleicons.') + return [] + }, + }) +} + +function isGoogleIcon(family: string) { + return fonts.includes(family) +} + +async function getFontDetails(family: string) { + let css = '' + + if (family.includes('Icons')) { + css += await $fetch('/css2', { + baseURL: 'https://fonts.googleapis.com/icon', + query: { + family: family, + }, + }) + } + + for (const extension in userAgents) { + // Legacy Material Icons + if (family.includes('Icons')) { + css += await $fetch('/icon', { + baseURL: 'https://fonts.googleapis.com', + headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] }, + query: { + family: family, + }, + }) + } + // New Material Symbols + else { + css += await $fetch('/css2', { + baseURL: 'https://fonts.googleapis.com', + headers: { 'user-agent': userAgents[extension as keyof typeof userAgents] }, + query: { + family: family + ':' + 'opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200', + }, + }) + } + } + + return addLocalFallbacks(family, extractFontFaceData(css)) +} + +const userAgents = { + woff2: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + ttf: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16', + // eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)', + // woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0', + // svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3', +} From 87a3ec188c2c35eac913c6256d5090a4b5e86d07 Mon Sep 17 00:00:00 2001 From: Tom Tang Date: Thu, 12 Sep 2024 06:31:04 +0800 Subject: [PATCH 36/37] feat: allow experimentally disabling local fallbacks (#225) --- src/providers/adobe.ts | 4 ++-- src/providers/bunny.ts | 4 ++-- src/providers/fontshare.ts | 4 ++-- src/providers/fontsource.ts | 3 +-- src/providers/google.ts | 4 ++-- src/providers/googleicons.ts | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/providers/adobe.ts b/src/providers/adobe.ts index e2ac33b..2ff3dbe 100644 --- a/src/providers/adobe.ts +++ b/src/providers/adobe.ts @@ -1,7 +1,7 @@ import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' -import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { extractFontFaceData } from '../css/parse' import { cachedData } from '../cache' import { $fetch } from '../fetch' import { logger } from '../logger' @@ -119,7 +119,7 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) // TODO: Not sure whether this css_names array always has a single element. Still need to investigate. const cssName = font.css_names[0] ?? family.toLowerCase().split(' ').join('-') - return addLocalFallbacks(family, extractFontFaceData(css, cssName)) + return extractFontFaceData(css, cssName) } return [] diff --git a/src/providers/bunny.ts b/src/providers/bunny.ts index 93adeaa..4660777 100644 --- a/src/providers/bunny.ts +++ b/src/providers/bunny.ts @@ -1,7 +1,7 @@ import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' -import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { extractFontFaceData } from '../css/parse' import { cachedData } from '../cache' import { $fetch } from '../fetch' import { logger } from '../logger' @@ -84,5 +84,5 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) }) // TODO: support subsets - return addLocalFallbacks(family, extractFontFaceData(css)) + return extractFontFaceData(css) } diff --git a/src/providers/fontshare.ts b/src/providers/fontshare.ts index de00c69..4b3e304 100644 --- a/src/providers/fontshare.ts +++ b/src/providers/fontshare.ts @@ -1,7 +1,7 @@ import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' -import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { extractFontFaceData } from '../css/parse' import { cachedData } from '../cache' import { $fetch } from '../fetch' import { logger } from '../logger' @@ -115,5 +115,5 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) const css = await fontAPI(`/css?f[]=${font.slug + '@' + numbers.join(',')}`) // TODO: support subsets and axes - return addLocalFallbacks(family, extractFontFaceData(css)) + return extractFontFaceData(css) } diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts index 0ed0cfe..0929e2e 100644 --- a/src/providers/fontsource.ts +++ b/src/providers/fontsource.ts @@ -1,7 +1,6 @@ import { hash } from 'ohash' import type { FontProvider, NormalizedFontFaceData, ResolveFontFacesOptions } from '../types' -import { addLocalFallbacks } from '../css/parse' import { cachedData } from '../cache' import { $fetch } from '../fetch' import { logger } from '../logger' @@ -152,5 +151,5 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) } } - return addLocalFallbacks(family, fontFaceData) + return fontFaceData } diff --git a/src/providers/google.ts b/src/providers/google.ts index c13ef57..057cd60 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -1,7 +1,7 @@ import { hash } from 'ohash' import type { FontProvider, ResolveFontFacesOptions } from '../types' -import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { extractFontFaceData } from '../css/parse' import { cachedData } from '../cache' import { $fetch } from '../fetch' import { logger } from '../logger' @@ -96,7 +96,7 @@ async function getFontDetails(family: string, variants: ResolveFontFacesOptions) } // TODO: support subsets - return addLocalFallbacks(family, extractFontFaceData(css)) + return extractFontFaceData(css) } const userAgents = { diff --git a/src/providers/googleicons.ts b/src/providers/googleicons.ts index d511617..f17a82e 100644 --- a/src/providers/googleicons.ts +++ b/src/providers/googleicons.ts @@ -1,7 +1,7 @@ import { hash } from 'ohash' import type { FontProvider } from '../types' -import { extractFontFaceData, addLocalFallbacks } from '../css/parse' +import { extractFontFaceData } from '../css/parse' import { cachedData } from '../cache' import { $fetch } from '../fetch' import { logger } from '../logger' @@ -86,7 +86,7 @@ async function getFontDetails(family: string) { } } - return addLocalFallbacks(family, extractFontFaceData(css)) + return extractFontFaceData(css) } const userAgents = { From ea8e5e19fd4d5dded6d8a0539df707e6ec0a8ac2 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 23 Sep 2024 13:02:47 +0100 Subject: [PATCH 37/37] fix(local): use `extname` for extension priority test resolves https://github.com/nuxt/fonts/issues/135 resolves https://github.com/nuxt/fonts/issues/223 --- src/providers/local.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/providers/local.ts b/src/providers/local.ts index 03ef9fb..4095444 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -1,5 +1,5 @@ import { glob } from 'tinyglobby' -import { join, relative, resolve } from 'pathe' +import { join, extname, relative, resolve } from 'pathe' import { filename } from 'pathe/utils' import { anyOf, createRegExp, not, wordBoundary } from 'magic-regexp' @@ -147,7 +147,7 @@ function unregisterFont(path: string) { } } -const extensionPriority = ['woff2', 'woff', 'ttf', 'otf', 'eot'] +const extensionPriority = ['.woff2', '.woff', '.ttf', '.otf', '.eot'] function lookupFont(family: string, suffixes: Array): string[] { const slug = [fontFamilyToSlug(family), ...suffixes].join('-') const paths = providerContext.registry[slug] @@ -162,8 +162,8 @@ function lookupFont(family: string, suffixes: Array): string[] } return [...fonts].sort((a, b) => { - const extA = filename(a).split('.').pop()! - const extB = filename(b).split('.').pop()! + const extA = extname(a) + const extB = extname(b) return extensionPriority.indexOf(extA) - extensionPriority.indexOf(extB) })