fontaine 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/dist/capsize-font-metrics.mjs +1 -1
- package/dist/fallbacks-Cj4LUWhi.d.cts +34 -0
- package/dist/fallbacks-Cj4LUWhi.d.mts +34 -0
- package/dist/index.cjs +138 -299
- package/dist/index.d.cts +65 -98
- package/dist/index.d.mts +64 -98
- package/dist/index.mjs +120 -277
- package/dist/metrics-ChAPPk2j.cjs +425 -0
- package/dist/metrics-DgPzfV2U.mjs +354 -0
- package/dist/postcss.cjs +91 -0
- package/dist/postcss.d.cts +52 -0
- package/dist/postcss.d.mts +52 -0
- package/dist/postcss.mjs +91 -0
- package/package.json +34 -15
package/dist/index.mjs
CHANGED
|
@@ -1,283 +1,92 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { a as resolveCategoryFallbacks, c as generateFontFace, d as withoutQueryOrFragment, f as withoutQuotes, i as DEFAULT_CATEGORY_FALLBACKS, l as genericCSSFamilies, n as readMetrics, o as cssWideKeywords, r as readMetricsForSource, s as generateFallbackName, t as getMetricsForFamily, u as parseFontFace } from "./metrics-DgPzfV2U.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { parse, walk } from "css-tree";
|
|
4
|
+
import { anyOf, char, createRegExp, exactly, oneOrMore } from "magic-regexp";
|
|
5
|
+
import { isAbsolute } from "pathe";
|
|
3
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import { fromFile } from "@capsizecss/unpack/fs";
|
|
6
|
-
import { parseURL } from "ufo";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
7
8
|
import MagicString from "magic-string";
|
|
8
|
-
import { isAbsolute } from "pathe";
|
|
9
9
|
import { createUnplugin } from "unplugin";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
|
|
20
|
-
const genericCSSFamilies = new Set([
|
|
21
|
-
"serif",
|
|
22
|
-
"sans-serif",
|
|
23
|
-
"monospace",
|
|
24
|
-
"cursive",
|
|
25
|
-
"fantasy",
|
|
26
|
-
"system-ui",
|
|
27
|
-
"ui-serif",
|
|
28
|
-
"ui-sans-serif",
|
|
29
|
-
"ui-monospace",
|
|
30
|
-
"ui-rounded",
|
|
31
|
-
"emoji",
|
|
32
|
-
"math",
|
|
33
|
-
"fangsong"
|
|
34
|
-
]);
|
|
35
|
-
const fontProperties = new Set([
|
|
36
|
-
"font-weight",
|
|
37
|
-
"font-style",
|
|
38
|
-
"font-stretch"
|
|
39
|
-
]);
|
|
10
|
+
//#region src/transform.ts
|
|
11
|
+
const supportedExtensions = [
|
|
12
|
+
"woff2",
|
|
13
|
+
"woff",
|
|
14
|
+
"ttf"
|
|
15
|
+
];
|
|
16
|
+
const CSS_RE = createRegExp(exactly(".").and(anyOf("sass", "css", "scss")).and(exactly("?").and(oneOrMore(char)).optionally()).at.lineEnd());
|
|
17
|
+
const RELATIVE_RE = createRegExp(exactly(".").or("..").and(anyOf("/", "\\")).at.lineStart());
|
|
18
|
+
const SCSS_RE = createRegExp(exactly(".").and(anyOf("sass", "scss")).and(exactly("?").and(oneOrMore(char)).optionally()).at.lineEnd());
|
|
40
19
|
/**
|
|
41
|
-
*
|
|
20
|
+
* Replaces `//` line comments with spaces, preserving offsets.
|
|
42
21
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
22
|
+
* css-tree has no SCSS mode and bails on the first `//` comment, parsing the
|
|
23
|
+
* remainder of the file as a single `Raw` node, which hides every at-rule that
|
|
24
|
+
* follows it.
|
|
45
25
|
*/
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
function blankLineComments(code) {
|
|
27
|
+
let result = "";
|
|
28
|
+
let index = 0;
|
|
29
|
+
while (index < code.length) {
|
|
30
|
+
const char = code[index];
|
|
31
|
+
if (char === "\"" || char === "'") {
|
|
32
|
+
const end = code.indexOf(char, index + 1);
|
|
33
|
+
const stop = end === -1 ? code.length : end + 1;
|
|
34
|
+
result += code.slice(index, stop);
|
|
35
|
+
index = stop;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === "/" && code[index + 1] === "*") {
|
|
39
|
+
const end = code.indexOf("*/", index + 2);
|
|
40
|
+
const stop = end === -1 ? code.length : end + 2;
|
|
41
|
+
result += code.slice(index, stop);
|
|
42
|
+
index = stop;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if ((char === "u" || char === "U") && /^url\(/i.test(code.slice(index, index + 4))) {
|
|
46
|
+
const end = code.indexOf(")", index);
|
|
47
|
+
const stop = end === -1 ? code.length : end + 1;
|
|
48
|
+
result += code.slice(index, stop);
|
|
49
|
+
index = stop;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (char === "/" && code[index + 1] === "/") {
|
|
53
|
+
const newline = code.indexOf("\n", index);
|
|
54
|
+
const stop = newline === -1 ? code.length : newline;
|
|
55
|
+
result += " ".repeat(stop - index);
|
|
56
|
+
index = stop;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
result += char;
|
|
60
|
+
index++;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function extractCssImports(ast) {
|
|
65
|
+
const imports = [];
|
|
66
|
+
walk(ast, {
|
|
49
67
|
visit: "Atrule",
|
|
50
68
|
enter(node) {
|
|
51
|
-
if (node.name !== "
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (child.type === "String") {
|
|
60
|
-
family = withoutQuotes(child.value);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
if (child.type === "Identifier" && !genericCSSFamilies.has(child.name)) {
|
|
64
|
-
family = child.name;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (fontProperties.has(declaration.property)) {
|
|
69
|
-
if (declaration.value.type === "Value") for (const child of declaration.value.children) {
|
|
70
|
-
const hasValue = !!properties[declaration.property];
|
|
71
|
-
properties[declaration.property] ||= "";
|
|
72
|
-
properties[declaration.property] += (hasValue ? " " : "") + generate(child);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (declaration.property === "src") walk(declaration.value, {
|
|
76
|
-
visit: "Url",
|
|
77
|
-
enter(urlNode) {
|
|
78
|
-
const source = withoutQuotes(urlNode.value);
|
|
79
|
-
if (source) sources.push(source);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
69
|
+
if (node.name !== "import" || node.prelude?.type !== "AtrulePrelude") return;
|
|
70
|
+
let specifier;
|
|
71
|
+
let conditional = false;
|
|
72
|
+
for (const child of node.prelude.children) if (child.type === "String" || child.type === "Url") specifier ||= withoutQuotes(child.value);
|
|
73
|
+
else if (child.type === "MediaQueryList" || child.type === "Function" && child.name.toLowerCase() === "supports") conditional = true;
|
|
74
|
+
if (specifier && !conditional && !specifier.startsWith("http:") && !specifier.startsWith("https:") && !specifier.startsWith("data:")) imports.push({
|
|
75
|
+
specifier: specifier.replace(/[?#].*$/, ""),
|
|
76
|
+
end: node.loc.end.offset
|
|
83
77
|
});
|
|
84
|
-
if (family) {
|
|
85
|
-
for (const source of sources) families.push({
|
|
86
|
-
index: node.loc.start.offset,
|
|
87
|
-
family,
|
|
88
|
-
source,
|
|
89
|
-
properties
|
|
90
|
-
});
|
|
91
|
-
if (!sources.length) families.push({
|
|
92
|
-
index: node.loc.start.offset,
|
|
93
|
-
family,
|
|
94
|
-
properties
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
78
|
}
|
|
98
79
|
});
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Generates a fallback name based on the first font family specified in the input string.
|
|
103
|
-
* @param {string} name - The full font family string.
|
|
104
|
-
* @returns {string} - The fallback font name.
|
|
105
|
-
*/
|
|
106
|
-
function generateFallbackName(name) {
|
|
107
|
-
return `${withoutQuotes(name.split(",").shift())} fallback`;
|
|
80
|
+
return imports;
|
|
108
81
|
}
|
|
109
|
-
|
|
110
|
-
* Generates a CSS `@font-face' declaration for a font, taking fallback and resizing into account.
|
|
111
|
-
* @param {FontFaceMetrics} metrics - The metrics of the preferred font. See {@link FontFaceMetrics}.
|
|
112
|
-
* @param {FallbackOptions} fallback - The fallback options, including name, font and optional metrics. See {@link FallbackOptions}.
|
|
113
|
-
* @returns {string} - The full `@font-face` CSS declaration.
|
|
114
|
-
*/
|
|
115
|
-
function generateFontFace(metrics, fallback) {
|
|
116
|
-
const { name: fallbackName, font: fallbackFontName, metrics: fallbackMetrics, ...properties } = fallback;
|
|
117
|
-
const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm;
|
|
118
|
-
const fallbackFontXAvgRatio = fallbackMetrics ? fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm : 1;
|
|
119
|
-
const sizeAdjust = fallbackMetrics && preferredFontXAvgRatio && fallbackFontXAvgRatio ? preferredFontXAvgRatio / fallbackFontXAvgRatio : 1;
|
|
120
|
-
const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust;
|
|
121
|
-
const ascentOverride = metrics.ascent / adjustedEmSquare;
|
|
122
|
-
const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare;
|
|
123
|
-
const lineGapOverride = metrics.lineGap / adjustedEmSquare;
|
|
124
|
-
return `@font-face {\n${toCSS({
|
|
125
|
-
"font-family": JSON.stringify(fallbackName),
|
|
126
|
-
"src": `local(${JSON.stringify(fallbackFontName)})`,
|
|
127
|
-
"size-adjust": toPercentage(sizeAdjust),
|
|
128
|
-
"ascent-override": toPercentage(ascentOverride),
|
|
129
|
-
"descent-override": toPercentage(descentOverride),
|
|
130
|
-
"line-gap-override": toPercentage(lineGapOverride),
|
|
131
|
-
...properties
|
|
132
|
-
})}\n}\n`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/fallbacks.ts
|
|
137
|
-
/**
|
|
138
|
-
* Default fallback font stacks for each font category.
|
|
139
|
-
* These are system fonts that work across different platforms.
|
|
140
|
-
*/
|
|
141
|
-
const DEFAULT_CATEGORY_FALLBACKS = {
|
|
142
|
-
"sans-serif": [
|
|
143
|
-
"BlinkMacSystemFont",
|
|
144
|
-
"Segoe UI",
|
|
145
|
-
"Helvetica Neue",
|
|
146
|
-
"Arial",
|
|
147
|
-
"Noto Sans"
|
|
148
|
-
],
|
|
149
|
-
"serif": [
|
|
150
|
-
"Times New Roman",
|
|
151
|
-
"Georgia",
|
|
152
|
-
"Noto Serif"
|
|
153
|
-
],
|
|
154
|
-
"monospace": [
|
|
155
|
-
"Courier New",
|
|
156
|
-
"Roboto Mono",
|
|
157
|
-
"Noto Sans Mono"
|
|
158
|
-
],
|
|
159
|
-
"display": [
|
|
160
|
-
"BlinkMacSystemFont",
|
|
161
|
-
"Segoe UI",
|
|
162
|
-
"Helvetica Neue",
|
|
163
|
-
"Arial",
|
|
164
|
-
"Noto Sans"
|
|
165
|
-
],
|
|
166
|
-
"handwriting": [
|
|
167
|
-
"BlinkMacSystemFont",
|
|
168
|
-
"Segoe UI",
|
|
169
|
-
"Helvetica Neue",
|
|
170
|
-
"Arial",
|
|
171
|
-
"Noto Sans"
|
|
172
|
-
]
|
|
173
|
-
};
|
|
174
|
-
/**
|
|
175
|
-
* Resolves the appropriate fallback fonts for a given font family.
|
|
176
|
-
*
|
|
177
|
-
* Resolution order:
|
|
178
|
-
* 1. If fallbacks is an array, use it as a global override
|
|
179
|
-
* 2. If fallbacks is an object with the font family key, use that override
|
|
180
|
-
* 3. If metrics contain a category, use the category-based fallbacks
|
|
181
|
-
* 4. Default to sans-serif category fallbacks
|
|
182
|
-
*
|
|
183
|
-
* @param options - Configuration for fallback resolution
|
|
184
|
-
* @returns Array of fallback font family names
|
|
185
|
-
*/
|
|
186
|
-
function resolveCategoryFallbacks(options) {
|
|
187
|
-
const { fontFamily, fallbacks, metrics, categoryFallbacks } = options;
|
|
188
|
-
const mergedCategoryFallbacks = { ...DEFAULT_CATEGORY_FALLBACKS };
|
|
189
|
-
if (categoryFallbacks) for (const category in categoryFallbacks) {
|
|
190
|
-
const categoryKey = category;
|
|
191
|
-
const categoryFallbacksList = categoryFallbacks[categoryKey];
|
|
192
|
-
if (categoryFallbacksList) mergedCategoryFallbacks[categoryKey] = categoryFallbacksList;
|
|
193
|
-
}
|
|
194
|
-
if (Array.isArray(fallbacks)) return fallbacks;
|
|
195
|
-
const familyFallback = fallbacks[fontFamily];
|
|
196
|
-
if (familyFallback) return familyFallback;
|
|
197
|
-
if (metrics?.category) {
|
|
198
|
-
const categoryFallback = mergedCategoryFallbacks[metrics.category];
|
|
199
|
-
if (categoryFallback) return categoryFallback;
|
|
200
|
-
}
|
|
201
|
-
return mergedCategoryFallbacks["sans-serif"];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
//#endregion
|
|
205
|
-
//#region src/metrics.ts
|
|
206
|
-
const metricCache = {};
|
|
207
|
-
function filterRequiredMetrics(font) {
|
|
208
|
-
return {
|
|
209
|
-
ascent: font.ascent,
|
|
210
|
-
descent: font.descent,
|
|
211
|
-
lineGap: font.lineGap,
|
|
212
|
-
unitsPerEm: font.unitsPerEm,
|
|
213
|
-
xWidthAvg: font.xWidthAvg,
|
|
214
|
-
category: font.category
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Retrieves the font metrics for a given font family from the metrics collection. Uses caching to avoid redundant calculations.
|
|
219
|
-
* @param {string} family - The name of the font family for which metrics are requested.
|
|
220
|
-
* @returns {Promise<FontFaceMetrics | null>} - A promise that resolves with the filtered font metrics or null if not found. See {@link FontFaceMetrics}.
|
|
221
|
-
* @async
|
|
222
|
-
*/
|
|
223
|
-
async function getMetricsForFamily(family) {
|
|
224
|
-
family = withoutQuotes(family);
|
|
225
|
-
if (family in metricCache) return metricCache[family] ?? null;
|
|
82
|
+
function resolveCssImport(specifier, importer) {
|
|
226
83
|
try {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const metrics = entireMetricsCollection[name];
|
|
230
|
-
/* v8 ignore next 4 */
|
|
231
|
-
if (!("descent" in metrics)) {
|
|
232
|
-
metricCache[family] = null;
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
const filteredMetrics = filterRequiredMetrics(metrics);
|
|
236
|
-
metricCache[family] = filteredMetrics;
|
|
237
|
-
return filteredMetrics;
|
|
84
|
+
if (RELATIVE_RE.test(specifier) || isAbsolute(specifier)) return fileURLToPath(new URL(specifier, pathToFileURL(importer)));
|
|
85
|
+
return createRequire(importer).resolve(specifier.replace(/^~/, ""));
|
|
238
86
|
} catch {
|
|
239
|
-
|
|
240
|
-
return null;
|
|
87
|
+
return;
|
|
241
88
|
}
|
|
242
89
|
}
|
|
243
|
-
const urlRequestCache = /* @__PURE__ */ new Map();
|
|
244
|
-
/**
|
|
245
|
-
* Reads font metrics from a specified source URL or file path. This function supports both local files and remote URLs.
|
|
246
|
-
* It caches the results to optimise subsequent requests for the same source.
|
|
247
|
-
* @param {URL | string} _source - The source URL or local file path from which to read the font metrics.
|
|
248
|
-
* @returns {Promise<FontFaceMetrics | null>} - A promise that resolves to the filtered font metrics or null if the source cannot be processed.
|
|
249
|
-
* @async
|
|
250
|
-
*/
|
|
251
|
-
async function readMetrics(_source) {
|
|
252
|
-
const source = typeof _source !== "string" && "href" in _source ? _source.href : _source;
|
|
253
|
-
if (source in metricCache) return metricCache[source] ?? null;
|
|
254
|
-
const { protocol } = parseURL(source);
|
|
255
|
-
if (!protocol) return null;
|
|
256
|
-
let metrics;
|
|
257
|
-
if (protocol === "file:") metrics = await fromFile(fileURLToPath(source));
|
|
258
|
-
else if (urlRequestCache.has(source)) metrics = await urlRequestCache.get(source);
|
|
259
|
-
else {
|
|
260
|
-
const requestPromise = fromUrl(source);
|
|
261
|
-
urlRequestCache.set(source, requestPromise);
|
|
262
|
-
metrics = await requestPromise;
|
|
263
|
-
}
|
|
264
|
-
const filteredMetrics = filterRequiredMetrics(metrics);
|
|
265
|
-
metricCache[source] = filteredMetrics;
|
|
266
|
-
return filteredMetrics;
|
|
267
|
-
}
|
|
268
|
-
function fontFamilyToCamelCase(str) {
|
|
269
|
-
return str.split(/[\s|-]/).filter(Boolean).map((s, i) => `${s.charAt(0)[i > 0 ? "toUpperCase" : "toLowerCase"]()}${s.slice(1)}`).join("");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
//#endregion
|
|
273
|
-
//#region src/transform.ts
|
|
274
|
-
const supportedExtensions = [
|
|
275
|
-
"woff2",
|
|
276
|
-
"woff",
|
|
277
|
-
"ttf"
|
|
278
|
-
];
|
|
279
|
-
const CSS_RE = createRegExp(exactly(".").and(anyOf("sass", "css", "scss")).and(exactly("?").and(oneOrMore(char)).optionally()).at.lineEnd());
|
|
280
|
-
const RELATIVE_RE = createRegExp(exactly(".").or("..").and(anyOf("/", "\\")).at.lineStart());
|
|
281
90
|
/**
|
|
282
91
|
* Transforms CSS files to include font fallbacks.
|
|
283
92
|
*
|
|
@@ -290,9 +99,6 @@ const FontaineTransform = createUnplugin((options) => {
|
|
|
290
99
|
const resolvePath = options.resolvePath || ((id) => id);
|
|
291
100
|
const fallbackName = options.fallbackName || options.overrideName || generateFallbackName;
|
|
292
101
|
const skipFontFaceGeneration = options.skipFontFaceGeneration || (() => false);
|
|
293
|
-
function readMetricsFromId(path, importer) {
|
|
294
|
-
return readMetrics(isAbsolute(importer) && RELATIVE_RE.test(path) ? new URL(path, pathToFileURL(importer)) : resolvePath(path));
|
|
295
|
-
}
|
|
296
102
|
return {
|
|
297
103
|
name: "fontaine-transform",
|
|
298
104
|
enforce: "pre",
|
|
@@ -300,11 +106,46 @@ const FontaineTransform = createUnplugin((options) => {
|
|
|
300
106
|
filter: { id: [CSS_RE] },
|
|
301
107
|
async handler(code, id) {
|
|
302
108
|
const s = new MagicString(code);
|
|
303
|
-
const
|
|
304
|
-
|
|
109
|
+
const inserted = /* @__PURE__ */ new Set();
|
|
110
|
+
const ast = parse(SCSS_RE.test(id) ? blankLineComments(code) : code, { positions: true });
|
|
111
|
+
const fontFaces = parseFontFace(ast).map((face) => ({
|
|
112
|
+
...face,
|
|
113
|
+
importer: id,
|
|
114
|
+
prefix: ""
|
|
115
|
+
}));
|
|
116
|
+
if (isAbsolute(id)) {
|
|
117
|
+
const imports = extractCssImports(ast);
|
|
118
|
+
const insertionIndex = imports.length ? Math.max(...imports.map((i) => i.end)) : 0;
|
|
119
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
120
|
+
const queue = imports.map((i) => ({
|
|
121
|
+
specifier: i.specifier,
|
|
122
|
+
importer: id
|
|
123
|
+
}));
|
|
124
|
+
while (queue.length) {
|
|
125
|
+
const { specifier, importer } = queue.shift();
|
|
126
|
+
const resolved = resolveCssImport(specifier, importer);
|
|
127
|
+
if (!resolved || seen.has(resolved) || !CSS_RE.test(resolved)) continue;
|
|
128
|
+
seen.add(resolved);
|
|
129
|
+
const importedCss = await readFile(resolved, "utf-8").catch(() => null);
|
|
130
|
+
if (importedCss === null) continue;
|
|
131
|
+
const importedAst = parse(SCSS_RE.test(resolved) ? blankLineComments(importedCss) : importedCss, { positions: true });
|
|
132
|
+
for (const face of parseFontFace(importedAst)) fontFaces.push({
|
|
133
|
+
...face,
|
|
134
|
+
index: insertionIndex,
|
|
135
|
+
importer: resolved,
|
|
136
|
+
prefix: "\n"
|
|
137
|
+
});
|
|
138
|
+
for (const nested of extractCssImports(importedAst)) queue.push({
|
|
139
|
+
specifier: nested.specifier,
|
|
140
|
+
importer: resolved
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const { family, source: rawSource, index, properties, importer, prefix } of fontFaces) {
|
|
145
|
+
const source = rawSource && withoutQueryOrFragment(rawSource);
|
|
305
146
|
if (!supportedExtensions.some((e) => source?.endsWith(e))) continue;
|
|
306
147
|
if (skipFontFaceGeneration(fallbackName(family))) continue;
|
|
307
|
-
const metrics = await getMetricsForFamily(family) || source && await
|
|
148
|
+
const metrics = await getMetricsForFamily(family) || source && await readMetricsForSource(source, importer, resolvePath).catch(() => null);
|
|
308
149
|
/* v8 ignore next 2 */
|
|
309
150
|
if (!metrics) continue;
|
|
310
151
|
const familyFallbacks = resolveCategoryFallbacks({
|
|
@@ -323,8 +164,11 @@ const FontaineTransform = createUnplugin((options) => {
|
|
|
323
164
|
metrics: fallbackMetrics,
|
|
324
165
|
...properties
|
|
325
166
|
});
|
|
167
|
+
const key = `${index}:${fontFace}`;
|
|
168
|
+
if (inserted.has(key)) continue;
|
|
169
|
+
inserted.add(key);
|
|
326
170
|
cssContext.value += fontFace;
|
|
327
|
-
s.appendLeft(index, fontFace);
|
|
171
|
+
s.appendLeft(index, prefix + fontFace);
|
|
328
172
|
}
|
|
329
173
|
}
|
|
330
174
|
walk(ast, {
|
|
@@ -332,20 +176,20 @@ const FontaineTransform = createUnplugin((options) => {
|
|
|
332
176
|
enter(node) {
|
|
333
177
|
if (node.property !== "font-family") return;
|
|
334
178
|
if (this.atrule && this.atrule.name === "font-face") return;
|
|
335
|
-
if (node.value.type !== "Value")
|
|
336
|
-
/* v8 ignore next */ return;
|
|
179
|
+
if (node.value.type !== "Value") return;
|
|
337
180
|
for (const child of node.value.children) {
|
|
338
181
|
let family;
|
|
339
182
|
if (child.type === "String") family = withoutQuotes(child.value);
|
|
340
|
-
else if (child.type === "Identifier" && child.name
|
|
183
|
+
else if (child.type === "Identifier" && !cssWideKeywords.has(child.name.toLowerCase())) family = child.name;
|
|
341
184
|
if (!family) continue;
|
|
342
|
-
s.appendRight(child.loc.end.offset, `, "${fallbackName(family)}"`);
|
|
185
|
+
if (!genericCSSFamilies.has(family.toLowerCase())) s.appendRight(child.loc.end.offset, `, "${fallbackName(family)}"`);
|
|
343
186
|
return;
|
|
344
187
|
}
|
|
345
188
|
}
|
|
346
189
|
});
|
|
347
190
|
if (s.hasChanged()) return {
|
|
348
191
|
code: s.toString(),
|
|
192
|
+
/* v8 ignore next 3 */
|
|
349
193
|
map: options.sourcemap ? s.generateMap({
|
|
350
194
|
source: id,
|
|
351
195
|
includeContent: true
|
|
@@ -355,6 +199,5 @@ const FontaineTransform = createUnplugin((options) => {
|
|
|
355
199
|
}
|
|
356
200
|
};
|
|
357
201
|
});
|
|
358
|
-
|
|
359
202
|
//#endregion
|
|
360
|
-
export { DEFAULT_CATEGORY_FALLBACKS, FontaineTransform, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks };
|
|
203
|
+
export { DEFAULT_CATEGORY_FALLBACKS, FontaineTransform, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks };
|