diff --git a/README.md b/README.md index 1209097..fc44be9 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Plug-and-play custom web font optimization and configuration for Nuxt apps. - [x] built-in providers - [x] `google` - [x] `local` + - [x] `bunny` - [ ] `fontshare` - [ ] `fontsource` - - [ ] `bunny` - [x] custom providers for full control - [x] local download support (until `nuxt/assets` lands) - [ ] automatic font metric optimisation powered by https://github.com/unjs/fontaine diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 5a8142a..a8ad8db 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -5,6 +5,7 @@ export default defineNuxtConfig({ custom: '~/providers/custom' }, families: [ + { name: 'Abel', provider: 'bunny' }, { name: 'Kode Mono', provider: 'none' }, { name: 'MyCustom', src: '/font.woff2' }, { name: 'CustomGlobal', global: true, src: '/font-global.woff2' }, diff --git a/playground/pages/providers/bunny.vue b/playground/pages/providers/bunny.vue new file mode 100644 index 0000000..88a398d --- /dev/null +++ b/playground/pages/providers/bunny.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/module.ts b/src/module.ts index dd09287..c9cbcaf 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,8 +1,9 @@ import { addBuildPlugin, addTemplate, defineNuxtModule, resolveAlias, resolvePath, useLogger, useNuxt } from '@nuxt/kit' import jiti from 'jiti' -import google from './providers/google' import local from './providers/local' +import google from './providers/google' +import bunny from './providers/bunny' import { FontFamilyInjectionPlugin } from './plugins/transform' import { generateFontFaces } from './css/render' @@ -40,6 +41,7 @@ export default defineNuxtModule({ providers: { local, google, + bunny, }, }, async setup (options, nuxt) { @@ -92,7 +94,7 @@ export default defineNuxtModule({ // Handle explicit provider if (override.provider) { if (override.provider in providers) { - const result = await providers[override.provider]!.resolveFontFaces!(fontFamily, override as ResolveFontFacesOptions) + const result = await providers[override.provider]!.resolveFontFaces!(fontFamily, defaults as ResolveFontFacesOptions) if (!result) { return logger.warn(`Could not produce font face declaration from \`${override.provider}\` for font family \`${fontFamily}\`.`) } 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) +} diff --git a/test/basic.test.ts b/test/basic.test.ts index 0915ae6..66b4945 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -23,6 +23,22 @@ describe('providers', async () => { `) }) + it('generates inlined font face rules for `bunny` provider', async () => { + const html = await $fetch('/providers/bunny') + expect(extractFontFaces('Abel', html)).toMatchInlineSnapshot(` + [ + "@font-face { + font-family: 'Abel'; + src: url("/file.woff2") format(woff2), url("/file.woff") format(woff); + font-display: swap; + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; + font-weight: 400; + font-style: normal; + }", + ] + `) + }) + it('generates inlined font face rules for `google` provider', async () => { const html = await $fetch('/providers/google') const poppins = extractFontFaces('Poppins', html)