|
| 1 | +import { hasProtocol } from 'ufo' |
| 2 | +import { extname, relative } from 'pathe' |
| 3 | +import { getMetricsForFamily, generateFontFace as generateFallbackFontFace, readMetrics } from 'fontaine' |
| 4 | +import type { RemoteFontSource, FontFaceData } from 'unifont' |
| 5 | +import type { FontSource } from '../types' |
| 6 | + |
| 7 | +export function generateFontFace(family: string, font: FontFaceData) { |
| 8 | + return [ |
| 9 | + '@font-face {', |
| 10 | + ` font-family: '${family}';`, |
| 11 | + ` src: ${renderFontSrc(font.src)};`, |
| 12 | + ` font-display: ${font.display || 'swap'};`, |
| 13 | + font.unicodeRange && ` unicode-range: ${font.unicodeRange};`, |
| 14 | + font.weight && ` font-weight: ${Array.isArray(font.weight) ? font.weight.join(' ') : font.weight};`, |
| 15 | + font.style && ` font-style: ${font.style};`, |
| 16 | + font.stretch && ` font-stretch: ${font.stretch};`, |
| 17 | + font.featureSettings && ` font-feature-settings: ${font.featureSettings};`, |
| 18 | + font.variationSettings && ` font-variation-settings: ${font.variationSettings};`, |
| 19 | + `}`, |
| 20 | + ].filter(Boolean).join('\n') |
| 21 | +} |
| 22 | + |
| 23 | +export async function generateFontFallbacks(family: string, data: FontFaceData, fallbacks?: Array<{ name: string, font: string }>) { |
| 24 | + if (!fallbacks?.length) return [] |
| 25 | + |
| 26 | + const fontURL = data.src!.find(s => 'url' in s) as RemoteFontSource | undefined |
| 27 | + const metrics = await getMetricsForFamily(family) || (fontURL && await readMetrics(fontURL.originalURL || fontURL.url)) |
| 28 | + |
| 29 | + if (!metrics) return [] |
| 30 | + |
| 31 | + const css: string[] = [] |
| 32 | + for (const fallback of fallbacks) { |
| 33 | + css.push(generateFallbackFontFace(metrics, { |
| 34 | + ...fallback, |
| 35 | + metrics: await getMetricsForFamily(fallback.font) || undefined, |
| 36 | + })) |
| 37 | + } |
| 38 | + return css |
| 39 | +} |
| 40 | + |
| 41 | +const formatMap: Record<string, string> = { |
| 42 | + woff2: 'woff2', |
| 43 | + woff: 'woff', |
| 44 | + otf: 'opentype', |
| 45 | + ttf: 'truetype', |
| 46 | + eot: 'embedded-opentype', |
| 47 | + svg: 'svg', |
| 48 | +} |
| 49 | +const extensionMap = Object.fromEntries(Object.entries(formatMap).map(([key, value]) => [value, key])) |
| 50 | +export const formatToExtension = (format?: string) => format && format in extensionMap ? '.' + extensionMap[format] : undefined |
| 51 | + |
| 52 | +export function parseFont(font: string) { |
| 53 | + // render as `url("url/to/font") format("woff2")` |
| 54 | + if (font.startsWith('/') || hasProtocol(font)) { |
| 55 | + const extension = extname(font).slice(1) |
| 56 | + const format = formatMap[extension] |
| 57 | + |
| 58 | + return { |
| 59 | + url: font, |
| 60 | + format, |
| 61 | + } satisfies RemoteFontSource as RemoteFontSource |
| 62 | + } |
| 63 | + |
| 64 | + // render as `local("Font Name")` |
| 65 | + return { name: font } |
| 66 | +} |
| 67 | + |
| 68 | +function renderFontSrc(sources: Exclude<FontSource, string>[]) { |
| 69 | + return sources.map((src) => { |
| 70 | + if ('url' in src) { |
| 71 | + let rendered = `url("${src.url}")` |
| 72 | + for (const key of ['format', 'tech'] as const) { |
| 73 | + if (key in src) { |
| 74 | + rendered += ` ${key}(${src[key]})` |
| 75 | + } |
| 76 | + } |
| 77 | + return rendered |
| 78 | + } |
| 79 | + return `local("${src.name}")` |
| 80 | + }).join(', ') |
| 81 | +} |
| 82 | + |
| 83 | +export function relativiseFontSources(font: FontFaceData, relativeTo: string) { |
| 84 | + return { |
| 85 | + ...font, |
| 86 | + src: font.src.map((source) => { |
| 87 | + if ('name' in source) return source |
| 88 | + if (!source.url.startsWith('/')) return source |
| 89 | + return { |
| 90 | + ...source, |
| 91 | + url: relative(relativeTo, source.url), |
| 92 | + } |
| 93 | + }), |
| 94 | + } satisfies FontFaceData |
| 95 | +} |
0 commit comments