diff --git a/README.md b/README.md index 93c6250..6d0848f 100644 --- a/README.md +++ b/README.md @@ -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: ['', ''], + }, + } +}) +``` + ### 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: diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index bb50a52..d650bba 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -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'] diff --git a/playground/pages/providers/adobe.vue b/playground/pages/providers/adobe.vue new file mode 100644 index 0000000..1b89078 --- /dev/null +++ b/playground/pages/providers/adobe.vue @@ -0,0 +1,11 @@ + + + diff --git a/playground/pages/providers/adobe2.vue b/playground/pages/providers/adobe2.vue new file mode 100644 index 0000000..b54ae4a --- /dev/null +++ b/playground/pages/providers/adobe2.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/css/parse.ts b/src/css/parse.ts index e627acc..8b31e1c 100644 --- a/src/css/parse.ts +++ b/src/css/parse.ts @@ -13,7 +13,7 @@ const extractableKeyMap: Record = { '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')) { @@ -22,6 +22,16 @@ export function extractFontFaceData (css: string): NormalizedFontFaceData[] { const data: Partial = {} 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 diff --git a/src/module.ts b/src/module.ts index 9c4055b..e661b6a 100644 --- a/src/module.ts +++ b/src/module.ts @@ -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' @@ -85,8 +86,12 @@ export default defineNuxtModule({ }, local: {}, google: {}, + adobe: { + id: '', + }, providers: { local, + adobe, google, bunny, fontshare, @@ -128,7 +133,7 @@ export default defineNuxtModule({ 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) 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 [] +} diff --git a/src/types.ts b/src/types.ts index 3f5881e..6e6ae0a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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. *