8000 refactor!: migrate to `unifont` by danielroe Β· Pull Request #315 Β· nuxt/fonts Β· GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

refactor!: migrate to unifont #315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'

import type { ClientFunctions, ServerFunctions, ManualFontDetails, ProviderFontDetails } from '../src/devtools'
import { DEVTOOLS_RPC_NAMESPACE } from '../src/constants'
import type { NormalizedFontFaceData } from '../src/types'
import type { FontFaceData } from '../src/types'

type AnnotatedFont = (ManualFontDetails | ProviderFontDetails) & {
css?: string
Expand Down Expand Up @@ -42,7 +42,7 @@ function removeDuplicates<T extends ManualFontDetails | ProviderFontDetails>(arr
return array.filter((item, index) => index === array.findIndex(other => JSON.stringify(other) === JSON.stringify(item)))
}

function prettyURL(font: NormalizedFontFaceData) {
function prettyURL(font: FontFaceData) {
const firstRemoteSource = font.src.find(i => 'url' in i)
if (firstRemoteSource) {
return firstRemoteSource.originalURL || firstRemoteSource.url
Expand Down
56 changes: 29 additions & 27 deletions docs/content/1.get-started/4.providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,29 +64,29 @@ You should read [their terms in full](https://www.adobe.com/legal/terms.html) be

## Custom Providers

The provider API is likely to evolve in the next few releases of Nuxt Fonts, but at the moment it looks like this:
The core of the provider API in Nuxt Fonts has been extracted to a common library that can be used by any framework - [`unifont`](https://github.com/unjs/unifont).

The provider API is likely to evolve in the next few releases of `unifont`, but at the moment it looks like this:

```ts
import { defineFontProvider } from '@nuxt/fonts/utils'

export default defineFontProvider({
async setup () {
// do some setup
},
async resolveFontFaces (fontFamily, defaults) {
if (fontFamily === 'My Font Family') {
return {
fonts: [
{
src: [
{ url: 'https://cdn.org/my-font.woff2', format: 'woff2' },
// this will be inferred as a `woff` format file
'https://cdn.org/my-font.woff',
],
weight: 400,
style: 'normal',
}
]
import { defineFontProvider } from 'unifont'

export default defineFontProvider('some-custom-provider', async (options) => {
// do some setup
return {
async resolveFont (fontFamily, options) {
if (fontFamily === 'My Font Family') {
return {
fonts: [
{
src: [
{ url: 'https://cdn.org/my-font.woff2', format: 'woff2' },
],
weight: 400,
style: 'normal',
}
]
}
}
}
}
Expand All @@ -97,12 +97,14 @@ Module authors can also add their own providers (or remove existing ones) in the

```ts
nuxt.hook('fonts:providers', providers => {
providers.push({
async setup () {
/** some setup */
},
async resolveFontFaces (fontFamily, defaults) {
/** resolve font faces */
delete providers.adobe

providers['custom-provider'] = defineFontProvider('custom-provider', async () => {
/** some setup */
return {
async resolveFont (fontFamily, options) {
/** resolve font faces */
}
}
})
})
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"sirv": "^2.0.4",
"tinyglobby": "^0.2.9",
"ufo": "^1.5.4",
"unifont": "^0.1.0",
"unplugin": "^1.14.1",
"unstorage": "^1.12.0"
},
Expand Down
1 change: 1 addition & 0 deletions playground/providers/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
// Do some stuff
resolvableFonts.add('SomeFontFromCustomProvider')
},
// @ts-expect-error testing legacy API
async resolveFontFaces(fontFamily) {
if (!resolvableFonts.has(fontFamily)) {
return
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import { hash } from 'ohash'
import { storage } from './cache'
import { logger } from './logger'
import { formatToExtension, parseFont } from './css/render'
import type { FontFaceData, ModuleOptions, NormalizedFontFaceData } from './types'
import type { ModuleOptions, FontFaceData, RawFontFaceData } from './types'

// TODO: replace this with nuxt/assets when it is released
export function setupPublicAssetStrategy(options: ModuleOptions['assets'] = {}) {
const assetsBaseURL = options.prefix || '/_fonts'
const nuxt = useNuxt()
const renderedFontURLs = new Map<string, string>()

function normalizeFontData(faces: FontFaceData | FontFaceData[]): NormalizedFontFaceData[] {
const data: NormalizedFontFaceData[] = []
function normalizeFontData(faces: RawFontFaceData | FontFaceData[]): FontFaceData[] {
const data: FontFaceData[] = []
for (const face of Array.isArray(faces) ? faces : [faces]) {
data.push({
...face,
Expand Down
24 changes: 0 additions & 24 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,9 @@
import { createStorage } from 'unstorage'
import fsDriver from 'unstorage/drivers/fs'

import { version } from '../package.json'
import type { Awaitable } from './types'

export const cacheBase = 'node_modules/.cache/nuxt/fonts/meta'

// TODO: refactor to use nitro storage when possible
export const storage = createStorage({
driver: fsDriver({ base: cacheBase }),
})

export async function cachedData<T = unknown>(key: string, fetcher: () => Awaitable<T>, options?: {
onError?: (err: unknown) => Awaitable<T>
ttl?: number
}) {
const cached = await storage.getItem<null | { expires: number, version: string, data: T }>(key)
if (!cached || cached.version !== version || cached.expires < Date.now()) {
try {
const data = await fetcher()
await storage.setItem(key, { expires: Date.now() + (options?.ttl || 1000 * 60 * 60 * 24 * 7), version, data })
return data
}
catch (err) {
if (options?.onError) {
return options.onError(err)
}
throw err
}
}
return cached.data
}
139 changes: 3 additions & 136 deletions src/css/parse.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import { findAll, parse, type Declaration } from 'css-tree'
import type { Declaration } from 'css-tree'

import type { LocalFontSource, NormalizedFontFaceData, RemoteFontSource } from '../types'
import { formatPriorityList } from '../css/render'

const extractableKeyMap: Record<string, keyof NormalizedFontFaceData> = {
'src': 'src',
'font-display': 'display',
'font-weight': 'weight',
'font-style': 'style',
'font-feature-settings': 'featureSettings',
'font-variations-settings': 'variationSettings',
'unicode-range': 'unicodeRange',
}
import type { FontFaceData } from '../types'

const weightMap: Record<string, string> = {
100: 'Thin',
Expand All @@ -31,106 +20,10 @@ const styleMap: Record<string, string> = {
normal: '',
}

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')) {
if (node.type !== 'Atrule' || node.name !== 'font-face') {
continue
}

if (family) {
const isCorrectFontFace = node.block?.children.some((child) => {
if (child.type !== 'Declaration' || child.property !== 'font-family') {
return false
}

const value = extractCSSValue(child) as string | string[]
const slug = family.toLowerCase()
if (typeof value === 'string' && value.toLowerCase() === slug) {
return true
}
if (Array.isArray(value) && value.length > 0 && value.some(v => v.toLowerCase() === slug)) {
return true
}
return false
})

// Don't extract font face data from this `@font-face` rule if it doesn't match the specified family
if (!isCorrectFontFace) {
continue
}
}

const data: Partial<NormalizedFontFaceData> = {}
for (const child of node.block?.children || []) {
if (child.type === 'Declaration' && child.property in extractableKeyMap) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = extractCSSValue(child) as any
data[extractableKeyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value
}
}
fontFaces.push(data as NormalizedFontFaceData)
}

return mergeFontSources(fontFaces)
}

function processRawValue(v 10000 alue: string) {
return value.split(',').map(v => v.trim().replace(/^(?<quote>['"])(.*)\k<quote>$/, '$2'))
}

function extractCSSValue(node: Declaration) {
if (node.value.type == 'Raw') {
return processRawValue(node.value.value)
}

const values = [] as Array<string | number | RemoteFontSource | LocalFontSource>
let buffer = ''
for (const child of node.value.children) {
if (child.type === 'Function') {
if (child.name === 'local' && child.children.first?.type === 'String') {
values.push({ name: child.children.first.value })
}
if (child.name === 'format' && child.children.first?.type === 'String') {
(values.at(-1) as RemoteFontSource).format = child.children.first.value
}
if (child.name === 'tech' && child.children.first?.type === 'String') {
(values.at(-1) as RemoteFontSource).tech = child.children.first.value
}
}
if (child.type === 'Url') {
values.push({ url: child.value })
}
if (child.type === 'Identifier') {
buffer = buffer ? `${buffer} ${child.name}` : child.name
}
if (child.type === 'String') {
values.push(child.value)
}
if (child.type === 'Operator' && child.value === ',' && buffer) {
values.push(buffer)
buffer = ''
}
if (child.type === 'UnicodeRange') {
values.push(child.value)
}
if (child.type === 'Number') {
values.push(Number(child.value))
}
}

if (buffer) {
values.push(buffer)
}

if (values.length === 1) {
return values[0]
}

return values
}

// https://developer.mozilla.org/en-US/docs/Web/CSS/font-family
/* A generic family name only */
const _genericCSSFamilies = [
Expand Down Expand Up @@ -218,33 +111,7 @@ export function extractFontFamilies(node: Declaration) {
return families
}

function mergeFontSources(data: NormalizedFontFaceData[]) {
const mergedData: NormalizedFontFaceData[] = []
for (const face of data) {
const keys = Object.keys(face).filter(k => k !== 'src') as Array<keyof typeof face>
const existing = mergedData.find(f => (Object.keys(f).length === keys.length + 1) && keys.every(key => f[key]?.toString() === face[key]?.toString()))
if (existing) {
existing.src.push(...face.src)
}
else {
mergedData.push(face)
}
}

// Sort font sources by priority
for (const face of mergedData) {
face.src.sort((a, b) => {
// Prioritise local fonts (with 'name' property) over remote fonts, and then formats by formatPriorityList
const aIndex = 'format' in a ? formatPriorityList.indexOf(a.format || 'woff2') : -2
const bIndex = 'format' in b ? formatPriorityList.indexOf(b.format || 'woff2') : -2
return aIndex - bIndex
})
}

return mergedData
}

export function addLocalFallbacks(fontFamily: string, data: NormalizedFontFaceData[]) {
export function addLocalFallbacks(fontFamily: string, data: FontFaceData[]) {
for (const face of data) {
const style = (face.style ? styleMap[face.style] : '') ?? ''

Expand Down
Loading
0