Skip to content

Commit

Permalink
feat: add adobe fonts provider (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwerzl authored Mar 12, 2024
1 parent e864de2 commit 12a51d3
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 2 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,23 @@ 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`.

### `adobe`

[Adobe Fonts](https://fonts.adobe.com/) is a font service for both personal and commercial use included with Creative Cloud subscriptions.

To configure the Adobe provider in your Nuxt app, you must provide a Project ID or array of Project IDs corresponding to the Web Projects you have created in Adobe Fonts.

```ts
export default defineNuxtConfig({
modules: ['@nuxt/fonts'],
fonts: {
adobe: {
id: ['<some-id>', '<another-kit-id>'],
},
}
})
```

### Writing a custom provider

The provider API is likely to evolve in the next few releases of Nuxt Fonts, but at the moment it looks like this:
Expand Down
5 changes: 5 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export default defineNuxtConfig({
{ name: 'MyCustom', src: '/custom-font.woff2' },
{ name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
{ name: 'Oswald', fallbacks: ['Times New Roman'] },
{ name: 'Aleo', provider: 'adobe'},
{ name: 'Barlow Semi Condensed', provider: 'adobe' }
],
adobe: {
id: ['sij5ufr', 'grx7wdj'],
},
defaults: {
fallbacks: {
monospace: ['Tahoma']
Expand Down
11 changes: 11 additions & 0 deletions playground/pages/providers/adobe.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>
Nuxt module playground!
</div>
</template>

<style scoped>
div {
font-family: 'Aleo', sans-serif;
}
</style>
11 changes: 11 additions & 0 deletions playground/pages/providers/adobe2.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>
Nuxt module playground!
</div>
</template>

<style scoped>
div {
font-family: 'Barlow Semi Condensed', sans-serif;
}
</style>
12 changes: 11 additions & 1 deletion src/css/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const extractableKeyMap: Record<string, keyof NormalizedFontFaceData> = {
'unicode-range': 'unicodeRange',
}

export function extractFontFaceData (css: string): NormalizedFontFaceData[] {
export function extractFontFaceData (css: string, family?: string): NormalizedFontFaceData[] {
const fontFaces: NormalizedFontFaceData[] = []

for (const node of findAll(parse(css), node => node.type === 'Atrule' && node.name === 'font-face')) {
Expand All @@ -22,6 +22,16 @@ export function extractFontFaceData (css: string): NormalizedFontFaceData[] {
const data: Partial<NormalizedFontFaceData> = {}
for (const child of node.block?.children || []) {
if (child.type !== 'Declaration') { continue }
if (family && child.property === 'font-family') {
const value = extractCSSValue(child) as string | string[]
const slug = family.toLowerCase()
if (typeof value === 'string' && value.toLowerCase() !== slug) {
return []
}
if (Array.isArray(value) && value.length > 0 && value.every(v => v.toLowerCase() !== slug)) {
return []
}
}
if (child.property in extractableKeyMap) {
const value = extractCSSValue(child) as any
data[extractableKeyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value
Expand Down
7 changes: 6 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import local from './providers/local'
import google from './providers/google'
import bunny from './providers/bunny'
import fontshare from './providers/fontshare'
import adobe from './providers/adobe'

import { FontFamilyInjectionPlugin, type FontFaceResolution } from './plugins/transform'
import { generateFontFace } from './css/render'
Expand Down Expand Up @@ -85,8 +86,12 @@ export default defineNuxtModule<ModuleOptions>({
},
local: {},
google: {},
adobe: {
id: '',
},
providers: {
local,
adobe,
google,
bunny,
fontshare,
Expand Down Expand Up @@ -128,7 +133,7 @@ export default defineNuxtModule<ModuleOptions>({
if (options.providers?.[key] === false || (options.provider && options.provider !== key)) {
delete providers[key]
} else if (provider.setup) {
setups.push(provider.setup(options[key as 'google' | 'local'] || {}, nuxt))
setups.push(provider.setup(options[key as 'google' | 'local' | 'adobe'] || {}, nuxt))
}
}
await Promise.all(setups)
Expand Down
118 changes: 118 additions & 0 deletions src/providers/adobe.ts
Original file line number Diff line number Diff line change
@@ -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<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))
}
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 []
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export interface ModuleOptions {
google?: {}
/** Options passed directly to `local` font provider (none currently) */
local?: {}
/** Options passed directly to `adobe` font provider */
adobe?: {
id: string | string[]
}
/**
* An ordered list of providers to check when resolving font families.
*
Expand Down

0 comments on commit 12a51d3

Please sign in to comment.