Skip to content

Commit

Permalink
fix(local): refactor scanning/lookup mechanism
Browse files Browse the repository at this point in the history
resolves #22
  • Loading branch information
danielroe committed Mar 7, 2024
1 parent a44c4c4 commit bc7c12a
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 50 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"globby": "^14.0.1",
"h3": "^1.11.1",
"jiti": "^1.21.0",
"magic-regexp": "^0.8.0",
"magic-string": "^0.30.8",
"ofetch": "^1.3.3",
"ohash": "^1.1.3",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

116 changes: 66 additions & 50 deletions src/providers/local.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { globby } from 'globby'
import { join, relative, resolve } from 'pathe'
import { filename } from 'pathe/utils'
import { anyOf, createRegExp, not, wordBoundary } from 'magic-regexp'

import type { FontFaceData, FontProvider } from '../types'
import { withLeadingSlash, withTrailingSlash } from 'ufo'
Expand Down Expand Up @@ -42,31 +43,14 @@ export default {
resolveFontFaces (fontFamily, defaults) {
const fonts: FontFaceData[] = []

// Generate all possible permutations of font family names
// and resolve the first one that exists
// Resolve font files for each combination of weight, style and subset
for (const weight of defaults.weights) {
const isDefaultWeight = weight === 'normal' || weight === 400
for (const style of defaults.styles) {
const isDefaultStyle = style === 'normal'
for (const subset of defaults.subsets) {
const isDefaultSubset = subset === 'latin'
const options = [
[weight, style, subset],
[weight, subset, style],
[style, weight, subset],
[style, subset, weight],
[subset, weight, style],
[subset, style, weight],
...isDefaultWeight ? [[style, subset], [subset, style]] : [],
...isDefaultStyle ? [[weight, subset], [subset, weight]] : [],
...isDefaultSubset ? [[weight, style], [style, weight]] : [],
...(isDefaultStyle && isDefaultWeight) ? [[subset]] : [],
...(isDefaultStyle && isDefaultWeight && isDefaultSubset) ? [[]] : []
]
const resolved = findFirst([fontFamily, fontFamily.replace(NON_WORD_RE, '-'), fontFamily.replace(NON_WORD_RE, '')], options)
if (resolved) {
const resolved = lookupFont(fontFamily, [weightMap[weight] || weight, style, subset])
if (resolved.length > 0) {
fonts.push({
src: [...new Set(resolved)],
src: resolved,
weight,
style,
})
Expand All @@ -88,30 +72,58 @@ const NON_WORD_RE = /[^\w\d]+/g

export const isFontFile = (id: string) => FONT_RE.test(id)

function findFirst (families: string[], options: Array<string | number>[]) {
for (const family of families) {
for (const option of options) {
const resolved = lookupFont([family, ...option].join('-')) || lookupFont([family, ...option].join(''))
if (resolved) {
return resolved
}
}
}
const weightMap: Record<string, string> = {
'100': 'thin',
'200': 'extra-light',
'300': 'light',
'400': 'normal',
'500': 'medium',
'600': 'semi-bold',
'700': 'bold',
'800': 'extra-bold',
'900': 'black',
}

const weights = Object.entries(weightMap).flatMap(e => e).filter(r => r !== 'normal')
const WEIGHT_RE = createRegExp(anyOf(...weights).groupedAs('weight').after(not.digit).before(not.digit.or(wordBoundary)), ['i'])

const styles = ['italic', 'oblique'] as const
const STYLE_RE = createRegExp(anyOf(...styles).groupedAs('style').before(not.wordChar.or(wordBoundary)), ['i'])

const subsets = [
'cyrillic-ext',
'cyrillic',
'greek-ext',
'greek',
'vietnamese',
'latin-ext',
'latin',
] as const
const SUBSET_RE = createRegExp(anyOf(...subsets).groupedAs('subset').before(not.wordChar.or(wordBoundary)), ['i'])

function generateSlugs (path: string) {
const name = filename(path)
return [...new Set([
name.toLowerCase(),
// Barlow-das324jasdf => barlow
name.replace(/-[\w\d]+$/, '').toLowerCase(),
// Barlow.das324jasdf => barlow
name.replace(/\.[\w\d]+$/, '').toLowerCase(),
// Open+Sans => open-sans
name.replace(NON_WORD_RE, '-').toLowerCase(),
// Open+Sans => opensans
name.replace(NON_WORD_RE, '').toLowerCase(),
])]
let name = filename(path)

const weight = name.match(WEIGHT_RE)?.groups?.weight || 'normal'
const style = name.match(STYLE_RE)?.groups?.style || 'normal'
const subset = name.match(SUBSET_RE)?.groups?.subset || 'latin'

for (const slug of [weight, style, subset]) {
name = name.replace(slug, '')
}

const slugs = new Set<string>()

for (const slug of [name.replace(/[.][\w\d]*$/, ''), name.replace(/[._-][\w\d]*$/, '')]) {
slugs.add([
fontFamilyToSlug(slug.replace(/[\W._-]+$/, '')),
weightMap[weight] || weight,
style,
subset
].join('-').toLowerCase())
}

return [...slugs]
}

function registerFont (path: string) {
Expand All @@ -130,19 +142,23 @@ function unregisterFont (path: string) {
}
}

function lookupFont (family: string): string[] | undefined {
const priority = ['woff2', 'woff', 'ttf', 'otf', 'eot']
const slug = fontFamilyToSlug(family)
const scannedFiles = providerContext.registry[slug]?.map(path => {
const extensionPriority = ['woff2', 'woff', 'ttf', 'otf', 'eot']
function lookupFont (family: string, suffixes: Array<string | number>): string[] {
const slug = [fontFamilyToSlug(family), ...suffixes].join('-')
const paths = providerContext.registry[slug]
if (!paths || paths.length === 0) { return [] }

const fonts = new Set<string>()
for (const path of paths) {
const base = providerContext.rootPaths.find(root => path.startsWith(root))
return base ? withLeadingSlash(relative(base, path)) : path
})
fonts.add(base ? withLeadingSlash(relative(base, path)) : path)
}

return scannedFiles?.sort((a, b) => {
return [...fonts].sort((a, b) => {
const extA = filename(a).split('.').pop()!
const extB = filename(b).split('.').pop()!

return priority.indexOf(extA) - priority.indexOf(extB)
return extensionPriority.indexOf(extA) - extensionPriority.indexOf(extB)
})
}

Expand Down
67 changes: 67 additions & 0 deletions test/providers/local.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,73 @@ describe('local font provider', () => {
`)
await cleanup()
})

it('should resolve correct font weights, subsets and styles', async () => {
const cleanup = await createFixture('resolve-weights', [
'public/MyFont.woff',
'public/MyFont-normal.woff2',
'public/MyFont_bold.woff2',
'public/MyFont.700.eot',
'public/MyFont.600-234987akd.woff2',
'public/My-Font.200.woff2',
'public/MyFontbold-latin.ttf',
'public/MyFontbold-latin.woff',
])
const provider = await setupFixture(['resolve-weights'])
expect(provider.resolveFontFaces('MyFont', {
fallbacks: [],
weights: ['normal'],
styles: ['normal'],
subsets: ['latin']
})?.fonts).toMatchInlineSnapshot(`
[
{
"src": [
"/MyFont-normal.woff2",
"/MyFont.woff",
],
"style": "normal",
"weight": "normal",
},
]
`)
expect(provider.resolveFontFaces('MyFont', {
fallbacks: [],
weights: ['bold'],
styles: ['normal'],
subsets: ['latin']
})?.fonts).toMatchInlineSnapshot(`
[
{
"src": [
"/MyFont.700.eot",
"/MyFont_bold.woff2",
"/MyFontbold-latin.ttf",
"/MyFontbold-latin.woff",
],
"style": "normal",
"weight": "bold",
},
]
`)
expect(provider.resolveFontFaces('MyFont', {
fallbacks: [],
weights: ['extra-light'],
styles: ['normal'],
subsets: ['latin']
})?.fonts).toMatchInlineSnapshot(`
[
{
"src": [
"/My-Font.200.woff2",
],
"style": "normal",
"weight": "extra-light",
},
]
`)
await cleanup()
})
})

/** test utilities */
Expand Down

0 comments on commit bc7c12a

Please sign in to comment.