Skip to content

Commit

Permalink
feat: fontsource provider (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwerzl committed Mar 20, 2024
1 parent dae1f2f commit f9379b7
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 2 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Plug-and-play custom web font optimization and configuration for Nuxt apps.
## Features

- ✨ zero-configuration required
- 🔡 built-in providers (`google`, `bunny`, `fontshare`, `adobe`, `local` - more welcome!)
- 🔡 built-in providers (`google`, `bunny`, `fontshare`, `fontsource`, `adobe`, `local` - more welcome!)
- 💪 custom providers for full control
- ⏬ local download support (until `nuxt/assets` lands)
- ⚡️ automatic font metric optimisation powered by [**fontaine**](https://github.com/unjs/fontaine) and [**capsize**](https://github.com/seek-oss/capsize)
Expand Down Expand Up @@ -187,6 +187,10 @@ Then, when you use a `font-family` in your CSS, we check to see whether it match

You should read [their terms in full](https://www.fontshare.com/licenses/itf-ffl) before using a font through `fontshare`.

### `fontsource`

[Fontsource](https://fontsource.org/docs/getting-started/introduction) is a collection of open-source fonts that are designed for self-hosting in web applications.

### `adobe`

[Adobe Fonts](https://fonts.adobe.com/) is a font service for both personal and commercial use included with Creative Cloud subscriptions.
Expand Down
3 changes: 2 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export default defineNuxtConfig({
{ name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
{ name: 'Oswald', fallbacks: ['Times New Roman'] },
{ name: 'Aleo', provider: 'adobe'},
{ name: 'Barlow Semi Condensed', provider: 'adobe' }
{ name: 'Barlow Semi Condensed', provider: 'adobe' },
{ name: 'Roboto Mono', provider: 'fontsource' },
],
adobe: {
id: ['sij5ufr', 'grx7wdj'],
Expand Down
11 changes: 11 additions & 0 deletions playground/pages/providers/fontsource.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>
fontsource
</div>
</template>

<style scoped>
div {
font-family: 'Roboto Mono', sans-serif;
}
</style>
2 changes: 2 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import google from './providers/google'
import bunny from './providers/bunny'
import fontshare from './providers/fontshare'
import adobe from './providers/adobe'
import fontsource from './providers/fontsource'

import { FontFamilyInjectionPlugin, type FontFaceResolution } from './plugins/transform'
import { generateFontFace } from './css/render'
Expand Down Expand Up @@ -95,6 +96,7 @@ export default defineNuxtModule<ModuleOptions>({
google,
bunny,
fontshare,
fontsource,
},
},
async setup (options, nuxt) {
Expand Down
127 changes: 127 additions & 0 deletions src/providers/fontsource.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>
defSubset: string
variable: boolean
lastModified: string
category: string
version: string
type: string
variants: FontsourceFontVariant
}

let fonts: FontsourceFontMeta
const familyMap = new Map<string, string>()

async function initialiseFontMeta () {
fonts = await cachedData('fontsource:meta.json', () => fontAPI<FontsourceFontMeta[]>('/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<FontsourceFontDetail>(`/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)
}
10 changes: 10 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ describe('providers', async () => {
`)
})

it('generates inlined font face rules for `fontsource` provider', async () => {
const html = await $fetch('/providers/fontsource')
expect(extractFontFaces('Roboto Mono', html)).toMatchInlineSnapshot(`
[
"@font-face{font-family:Roboto Mono;src:local("Roboto Mono"),url(/_fonts/file.woff2) format(woff2),url(/_fonts/file.woff) format(woff),url(/_fonts/file.ttf) format(ttf);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight:400;font-style:italic}",
"@font-face{font-family:Roboto Mono;src:local("Roboto Mono"),url(/_fonts/file.woff2) format(woff2),url(/_fonts/file.woff) format(woff),url(/_fonts/file.ttf) format(ttf);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight: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)
Expand Down

0 comments on commit f9379b7

Please sign in to comment.