Skip to content

Commit

Permalink
fix: prepend local font sources
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Feb 21, 2024
1 parent 0493d36 commit 70fc384
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 13 deletions.
40 changes: 38 additions & 2 deletions src/css/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { findAll, parse, type Declaration } from 'css-tree'

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

export function extractFontFaceData (css: string): NormalizedFontFaceData[] {
const fontFaces: NormalizedFontFaceData[] = []
Expand All @@ -21,13 +22,14 @@ export function extractFontFaceData (css: string): NormalizedFontFaceData[] {
for (const child of node.block?.children || []) {
if (child.type !== 'Declaration') { continue }
if (child.property in keyMap) {
data[keyMap[child.property]!] = extractCSSValue(child) as any
const value = extractCSSValue(child) as any
data[keyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value
}
}
fontFaces.push(data as NormalizedFontFaceData)
}

return fontFaces
return mergeFontSources(fontFaces)
}

function extractCSSValue (node: Declaration) {
Expand Down Expand Up @@ -115,3 +117,37 @@ 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[]) {
for (const face of data) {
if (face.src[0] && !('name' in face.src[0])) {
face.src.unshift({ name: fontFamily })
}
}
return data
}
5 changes: 3 additions & 2 deletions src/css/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ export function generateFontFaces (family: string, sources: NormalizedFontFaceDa
}

const formatMap: Record<string, string> = {
otf: 'opentype',
woff: 'woff',
woff2: 'woff2',
woff: 'woff',
otf: 'opentype',
ttf: 'truetype',
eot: 'embedded-opentype',
svg: 'svg',
}
export const formatPriorityList = Object.values(formatMap)
const extensionMap = Object.fromEntries(Object.entries(formatMap).map(([key, value]) => [value, key]))
export const formatToExtension = (format?: string) => format && format in extensionMap ? '.' + extensionMap[format] : undefined

Expand Down
7 changes: 4 additions & 3 deletions src/providers/bunny.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $fetch } from 'ofetch'
import type { FontProvider, ResolveFontFacesOptions } from '../types'
import { extractFontFaceData } from '../css/parse'
import { extractFontFaceData, addLocalFallbacks } from '../css/parse'

export default {
async setup () {
Expand Down Expand Up @@ -49,7 +49,7 @@ function isBunnyFont (family: string) {
async function getFontDetails (family: string, variants: ResolveFontFacesOptions) {
const id = familyMap.get(family) as keyof typeof fonts
const font = fonts[id]!
const weights = variants.weights?.filter(weight => font.weights.includes(Number(weight))) || font.weights
const weights = variants.weights.filter(weight => font.weights.includes(Number(weight)))
const styleMap = {
italic: 'i',
oblique: 'i',
Expand All @@ -64,5 +64,6 @@ async function getFontDetails (family: string, variants: ResolveFontFacesOptions
}
})

return extractFontFaceData(css)
// TODO: support subsets
return addLocalFallbacks(family, extractFontFaceData(css))
}
12 changes: 6 additions & 6 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('providers', async () => {
[
"@font-face {
font-family: 'CustomFont';
src: url("/file.woff2") format(woff2);
src: url("/custom-font.woff2") format(woff2);
font-display: swap;
font-weight: 400;
font-style: normal;
Expand All @@ -29,7 +29,7 @@ describe('providers', async () => {
[
"@font-face {
font-family: 'Abel';
src: url("/file.woff2") format(woff2), url("/file.woff") format(woff);
src: local("Abel"), url("/_fonts/file.woff2") format(woff2), url("/_fonts/file.woff") format(woff);
font-display: swap;
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
font-weight: 400;
Expand Down Expand Up @@ -76,7 +76,7 @@ describe('providers', async () => {
[
"@font-face {
font-family: 'SomeFontFromCustomProvider';
src: url("/file.woff2") format(woff2);
src: url("/some-font.woff2") format(woff2);
font-display: swap;
}",
]
Expand All @@ -91,7 +91,7 @@ describe('features', () => {
[
"@font-face {
font-family: 'MyCustom';
src: url("/file.woff2") format(woff2);
src: url("/font.woff2") format(woff2);
font-display: swap;
}",
]
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('features', () => {
function extractFontFaces (fontFamily: string, html: string) {
const matches = html.matchAll(new RegExp(`@font-face\\s*{[^}]*font-family:\\s*['"]?${fontFamily}['"]?[^}]+}`, 'g'))
return Array.from(matches, (match) => match[0]
.replace(/"(https?:\/\/[^/]+)?\/[^"]+(\.[^".]+)"/g, '"$1/file$2"')
.replace(/"(https?:\/\/[^/]+)?\/[^".]+"/g, '"$1/file"')
.replace(/"(https?:\/\/[^/]+|\/_fonts)\/[^"]+(\.[^".]+)"/g, '"$1/file$2"')
.replace(/"(https?:\/\/[^/]+|\/_fonts)\/[^".]+"/g, '"$1/file"')
)
}

0 comments on commit 70fc384

Please sign in to comment.