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