UNPKG

unplugin-fonts

Version:
169 lines (168 loc) 5.9 kB
import { resolveFontFiles, resolveUserOption } from "./custom.mjs"; import MagicString from "magic-string"; import { pathToFileURL } from "node:url"; import { parse, walk } from "css-tree"; import { generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } from "fontaine"; //#region src/loaders/fallback.ts const VARIABLE_SUFFIX_RE = /(?: Variable|-variable)$/; const CSS_RE = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss)(?:\?.*)?$/; const GENERIC_FAMILIES = new Set([ "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "emoji", "math", "fangsong", "inherit", "initial", "unset", "revert" ]); /** * Yields all font families across loaders that have fallback configured. * Single iteration point — hasFallbacks, collectFallbackNames, and * generateAllFallbacks all consume this. */ function* iterateFallbackFamilies(options, resolvedCustom, root) { if (options.custom && resolvedCustom) for (const family of resolvedCustom.families) { if (!family.fallback) continue; yield { familyName: family.name, config: family.fallback, fontFilePath: root ? findRepresentativeFontFile(family, resolvedCustom, root) : void 0 }; } if (options.google) for (const family of options.google.families) { if (typeof family === "string" || !family.fallback) continue; yield { familyName: family.name, config: family.fallback }; } if (options.fontsource) for (const family of options.fontsource.families) { if (!family || typeof family === "string" || !family.fallback) continue; yield { familyName: family.name.replace(VARIABLE_SUFFIX_RE, ""), config: family.fallback }; } if (options.typekit?.families) for (const family of options.typekit.families) { if (typeof family === "string" || !family.fallback) continue; yield { familyName: family.name, config: family.fallback }; } } /** * Find a representative font file to read metrics from. * Prefers regular (400) weight, then .ttf/.woff2 formats. */ function findRepresentativeFontFile(family, resolvedOptions, root) { const faces = resolveFontFiles(family, resolvedOptions, root); if (faces.length === 0) return void 0; const regularFace = faces.find((f) => f.weight === 400) ?? faces[0]; for (const ext of [ ".ttf", ".woff2", ".woff", ".otf" ]) { const file = regularFace.files.find((f) => f.ext === ext); if (file) return file.src; } return regularFace.files[0]?.src; } async function generateFallbackForFamily(params) { const { familyName, config, fontFilePath } = params; let metrics = await getMetricsForFamily(familyName); if (!metrics && fontFilePath) metrics = await readMetrics(pathToFileURL(fontFilePath)).catch(() => null); if (!metrics) { console.warn(`unplugin-fonts: Could not resolve metrics for "${familyName}", skipping fallback generation`); return ""; } const systemFonts = config.fallbacks ?? resolveCategoryFallbacks({ fontFamily: familyName, fallbacks: {}, metrics: config.category ? { ...metrics, category: config.category } : metrics }); const fallbackName = config.name ?? generateFallbackName(familyName); const systemMetricsList = await Promise.all(systemFonts.map((font) => getMetricsForFamily(font))); const css = []; for (let i = 0; i < systemFonts.length; i++) { const systemMetrics = systemMetricsList[i]; if (!systemMetrics) continue; css.push(generateFontFace(metrics, { name: fallbackName, font: systemFonts[i], metrics: systemMetrics })); } return css.join(""); } function hasFallbacks(options) { return !iterateFallbackFamilies(options, options.custom ? resolveUserOption(options.custom) : void 0).next().done; } /** * Generate fallback CSS for all font families that have opted in. */ async function generateAllFallbacks(options, root) { const entries = [...iterateFallbackFamilies(options, options.custom ? resolveUserOption(options.custom) : void 0, root)]; return (await Promise.all(entries.map((entry) => generateFallbackForFamily(entry)))).filter(Boolean).join(""); } /** * Build a map of font family name → fallback name for all fonts * that have fallback configured. Used by the transform hook. */ function collectFallbackNames(options) { const resolvedCustom = options.custom ? resolveUserOption(options.custom) : void 0; const map = /* @__PURE__ */ new Map(); for (const { familyName, config } of iterateFallbackFamilies(options, resolvedCustom)) map.set(familyName, config.name ?? generateFallbackName(familyName)); return map; } /** * Transform CSS to append fallback font names to font-family declarations. * Only modifies declarations that reference a font with fallback configured. */ function transformFontFamilyDeclarations(code, id, fallbackNames, sourcemap) { if (!CSS_RE.test(id)) return void 0; if (fallbackNames.size === 0) return void 0; const s = new MagicString(code); walk(parse(code, { positions: true }), { visit: "Declaration", enter(node) { if (node.property !== "font-family" && node.property !== "font") return; if (this.atrule && this.atrule.name === "font-face") return; if (node.value.type !== "Value") return; for (const child of node.value.children) { let family; if (child.type === "String") family = child.value; else if (child.type === "Identifier" && !GENERIC_FAMILIES.has(child.name)) family = child.name; if (!family) continue; const fallbackName = fallbackNames.get(family); if (!fallbackName) continue; s.appendRight(child.loc.end.offset, `, "${fallbackName}"`); } } }); if (!s.hasChanged()) return void 0; return { code: s.toString(), map: sourcemap ? s.generateMap({ source: id, includeContent: true }) : void 0 }; } //#endregion export { collectFallbackNames, generateAllFallbacks, hasFallbacks, transformFontFamilyDeclarations };