/
transform.ts
125 lines (109 loc) · 3.58 KB
/
transform.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { createUnplugin } from 'unplugin'
import {
createRegExp,
exactly,
charIn,
charNotIn,
whitespace,
} from 'magic-regexp'
import MagicString from 'magic-string'
import { generateFontFace, parseFontFace, generateOverrideName } from './css'
import { getMetricsForFamily, readMetrics } from './metrics'
import { parseURL } from 'ufo'
import { isAbsolute, join } from 'pathe'
interface FontaineTransformOptions {
css?: { value?: string }
fallbacks: string[]
resolvePath?: (path: string) => string | URL
sourcemap?: boolean
}
export const FontaineTransform = createUnplugin(
(options: FontaineTransformOptions) => {
const cssContext = (options.css = options.css || {})
cssContext.value = ''
options.resolvePath = options.resolvePath || (id => id)
function readMetricsFromId(path: string, importer: string) {
const resolvedPath =
isAbsolute(importer) && path.startsWith('.')
? join(importer, path)
: options.resolvePath!(path)
return readMetrics(resolvedPath)
}
return {
name: 'fontaine-transform',
enforce: 'pre',
transformInclude(id) {
const { pathname } = parseURL(id)
return pathname.endsWith('.css') || id.endsWith('.css')
},
async transform(code, id) {
const s = new MagicString(code)
const faceRanges: [start: number, end: number][] = []
for (const match of code.matchAll(FONT_FACE_RE)) {
const matchContent = match[0]
if (match.index === undefined || !matchContent) continue
faceRanges.push([match.index, match.index + matchContent.length])
const { family, source } = parseFontFace(matchContent)
if (!family) continue
const metrics =
(await getMetricsForFamily(family)) ||
(source && (await readMetricsFromId(source, id).catch(() => null)))
if (metrics) {
const fontFace = generateFontFace(metrics, {
name: generateOverrideName(family),
fallbacks: options.fallbacks,
})
cssContext.value += fontFace
s.appendLeft(match.index, fontFace)
}
}
for (const match of code.matchAll(FONT_FAMILY_RE)) {
const { index, 0: matchContent } = match
if (index === undefined || !matchContent) continue
// Skip font-family definitions _within_ @font-face blocks
if (faceRanges.some(([start, end]) => index > start && index < end))
continue
const families = matchContent
.split(',')
.map(f => f.trim())
.filter(f => !f.startsWith('var('))
if (!families.length) continue
s.overwrite(
index,
index + matchContent.length,
' ' +
[
families[0],
`"${generateOverrideName(families[0])}"`,
...families.slice(1),
].join(', ')
)
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap
? s.generateMap({ source: id, includeContent: true })
: undefined,
}
}
},
}
}
)
const FONT_FACE_RE = createRegExp(
exactly('@font-face')
.and(whitespace.times.any())
.and('{')
.and(charNotIn('}').times.any())
.and('}'),
['g']
)
const FONT_FAMILY_RE = createRegExp(
charNotIn(';}')
.times.any()
.as('family')
.after(exactly('font-family:').and(whitespace.times.any()))
.before(charIn(';}').or(exactly('').at.lineEnd())),
['g']
)