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,354 @@
1
+ import { generate, parse, walk } from "css-tree";
2
+ import { char, charIn, createRegExp, oneOrMore } from "magic-regexp";
3
+ import { isAbsolute } from "pathe";
4
+ import { hasProtocol, parseURL } from "ufo";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { fromUrl } from "@capsizecss/unpack";
7
+ import { fromFile } from "@capsizecss/unpack/fs";
8
+ //#region src/css.ts
9
+ function toPercentage(value, fractionDigits = 4) {
10
+ return `${+(value * 100).toFixed(fractionDigits)}%`;
11
+ }
12
+ function toCSS(properties, indent = 2) {
13
+ return Object.entries(properties).map(([key, value]) => `${" ".repeat(indent)}${key}: ${value};`).join("\n");
14
+ }
15
+ const QUOTES_RE = createRegExp(charIn("\"'").at.lineStart().or(charIn("\"'").at.lineEnd()), ["g"]);
16
+ const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
17
+ const QUERY_OR_FRAGMENT_RE = createRegExp(charIn("?#").and(oneOrMore(char).optionally()).at.lineEnd());
18
+ /** Removes any `?query` or `#fragment` suffix, which is not part of the file a URL points to. */
19
+ function withoutQueryOrFragment(source) {
20
+ return source.replace(QUERY_OR_FRAGMENT_RE, "");
21
+ }
22
+ /**
23
+ * Whether a CSS URL may be resolved relative to the stylesheet that declared it, as opposed to
24
+ * the document root (`/font.woff2`) or an external location (`https:`, `//`, `data:`).
25
+ *
26
+ * Bare package specifiers and the webpack `~` convention are indistinguishable from
27
+ * stylesheet-relative paths here, so callers should fall back to their own resolver when
28
+ * resolving against the stylesheet finds nothing.
29
+ */
30
+ function isStylesheetRelative(source) {
31
+ return !hasProtocol(source, { acceptRelative: true }) && !isAbsolute(source);
32
+ }
33
+ const cssWideKeywords = /* @__PURE__ */ new Set([
34
+ "inherit",
35
+ "initial",
36
+ "revert",
37
+ "revert-layer",
38
+ "unset"
39
+ ]);
40
+ const genericCSSFamilies = /* @__PURE__ */ new Set([
41
+ "serif",
42
+ "sans-serif",
43
+ "monospace",
44
+ "cursive",
45
+ "fantasy",
46
+ "system-ui",
47
+ "ui-serif",
48
+ "ui-sans-serif",
49
+ "ui-monospace",
50
+ "ui-rounded",
51
+ "emoji",
52
+ "math",
53
+ "fangsong"
54
+ ]);
55
+ const genericFamilyLocals = {
56
+ "serif": [
57
+ "Times New Roman",
58
+ "Tinos",
59
+ "Liberation Serif",
60
+ "DejaVu Serif",
61
+ "Times"
62
+ ],
63
+ "sans-serif": [
64
+ "Arial",
65
+ "Arimo",
66
+ "Liberation Sans",
67
+ "DejaVu Sans",
68
+ "Helvetica"
69
+ ],
70
+ "monospace": [
71
+ "Courier New",
72
+ "Cousine",
73
+ "Liberation Mono",
74
+ "DejaVu Sans Mono",
75
+ "Menlo"
76
+ ],
77
+ "system-ui": [
78
+ "Segoe UI",
79
+ "Roboto",
80
+ "Helvetica Neue",
81
+ "Cantarell",
82
+ "Ubuntu",
83
+ "Noto Sans"
84
+ ],
85
+ "cursive": [
86
+ "Comic Sans MS",
87
+ "Apple Chancery",
88
+ "Segoe Script",
89
+ "URW Chancery L"
90
+ ],
91
+ "fantasy": [
92
+ "Impact",
93
+ "Papyrus",
94
+ "Luminari"
95
+ ]
96
+ };
97
+ genericFamilyLocals["ui-serif"] = genericFamilyLocals.serif;
98
+ genericFamilyLocals["ui-sans-serif"] = genericFamilyLocals["sans-serif"];
99
+ genericFamilyLocals["ui-monospace"] = genericFamilyLocals.monospace;
100
+ genericFamilyLocals["ui-rounded"] = genericFamilyLocals["system-ui"];
101
+ const fontProperties = /* @__PURE__ */ new Set([
102
+ "font-weight",
103
+ "font-style",
104
+ "font-stretch"
105
+ ]);
106
+ /**
107
+ * Extracts font family and source information from a CSS @font-face rule using css-tree.
108
+ *
109
+ * @param {string} css - The CSS containing @font-face rules
110
+ * @returns Array<{ family?: string, source?: string }> - Array of objects with font family and source information
111
+ */
112
+ function parseFontFace(css) {
113
+ const families = [];
114
+ const ast = typeof css === "string" ? parse(css, { positions: true }) : css;
115
+ walk(ast, {
116
+ visit: "Atrule",
117
+ enter(node) {
118
+ if (node.name !== "font-face") return;
119
+ let family;
120
+ const sources = [];
121
+ const properties = {};
122
+ if (node.block) walk(node.block, {
123
+ visit: "Declaration",
124
+ enter(declaration) {
125
+ if (declaration.property === "font-family" && declaration.value.type === "Value") for (const child of declaration.value.children) {
126
+ if (child.type === "String") {
127
+ family = withoutQuotes(child.value);
128
+ break;
129
+ }
130
+ if (child.type === "Identifier" && !genericCSSFamilies.has(child.name)) {
131
+ family = child.name;
132
+ break;
133
+ }
134
+ }
135
+ if (fontProperties.has(declaration.property)) {
136
+ if (declaration.value.type === "Value") for (const child of declaration.value.children) {
137
+ const hasValue = !!properties[declaration.property];
138
+ properties[declaration.property] ||= "";
139
+ properties[declaration.property] += (hasValue ? " " : "") + generate(child);
140
+ }
141
+ }
142
+ if (declaration.property === "src") walk(declaration.value, {
143
+ visit: "Url",
144
+ enter(urlNode) {
145
+ const source = withoutQuotes(urlNode.value);
146
+ if (source) sources.push(source);
147
+ }
148
+ });
149
+ }
150
+ });
151
+ if (family) {
152
+ for (const source of sources) families.push({
153
+ index: node.loc.start.offset,
154
+ family,
155
+ source,
156
+ properties
157
+ });
158
+ if (!sources.length) families.push({
159
+ index: node.loc.start.offset,
160
+ family,
161
+ properties
162
+ });
163
+ }
164
+ }
165
+ });
166
+ return families;
167
+ }
168
+ /**
169
+ * Generates a fallback name based on the first font family specified in the input string.
170
+ * @param {string} name - The full font family string.
171
+ * @returns {string} - The fallback font name.
172
+ */
173
+ function generateFallbackName(name) {
174
+ return `${withoutQuotes(name.split(",").shift())} fallback`;
175
+ }
176
+ /**
177
+ * Generates a CSS `@font-face' declaration for a font, taking fallback and resizing into account.
178
+ * @param {FontFaceMetrics} metrics - The metrics of the preferred font. See {@link FontFaceMetrics}.
179
+ * @param {FallbackOptions} fallback - The fallback options, including name, font and optional metrics. See {@link FallbackOptions}.
180
+ * @returns {string} - The full `@font-face` CSS declaration.
181
+ */
182
+ function generateFontFace(metrics, fallback) {
183
+ const { name: fallbackName, font: fallbackFontName, metrics: fallbackMetrics, ...properties } = fallback;
184
+ const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm;
185
+ const fallbackFontXAvgRatio = fallbackMetrics ? fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm : 1;
186
+ const sizeAdjust = fallbackMetrics && preferredFontXAvgRatio && fallbackFontXAvgRatio ? preferredFontXAvgRatio / fallbackFontXAvgRatio : 1;
187
+ const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust;
188
+ const ascentOverride = metrics.ascent / adjustedEmSquare;
189
+ const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare;
190
+ const lineGapOverride = metrics.lineGap / adjustedEmSquare;
191
+ return `@font-face {\n${toCSS({
192
+ "font-family": JSON.stringify(fallbackName),
193
+ "src": renderLocalSrc(fallbackFontName),
194
+ "size-adjust": toPercentage(sizeAdjust),
195
+ "ascent-override": toPercentage(ascentOverride),
196
+ "descent-override": toPercentage(descentOverride),
197
+ "line-gap-override": toPercentage(lineGapOverride),
198
+ ...properties
199
+ })}\n}\n`;
200
+ }
201
+ function renderLocalSrc(font) {
202
+ return (genericFamilyLocals[withoutQuotes(font).toLowerCase()] || [font]).map((local) => `local(${JSON.stringify(local)})`).join(", ");
203
+ }
204
+ //#endregion
205
+ //#region src/fallbacks.ts
206
+ /**
207
+ * Default fallback font stacks for each font category.
208
+ * These are system fonts that work across different platforms.
209
+ */
210
+ const DEFAULT_CATEGORY_FALLBACKS = {
211
+ "sans-serif": [
212
+ "BlinkMacSystemFont",
213
+ "Segoe UI",
214
+ "Helvetica Neue",
215
+ "Arial",
216
+ "Noto Sans"
217
+ ],
218
+ "serif": [
219
+ "Times New Roman",
220
+ "Georgia",
221
+ "Noto Serif"
222
+ ],
223
+ "monospace": [
224
+ "Courier New",
225
+ "Roboto Mono",
226
+ "Noto Sans Mono"
227
+ ],
228
+ "display": [
229
+ "BlinkMacSystemFont",
230
+ "Segoe UI",
231
+ "Helvetica Neue",
232
+ "Arial",
233
+ "Noto Sans"
234
+ ],
235
+ "handwriting": [
236
+ "BlinkMacSystemFont",
237
+ "Segoe UI",
238
+ "Helvetica Neue",
239
+ "Arial",
240
+ "Noto Sans"
241
+ ]
242
+ };
243
+ /**
244
+ * Resolves the appropriate fallback fonts for a given font family.
245
+ *
246
+ * Resolution order:
247
+ * 1. If fallbacks is an array, use it as a global override
248
+ * 2. If fallbacks is an object with the font family key, use that override
249
+ * 3. If metrics contain a category, use the category-based fallbacks
250
+ * 4. Default to sans-serif category fallbacks
251
+ *
252
+ * @param options - Configuration for fallback resolution
253
+ * @returns Array of fallback font family names
254
+ */
255
+ function resolveCategoryFallbacks(options) {
256
+ const { fontFamily, fallbacks, metrics, categoryFallbacks } = options;
257
+ const mergedCategoryFallbacks = { ...DEFAULT_CATEGORY_FALLBACKS };
258
+ if (categoryFallbacks) for (const category in categoryFallbacks) {
259
+ const categoryKey = category;
260
+ const categoryFallbacksList = categoryFallbacks[categoryKey];
261
+ if (categoryFallbacksList) mergedCategoryFallbacks[categoryKey] = categoryFallbacksList;
262
+ }
263
+ if (Array.isArray(fallbacks)) return fallbacks;
264
+ const familyFallback = fallbacks[fontFamily];
265
+ if (familyFallback) return familyFallback;
266
+ if (metrics?.category) {
267
+ const categoryFallback = mergedCategoryFallbacks[metrics.category];
268
+ if (categoryFallback) return categoryFallback;
269
+ }
270
+ return mergedCategoryFallbacks["sans-serif"];
271
+ }
272
+ //#endregion
273
+ //#region src/metrics.ts
274
+ const metricCache = {};
275
+ function filterRequiredMetrics(font) {
276
+ return {
277
+ ascent: font.ascent,
278
+ descent: font.descent,
279
+ lineGap: font.lineGap,
280
+ unitsPerEm: font.unitsPerEm,
281
+ xWidthAvg: font.xWidthAvg,
282
+ category: font.category
283
+ };
284
+ }
285
+ /**
286
+ * Retrieves the font metrics for a given font family from the metrics collection. Uses caching to avoid redundant calculations.
287
+ * @param {string} family - The name of the font family for which metrics are requested.
288
+ * @returns {Promise<FontFaceMetrics | null>} - A promise that resolves with the filtered font metrics or null if not found. See {@link FontFaceMetrics}.
289
+ * @async
290
+ */
291
+ async function getMetricsForFamily(family) {
292
+ family = withoutQuotes(family);
293
+ if (family in metricCache) return metricCache[family] ?? null;
294
+ try {
295
+ const name = fontFamilyToCamelCase(family);
296
+ const { entireMetricsCollection } = await import("#capsize-font-metrics");
297
+ const metrics = entireMetricsCollection[name];
298
+ /* v8 ignore next 4 */
299
+ if (!("descent" in metrics)) {
300
+ metricCache[family] = null;
301
+ return null;
302
+ }
303
+ const filteredMetrics = filterRequiredMetrics(metrics);
304
+ metricCache[family] = filteredMetrics;
305
+ return filteredMetrics;
306
+ } catch {
307
+ metricCache[family] = null;
308
+ return null;
309
+ }
310
+ }
311
+ const urlRequestCache = /* @__PURE__ */ new Map();
312
+ /**
313
+ * Reads font metrics from a specified source URL or file path. This function supports both local files and remote URLs.
314
+ * It caches the results to optimise subsequent requests for the same source.
315
+ * @param {URL | string} _source - The source URL or local file path from which to read the font metrics.
316
+ * @returns {Promise<FontFaceMetrics | null>} - A promise that resolves to the filtered font metrics or null if the source cannot be processed.
317
+ * @async
318
+ */
319
+ async function readMetrics(_source) {
320
+ const source = typeof _source !== "string" && "href" in _source ? _source.href : _source;
321
+ if (source in metricCache) return metricCache[source] ?? null;
322
+ const { protocol } = parseURL(source);
323
+ if (!protocol) return null;
324
+ let metrics;
325
+ if (protocol === "file:") metrics = await fromFile(fileURLToPath(source));
326
+ else if (urlRequestCache.has(source)) metrics = await urlRequestCache.get(source);
327
+ else {
328
+ const requestPromise = fromUrl(source);
329
+ urlRequestCache.set(source, requestPromise);
330
+ metrics = await requestPromise;
331
+ }
332
+ const filteredMetrics = filterRequiredMetrics(metrics);
333
+ metricCache[source] = filteredMetrics;
334
+ return filteredMetrics;
335
+ }
336
+ /**
337
+ * Reads font metrics for a `src` URL declared in a stylesheet.
338
+ *
339
+ * A scheme-less, non-rooted URL is resolved against the stylesheet first, then handed to
340
+ * `resolvePath` if that yields no metrics: bare package specifiers and webpack's `~` prefix
341
+ * look identical to stylesheet-relative paths, and only the caller's resolver can map them.
342
+ */
343
+ async function readMetricsForSource(source, importer, resolvePath) {
344
+ if (importer && isAbsolute(importer) && isStylesheetRelative(source)) {
345
+ const metrics = await readMetrics(new URL(source, pathToFileURL(importer))).catch(() => null);
346
+ if (metrics) return metrics;
347
+ }
348
+ return readMetrics(resolvePath(source));
349
+ }
350
+ function fontFamilyToCamelCase(str) {
351
+ return str.split(/[\s|-]/).filter(Boolean).map((s, i) => `${s.charAt(0)[i > 0 ? "toUpperCase" : "toLowerCase"]()}${s.slice(1)}`).join("");
352
+ }
353
+ //#endregion
354
+ export { resolveCategoryFallbacks as a, generateFontFace as c, withoutQueryOrFragment as d, withoutQuotes as f, DEFAULT_CATEGORY_FALLBACKS as i, genericCSSFamilies as l, readMetrics as n, cssWideKeywords as o, readMetricsForSource as r, generateFallbackName as s, getMetricsForFamily as t, parseFontFace as u };
@@ -0,0 +1,91 @@
1
+ const require_metrics = require("./metrics-ChAPPk2j.cjs");
2
+ let css_tree = require("css-tree");
3
+ //#region src/postcss.ts
4
+ const supportedExtensions = [
5
+ "woff2",
6
+ "woff",
7
+ "ttf"
8
+ ];
9
+ /**
10
+ * Resolves the file a node originated from, consulting any upstream source map so that
11
+ * relative font URLs are resolved against the stylesheet that actually declared them
12
+ * rather than the entrypoint they were inlined into.
13
+ */
14
+ function originatingFile(node, result) {
15
+ const source = node?.source;
16
+ if (source?.start && source.input.map) {
17
+ const origin = source.input.origin(source.start.line, source.start.column);
18
+ if (origin && "file" in origin && origin.file) return origin.file;
19
+ }
20
+ return source?.input.file ?? result.opts.from;
21
+ }
22
+ function isFontFaceRule(node) {
23
+ return node?.type === "atrule" && node.name.toLowerCase() === "font-face";
24
+ }
25
+ function fontaine(options) {
26
+ const resolvePath = options.resolvePath || ((id) => id);
27
+ const fallbackName = options.fallbackName || require_metrics.generateFallbackName;
28
+ const skipFontFaceGeneration = options.skipFontFaceGeneration || (() => false);
29
+ return {
30
+ postcssPlugin: "fontaine",
31
+ async Once(root, { result, postcss }) {
32
+ const fontFaces = [];
33
+ root.walkAtRules(/^font-face$/i, (rule) => void fontFaces.push(rule));
34
+ for (const rule of fontFaces) for (const { family, source: url, properties } of require_metrics.parseFontFace(rule.toString())) {
35
+ const source = url && require_metrics.withoutQueryOrFragment(url);
36
+ if (!source || !supportedExtensions.some((e) => source.endsWith(e))) continue;
37
+ if (skipFontFaceGeneration(fallbackName(family))) continue;
38
+ const metrics = await require_metrics.getMetricsForFamily(family) || await require_metrics.readMetricsForSource(source, originatingFile(rule, result), resolvePath).catch(() => null);
39
+ if (!metrics) continue;
40
+ const familyFallbacks = require_metrics.resolveCategoryFallbacks({
41
+ fontFamily: family,
42
+ fallbacks: options.fallbacks,
43
+ metrics,
44
+ categoryFallbacks: options.categoryFallbacks
45
+ });
46
+ let css = "";
47
+ for (let i = familyFallbacks.length - 1; i >= 0; i--) {
48
+ const fallback = familyFallbacks[i];
49
+ const fallbackMetrics = await require_metrics.getMetricsForFamily(fallback);
50
+ if (!fallbackMetrics) continue;
51
+ css = require_metrics.generateFontFace(metrics, {
52
+ name: fallbackName(family),
53
+ font: fallback,
54
+ metrics: fallbackMetrics,
55
+ ...properties
56
+ }) + css;
57
+ }
58
+ if (!css) continue;
59
+ const generated = postcss.parse(css, { from: result.opts.from });
60
+ const before = rule.raws.before;
61
+ rule.raws.before = "\n";
62
+ generated.first.raws.before = before;
63
+ rule.before(generated);
64
+ break;
65
+ }
66
+ root.walkDecls(/^font-family$/i, (decl) => {
67
+ if (isFontFaceRule(decl.parent)) return;
68
+ const value = (0, css_tree.parse)(decl.value, {
69
+ context: "value",
70
+ positions: true
71
+ });
72
+ /* v8 ignore next 2 -- `value` context always yields a Value node or throws */
73
+ if (value.type !== "Value") return;
74
+ for (const child of value.children) {
75
+ let family;
76
+ if (child.type === "String") family = require_metrics.withoutQuotes(child.value);
77
+ else if (child.type === "Identifier" && !require_metrics.cssWideKeywords.has(child.name.toLowerCase())) family = child.name;
78
+ if (!family) continue;
79
+ if (!require_metrics.genericCSSFamilies.has(family.toLowerCase())) {
80
+ const offset = child.loc.end.offset;
81
+ decl.value = `${decl.value.slice(0, offset)}, "${fallbackName(family)}"${decl.value.slice(offset)}`;
82
+ }
83
+ return;
84
+ }
85
+ });
86
+ }
87
+ };
88
+ }
89
+ const plugin = Object.assign(fontaine, { postcss: true });
90
+ //#endregion
91
+ module.exports = plugin;
@@ -0,0 +1,52 @@
1
+ import { n as FontCategory } from "./fallbacks-Cj4LUWhi.cjs";
2
+ import { Plugin } from "postcss";
3
+ //#region src/postcss.d.ts
4
+ interface FontainePostcssOptions {
5
+ /**
6
+ * Font family fallbacks to use.
7
+ * Can be an array of fallback font family names to use for all fonts,
8
+ * or an object where keys are font family names and values are arrays of fallback font families.
9
+ */
10
+ fallbacks: string[] | Record<string, string[]>;
11
+ /**
12
+ * Category-specific fallback font stacks.
13
+ * When a font's category is detected (serif, sans-serif, monospace, etc.),
14
+ * these fallbacks will be used if no explicit per-family override is provided.
15
+ * @optional
16
+ */
17
+ categoryFallbacks?: Partial<Record<FontCategory, string[]>>;
18
+ /**
19
+ * Function to resolve a given path to a valid URL or local path.
20
+ * This is typically used to resolve font file paths.
21
+ * @optional
22
+ */
23
+ resolvePath?: (path: string) => string | URL;
24
+ /**
25
+ * A function to determine whether to skip font face generation for a given fallback name.
26
+ *
27
+ * Families skipped this way are still given a fallback in `font-family` declarations,
28
+ * on the assumption that the `@font-face` rule is provided elsewhere.
29
+ * @optional
30
+ */
31
+ skipFontFaceGeneration?: (fallbackName: string) => boolean;
32
+ /**
33
+ * Function to generate an unquoted font family name to use as a fallback.
34
+ * This should return a valid CSS font family name and should not include quotes.
35
+ * @optional
36
+ */
37
+ fallbackName?: (name: string) => string;
38
+ }
39
+ /**
40
+ * A PostCSS plugin that generates metric-adjusted fallback `@font-face` rules and appends
41
+ * them to `font-family` declarations.
42
+ *
43
+ * Unlike {@link FontaineTransform}, this runs over fully-compiled CSS, so it sees
44
+ * `@font-face` rules and `font-family` values produced by preprocessors such as Sass.
45
+ */
46
+ interface FontainePlugin {
47
+ (options: FontainePostcssOptions): Plugin;
48
+ postcss: true;
49
+ }
50
+ declare const plugin: FontainePlugin;
51
+ //#endregion
52
+ export { FontainePostcssOptions, plugin as default };
@@ -0,0 +1,52 @@
1
+ import { n as FontCategory } from "./fallbacks-Cj4LUWhi.mjs";
2
+ import { Plugin } from "postcss";
3
+ //#region src/postcss.d.ts
4
+ interface FontainePostcssOptions {
5
+ /**
6
+ * Font family fallbacks to use.
7
+ * Can be an array of fallback font family names to use for all fonts,
8
+ * or an object where keys are font family names and values are arrays of fallback font families.
9
+ */
10
+ fallbacks: string[] | Record<string, string[]>;
11
+ /**
12
+ * Category-specific fallback font stacks.
13
+ * When a font's category is detected (serif, sans-serif, monospace, etc.),
14
+ * these fallbacks will be used if no explicit per-family override is provided.
15
+ * @optional
16
+ */
17
+ categoryFallbacks?: Partial<Record<FontCategory, string[]>>;
18
+ /**
19
+ * Function to resolve a given path to a valid URL or local path.
20
+ * This is typically used to resolve font file paths.
21
+ * @optional
22
+ */
23
+ resolvePath?: (path: string) => string | URL;
24
+ /**
25
+ * A function to determine whether to skip font face generation for a given fallback name.
26
+ *
27
+ * Families skipped this way are still given a fallback in `font-family` declarations,
28
+ * on the assumption that the `@font-face` rule is provided elsewhere.
29
+ * @optional
30
+ */
31
+ skipFontFaceGeneration?: (fallbackName: string) => boolean;
32
+ /**
33
+ * Function to generate an unquoted font family name to use as a fallback.
34
+ * This should return a valid CSS font family name and should not include quotes.
35
+ * @optional
36
+ */
37
+ fallbackName?: (name: string) => string;
38
+ }
39
+ /**
40
+ * A PostCSS plugin that generates metric-adjusted fallback `@font-face` rules and appends
41
+ * them to `font-family` declarations.
42
+ *
43
+ * Unlike {@link FontaineTransform}, this runs over fully-compiled CSS, so it sees
44
+ * `@font-face` rules and `font-family` values produced by preprocessors such as Sass.
45
+ */
46
+ interface FontainePlugin {
47
+ (options: FontainePostcssOptions): Plugin;
48
+ postcss: true;
49
+ }
50
+ declare const plugin: FontainePlugin;
51
+ //#endregion
52
+ export { FontainePostcssOptions, plugin as default };
@@ -0,0 +1,91 @@
1
+ import { a as resolveCategoryFallbacks, c as generateFontFace, d as withoutQueryOrFragment, f as withoutQuotes, l as genericCSSFamilies, o as cssWideKeywords, r as readMetricsForSource, s as generateFallbackName, t as getMetricsForFamily, u as parseFontFace } from "./metrics-DgPzfV2U.mjs";
2
+ import { parse } from "css-tree";
3
+ //#region src/postcss.ts
4
+ const supportedExtensions = [
5
+ "woff2",
6
+ "woff",
7
+ "ttf"
8
+ ];
9
+ /**
10
+ * Resolves the file a node originated from, consulting any upstream source map so that
11
+ * relative font URLs are resolved against the stylesheet that actually declared them
12
+ * rather than the entrypoint they were inlined into.
13
+ */
14
+ function originatingFile(node, result) {
15
+ const source = node?.source;
16
+ if (source?.start && source.input.map) {
17
+ const origin = source.input.origin(source.start.line, source.start.column);
18
+ if (origin && "file" in origin && origin.file) return origin.file;
19
+ }
20
+ return source?.input.file ?? result.opts.from;
21
+ }
22
+ function isFontFaceRule(node) {
23
+ return node?.type === "atrule" && node.name.toLowerCase() === "font-face";
24
+ }
25
+ function fontaine(options) {
26
+ const resolvePath = options.resolvePath || ((id) => id);
27
+ const fallbackName = options.fallbackName || generateFallbackName;
28
+ const skipFontFaceGeneration = options.skipFontFaceGeneration || (() => false);
29
+ return {
30
+ postcssPlugin: "fontaine",
31
+ async Once(root, { result, postcss }) {
32
+ const fontFaces = [];
33
+ root.walkAtRules(/^font-face$/i, (rule) => void fontFaces.push(rule));
34
+ for (const rule of fontFaces) for (const { family, source: url, properties } of parseFontFace(rule.toString())) {
35
+ const source = url && withoutQueryOrFragment(url);
36
+ if (!source || !supportedExtensions.some((e) => source.endsWith(e))) continue;
37
+ if (skipFontFaceGeneration(fallbackName(family))) continue;
38
+ const metrics = await getMetricsForFamily(family) || await readMetricsForSource(source, originatingFile(rule, result), resolvePath).catch(() => null);
39
+ if (!metrics) continue;
40
+ const familyFallbacks = resolveCategoryFallbacks({
41
+ fontFamily: family,
42
+ fallbacks: options.fallbacks,
43
+ metrics,
44
+ categoryFallbacks: options.categoryFallbacks
45
+ });
46
+ let css = "";
47
+ for (let i = familyFallbacks.length - 1; i >= 0; i--) {
48
+ const fallback = familyFallbacks[i];
49
+ const fallbackMetrics = await getMetricsForFamily(fallback);
50
+ if (!fallbackMetrics) continue;
51
+ css = generateFontFace(metrics, {
52
+ name: fallbackName(family),
53
+ font: fallback,
54
+ metrics: fallbackMetrics,
55
+ ...properties
56
+ }) + css;
57
+ }
58
+ if (!css) continue;
59
+ const generated = postcss.parse(css, { from: result.opts.from });
60
+ const before = rule.raws.before;
61
+ rule.raws.before = "\n";
62
+ generated.first.raws.before = before;
63
+ rule.before(generated);
64
+ break;
65
+ }
66
+ root.walkDecls(/^font-family$/i, (decl) => {
67
+ if (isFontFaceRule(decl.parent)) return;
68
+ const value = parse(decl.value, {
69
+ context: "value",
70
+ positions: true
71
+ });
72
+ /* v8 ignore next 2 -- `value` context always yields a Value node or throws */
73
+ if (value.type !== "Value") return;
74
+ for (const child of value.children) {
75
+ let family;
76
+ if (child.type === "String") family = withoutQuotes(child.value);
77
+ else if (child.type === "Identifier" && !cssWideKeywords.has(child.name.toLowerCase())) family = child.name;
78
+ if (!family) continue;
79
+ if (!genericCSSFamilies.has(family.toLowerCase())) {
80
+ const offset = child.loc.end.offset;
81
+ decl.value = `${decl.value.slice(0, offset)}, "${fallbackName(family)}"${decl.value.slice(offset)}`;
82
+ }
83
+ return;
84
+ }
85
+ });
86
+ }
87
+ };
88
+ }
89
+ const plugin = Object.assign(fontaine, { postcss: true });
90
+ //#endregion
91
+ export { plugin as default };