Skip to content

Commit

Permalink
chore: merge provider work from nuxt/fonts
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Oct 1, 2024
2 parents 73444c7 + ea8e5e1 commit 420838c
Show file tree
Hide file tree
Showing 7 changed files with 868 additions and 0 deletions.
126 changes: 126 additions & 0 deletions src/providers/adobe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { hash } from 'ohash'

Check failure on line 1 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find module 'ohash' or its corresponding type declarations.

import type { FontProvider, ResolveFontFacesOptions } from '../types'

Check failure on line 3 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find module '../types' or its corresponding type declarations.
import { extractFontFaceData } from '../css/parse'

Check failure on line 4 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find module '../css/parse' or its corresponding type declarations.
import { cachedData } from '../cache'

Check failure on line 5 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find module '../cache' or its corresponding type declarations.
import { $fetch } from '../fetch'

Check failure on line 6 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find module '../fetch' or its corresponding type declarations.
import { logger } from '../logger'

Check failure on line 7 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find module '../logger' or its corresponding type declarations.

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) {

Check failure on line 20 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Parameter 'fontFamily' implicitly has an 'any' type.

Check failure on line 20 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Parameter 'defaults' implicitly has an 'any' type.
if (!isAdobeFont(fontFamily)) {
return
}

return {
fonts: await cachedData(`adobe:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), {
onError(err) {

Check failure on line 27 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Parameter 'err' implicitly has an 'any' type.
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<string, string>()

async function getAdobeFontMeta(id: string): Promise<AdobeFontKit> {
const { kit } = await fontAPI<AdobeFontAPI>(`/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)),

Check failure on line 81 in src/providers/adobe.ts

View workflow job for this annotation

GitHub Actions / test

Parameter 'meta' implicitly has an 'any' type.
}
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<string>(`${fonts.kits[kit]!.id}.css`)

// 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 extractFontFaceData(css, cssName)
}

return []
}
88 changes: 88 additions & 0 deletions src/providers/bunny.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { hash } from 'ohash'

import type { FontProvider, ResolveFontFacesOptions } from '../types'
import { extractFontFaceData } 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 (!isBunnyFont(fontFamily)) {
return
}

return {
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

/** internal */

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<string, number>
weights: number[]
}
}

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

async function initialiseFontMeta() {
fonts = await cachedData('bunny:meta.json', () => fontAPI<BunnyFontMeta>('/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)
}
}

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)))
const styleMap = {
italic: 'i',
oblique: 'i',
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<string>('/css', {
query: {
family: id + ':' + resolvedVariants.join(','),
},
})

// TODO: support subsets
return extractFontFaceData(css)
}
119 changes: 119 additions & 0 deletions src/providers/fontshare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { hash } from 'ohash'

import type { FontProvider, ResolveFontFacesOptions } from '../types'
import { extractFontFaceData } 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 (!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<string>()

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', {
responseType: 'json',
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(String(style.weight.number))) {
continue
}
numbers.push(style.weight.number)
}

if (numbers.length === 0) return []

const css = await fontAPI<string>(`/css?f[]=${font.slug + '@' + numbers.join(',')}`)

// TODO: support subsets and axes
return extractFontFaceData(css)
}
Loading

0 comments on commit 420838c

Please sign in to comment.