Skip to content

Commit

Permalink
feat: add automatic font metric optimisation
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Feb 21, 2024
1 parent 89caf13 commit ac0888b
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 111 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Plug-and-play custom web font optimization and configuration for Nuxt apps.
- [ ] `fontsource`
- [x] custom providers for full control
- [x] local download support (until `nuxt/assets` lands)
- [ ] automatic font metric optimisation powered by https://github.com/unjs/fontaine
- [x] automatic font metric optimisation powered by [**fontaine**](https://github.com/unjs/fontaine) and [**capsize**](https://github.com/seek-oss/capsize)
- [ ] devtools integration
- [ ] (automatic?) font subsetting support
- [ ] documentation (module usage, custom provider creation)
Expand Down
8 changes: 7 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export default defineNuxtConfig({
{ name: 'Kode Mono', provider: 'none' },
{ name: 'MyCustom', src: '/font.woff2' },
{ name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
]
{ name: 'Oswald', fallbacks: ['Times New Roman'] },
],
defaults: {
fallbacks: {
monospace: ['Tahoma']
}
}
},
})
34 changes: 34 additions & 0 deletions playground/pages/fallbacks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div>
<div>
'Nunito'
</div>
<section>
'Lato', sans-serif
</section>
<main>
'Oswald', sans-serif
</main>
<span>
'Fredoka', monospace
</span>
</div>
</template>

<style scoped>
div {
font-family: 'Nunito';
}
section {
font-family: 'Lato', sans-serif;
}
main {
font-family: 'Oswald', sans-serif;
}
span {
font-family: 'Fredoka', monospace;
}
</style>
45 changes: 30 additions & 15 deletions src/css/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@ import { findAll, parse, 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',
}

export function extractFontFaceData (css: 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 }

const data: Partial<NormalizedFontFaceData> = {}
const keyMap: 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',
}
for (const child of node.block?.children || []) {
if (child.type !== 'Declaration') { continue }
if (child.property in keyMap) {
if (child.property in extractableKeyMap) {
const value = extractCSSValue(child) as any
data[keyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value
data[extractableKeyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value
}
}
fontFaces.push(data as NormalizedFontFaceData)
Expand Down Expand Up @@ -76,8 +77,8 @@ function extractCSSValue (node: Declaration) {


// https://developer.mozilla.org/en-US/docs/Web/CSS/font-family
const genericCSSFamilies = new Set([
/* A generic family name only */
/* A generic family name only */
const _genericCSSFamilies = [
'serif',
'sans-serif',
'monospace',
Expand All @@ -91,23 +92,37 @@ const genericCSSFamilies = new Set([
'emoji',
'math',
'fangsong',
] as const
export type GenericCSSFamily = typeof _genericCSSFamilies[number]
const genericCSSFamilies = new Set(_genericCSSFamilies)

/* Global values */
/* Global values */
const globalCSSValues = new Set([
'inherit',
'initial',
'revert',
'revert-layer',
'unset',
])

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

for (const child of node.value.children) {
if (child.type === 'Identifier' && genericCSSFamilies.has(child.name as GenericCSSFamily)) {
return child.name as GenericCSSFamily
}
}
}

export function extractFontFamilies (node: Declaration) {
if (node.value.type == 'Raw') {
return [node.value.value]
}

const families = [] as string[]
for (const child of node.value.children) {
if (child.type === 'Identifier' && !genericCSSFamilies.has(child.name)) {
if (child.type === 'Identifier' && !genericCSSFamilies.has(child.name as GenericCSSFamily) && !globalCSSValues.has(child.name)) {
families.push(child.name)
}
if (child.type === 'String') {
Expand Down
47 changes: 29 additions & 18 deletions src/css/render.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { hasProtocol } from 'ufo'
import type { FontSource, NormalizedFontFaceData } from '../types'
import type { FontSource, NormalizedFontFaceData, RemoteFontSource } from '../types'
import { extname } from 'pathe'
import { getMetricsForFamily, readMetrics, generateFontFace as generateFallbackFontFace } from 'fontaine'

export function generateFontFaces (family: string, sources: NormalizedFontFaceData[]) {
const declarations: string[] = []
for (const font of sources) {
declarations.push([
'@font-face {',
` font-family: '${family}';`,
` src: ${renderFontSrc(font.src)};`,
` font-display: ${font.display || 'swap'};`,
font.unicodeRange && ` unicode-range: ${font.unicodeRange};`,
font.weight && ` font-weight: ${font.weight};`,
font.style && ` font-style: ${font.style};`,
font.featureSettings && ` font-feature-settings: ${font.featureSettings};`,
font.variationSettings && ` font-variation-settings: ${font.variationSettings};`,
`}`
].filter(Boolean).join('\n'))
}
export function generateFontFace (family: string, font: NormalizedFontFaceData) {
return [
'@font-face {',
` font-family: '${family}';`,
` src: ${renderFontSrc(font.src)};`,
` font-display: ${font.display || 'swap'};`,
font.unicodeRange && ` unicode-range: ${font.unicodeRange};`,
font.weight && ` font-weight: ${font.weight};`,
font.style && ` font-style: ${font.style};`,
font.featureSettings && ` font-feature-settings: ${font.featureSettings};`,
font.variationSettings && ` font-variation-settings: ${font.variationSettings};`,
`}`
].filter(Boolean).join('\n')
}

export async function generateFontFallbacks (family: string, data: NormalizedFontFaceData, fallbacks?: Array<{ name: string, font: string }>) {
if (!fallbacks?.length) return []

return declarations
const fontURL = data.src!.find(s => 'url' in s) as RemoteFontSource | undefined
const metrics = await getMetricsForFamily(family) || fontURL && await readMetrics(fontURL.url)

if (!metrics) return []

const css: string[] = []
for (const fallback of fallbacks) {
css.push(generateFallbackFontFace(metrics, fallback))
}
return css
}

const formatMap: Record<string, string> = {
Expand Down
Loading

0 comments on commit ac0888b

Please sign in to comment.