340 lines
12 KiB
JavaScript
340 lines
12 KiB
JavaScript
|
|
import { mergeDeep, toArray, definePreset } from '@unocss/core';
|
||
|
|
|
||
|
|
const LAYER_IMPORTS = "imports";
|
||
|
|
|
||
|
|
function createBunnyFontsProvider(name, host) {
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
getImportUrl(fonts) {
|
||
|
|
const fontFamilies = fonts.map((font) => {
|
||
|
|
const { name: name2, weights, italic } = font;
|
||
|
|
const formattedName = name2.toLowerCase().replace(/\s/g, "-");
|
||
|
|
if (!weights?.length)
|
||
|
|
return `${formattedName}${italic ? ":i" : ""}`;
|
||
|
|
let weightsAsString = weights.map((weight) => weight.toString());
|
||
|
|
const weightsHaveItalic = weightsAsString.some((weight) => weight.endsWith("i"));
|
||
|
|
if (!weightsHaveItalic && italic)
|
||
|
|
weightsAsString = weightsAsString.map((weight) => weight += "i");
|
||
|
|
return `${formattedName}:${weightsAsString.join(",")}`;
|
||
|
|
});
|
||
|
|
return `${host}/css?family=${fontFamilies.join("|")}&display=swap`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const BunnyFontsProvider = createBunnyFontsProvider(
|
||
|
|
"bunny",
|
||
|
|
"https://fonts.bunny.net"
|
||
|
|
);
|
||
|
|
|
||
|
|
const FontshareProvider = createFontshareProvider("fontshare", "https://api.fontshare.com");
|
||
|
|
function createFontshareProvider(name, host) {
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
getImportUrl(fonts) {
|
||
|
|
const strings = fonts.map((f) => {
|
||
|
|
let name2 = f.name.replace(/\s+/g, "-").toLocaleLowerCase();
|
||
|
|
if (f.weights?.length)
|
||
|
|
name2 += `@${f.weights.flatMap((w) => f.italic ? Number(w) + 1 : w).sort().join()}`;
|
||
|
|
else name2 += `@${f.italic ? 2 : 1}`;
|
||
|
|
return `f[]=${name2}`;
|
||
|
|
}).join("&");
|
||
|
|
return `${host}/v2/css?${strings}&display=swap`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function createFontSourceProvider(name, host) {
|
||
|
|
const fontsMap = /* @__PURE__ */ new Map();
|
||
|
|
const variablesMap = /* @__PURE__ */ new Map();
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
async getPreflight(fonts) {
|
||
|
|
const list = await Promise.all(fonts.map(async (font) => {
|
||
|
|
const css = [];
|
||
|
|
const id = font.name.toLowerCase().replace(/\s+/g, "-");
|
||
|
|
let metadata = fontsMap.get(id);
|
||
|
|
if (!metadata) {
|
||
|
|
const url = `https://api.fontsource.org/v1/fonts/${id}`;
|
||
|
|
try {
|
||
|
|
metadata = await (await import('ofetch')).$fetch(url);
|
||
|
|
fontsMap.set(id, metadata);
|
||
|
|
} catch {
|
||
|
|
throw new Error(`Failed to fetch font: ${font.name}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const { subsets, weights } = metadata;
|
||
|
|
if (metadata.variable) {
|
||
|
|
let variableData = variablesMap.get(id);
|
||
|
|
const url = `https://api.fontsource.org/v1/variable/${id}`;
|
||
|
|
try {
|
||
|
|
variableData = await (await import('ofetch')).$fetch(url);
|
||
|
|
variablesMap.set(id, variableData);
|
||
|
|
} catch {
|
||
|
|
throw new Error(`Failed to fetch font variable: ${font.name}`);
|
||
|
|
}
|
||
|
|
const mergeAxes = mergeDeep(variableData.axes, font.variable ?? {});
|
||
|
|
for (const subset of subsets) {
|
||
|
|
const url2 = `${host}/fontsource/fonts/${metadata.id}:vf@latest/${subset}-${font.italic ? "wght-italic" : "wght-normal"}.woff2`;
|
||
|
|
const fontObj = {
|
||
|
|
family: `${metadata.family}`,
|
||
|
|
display: "swap",
|
||
|
|
style: font.italic ? "italic" : "normal",
|
||
|
|
weight: 400,
|
||
|
|
src: [{
|
||
|
|
url: url2,
|
||
|
|
format: "woff2-variations"
|
||
|
|
}],
|
||
|
|
variable: {
|
||
|
|
wght: mergeAxes.wght ?? void 0,
|
||
|
|
wdth: mergeAxes.wdth ?? void 0,
|
||
|
|
slnt: mergeAxes.slnt ?? void 0
|
||
|
|
},
|
||
|
|
unicodeRange: metadata.unicodeRange[subset],
|
||
|
|
comment: `${metadata.id}-${subset}-wght-normal`
|
||
|
|
};
|
||
|
|
css.push(generateFontFace(fontObj));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const _weights = font.weights && font.weights.length > 0 ? font.weights : weights;
|
||
|
|
for (const subset of subsets) {
|
||
|
|
for (const weight of _weights) {
|
||
|
|
const url = `${host}/fontsource/fonts/${metadata.id}@latest/${subset}-${weight}-${font.italic ? "italic" : "normal"}`;
|
||
|
|
const fontObj = {
|
||
|
|
family: metadata.family,
|
||
|
|
display: "swap",
|
||
|
|
style: font.italic ? "italic" : "normal",
|
||
|
|
weight: Number(weight),
|
||
|
|
src: [{
|
||
|
|
url: `${url}.woff2`,
|
||
|
|
format: "woff2"
|
||
|
|
}],
|
||
|
|
unicodeRange: metadata.unicodeRange[subset],
|
||
|
|
comment: `${metadata.id}-${subset}-${weight}${font.italic ? "-italic" : "-normal"}`
|
||
|
|
};
|
||
|
|
css.push(generateFontFace(fontObj));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return css;
|
||
|
|
}));
|
||
|
|
return list.flat().join("\n\n");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const FontSourceProvider = createFontSourceProvider("fontsource", "https://cdn.jsdelivr.net");
|
||
|
|
function generateFontFace(font) {
|
||
|
|
const {
|
||
|
|
family,
|
||
|
|
style,
|
||
|
|
display,
|
||
|
|
weight,
|
||
|
|
variable,
|
||
|
|
src,
|
||
|
|
unicodeRange,
|
||
|
|
comment,
|
||
|
|
spacer = "\n "
|
||
|
|
} = font;
|
||
|
|
const { wght, wdth, slnt } = variable ?? {};
|
||
|
|
let result = "@font-face {";
|
||
|
|
result += `${spacer}font-family: '${family}';`;
|
||
|
|
result += `${spacer}font-style: ${slnt ? `oblique ${Number(slnt.max) * -1}deg ${Number(slnt.min) * -1}deg` : style};`;
|
||
|
|
result += `${spacer}font-display: ${display};`;
|
||
|
|
result += `${spacer}font-weight: ${wght ? getVariableWght(wght) : weight};`;
|
||
|
|
if (wdth)
|
||
|
|
result += `${spacer}font-stretch: ${wdth.min}% ${wdth.max}%;`;
|
||
|
|
result += `${spacer}src: ${src.map(({ url, format }) => `url(${url}) format('${format}')`).join(", ")};`;
|
||
|
|
if (unicodeRange)
|
||
|
|
result += `${spacer}unicode-range: ${unicodeRange};`;
|
||
|
|
if (comment)
|
||
|
|
result = `/* ${comment} */
|
||
|
|
${result}`;
|
||
|
|
return `${result}
|
||
|
|
}`;
|
||
|
|
}
|
||
|
|
function getVariableWght(axes) {
|
||
|
|
if (!axes)
|
||
|
|
return "400";
|
||
|
|
if (axes.min === axes.max)
|
||
|
|
return `${axes.min}`;
|
||
|
|
return `${axes.min} ${axes.max}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createGoogleCompatibleProvider(name, host) {
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
getImportUrl(fonts) {
|
||
|
|
const sort = (weights) => {
|
||
|
|
const firstW = weights.map((w) => w[0]);
|
||
|
|
const lastW = weights.map((w) => w[1]);
|
||
|
|
return `${firstW.join(";")};${lastW.join(";")}`;
|
||
|
|
};
|
||
|
|
const strings = fonts.map((i) => {
|
||
|
|
let name2 = i.name.replace(/\s+/g, "+");
|
||
|
|
if (i.weights?.length) {
|
||
|
|
name2 += i.italic ? `:ital,wght@${sort(i.weights.map((w) => [`0,${w}`, `1,${w}`]))}` : `:wght@${i.weights.join(";")}`;
|
||
|
|
}
|
||
|
|
return `family=${name2}`;
|
||
|
|
}).join("&");
|
||
|
|
return `${host}/css2?${strings}&display=swap`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const GoogleFontsProvider = createGoogleCompatibleProvider("google", "https://fonts.googleapis.com");
|
||
|
|
|
||
|
|
const NoneProvider = {
|
||
|
|
name: "none",
|
||
|
|
getPreflight() {
|
||
|
|
return "";
|
||
|
|
},
|
||
|
|
getFontName(font) {
|
||
|
|
return font.name;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const builtinProviders = {
|
||
|
|
google: GoogleFontsProvider,
|
||
|
|
bunny: BunnyFontsProvider,
|
||
|
|
fontshare: FontshareProvider,
|
||
|
|
fontsource: FontSourceProvider,
|
||
|
|
none: NoneProvider
|
||
|
|
};
|
||
|
|
function resolveProvider(provider) {
|
||
|
|
if (typeof provider === "string")
|
||
|
|
return builtinProviders[provider];
|
||
|
|
return provider;
|
||
|
|
}
|
||
|
|
function normalizedFontMeta(meta, defaultProvider) {
|
||
|
|
if (typeof meta !== "string") {
|
||
|
|
meta.provider = resolveProvider(meta.provider || defaultProvider);
|
||
|
|
if (meta.weights)
|
||
|
|
meta.weights = [...new Set(meta.weights.sort((a, b) => a.toString().localeCompare(b.toString(), "en", { numeric: true })))];
|
||
|
|
return meta;
|
||
|
|
}
|
||
|
|
const [name, weights = ""] = meta.split(":");
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
weights: [...new Set(weights.split(/[,;]\s*/).filter(Boolean).sort((a, b) => a.localeCompare(b, "en", { numeric: true })))],
|
||
|
|
provider: resolveProvider(defaultProvider)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function createWebFontPreset(fetcher) {
|
||
|
|
return (options = {}) => {
|
||
|
|
const {
|
||
|
|
provider: defaultProvider = "google",
|
||
|
|
extendTheme = true,
|
||
|
|
inlineImports = true,
|
||
|
|
themeKey = "fontFamily",
|
||
|
|
customFetch = fetcher,
|
||
|
|
timeouts = {}
|
||
|
|
} = options;
|
||
|
|
const processors = toArray(options.processors || []);
|
||
|
|
const fontObject = Object.fromEntries(
|
||
|
|
Object.entries(options.fonts || {}).map(([name, meta]) => [name, toArray(meta).map((m) => normalizedFontMeta(m, defaultProvider))])
|
||
|
|
);
|
||
|
|
const fonts = Object.values(fontObject).flatMap((i) => i);
|
||
|
|
const importCache = {};
|
||
|
|
async function fetchWithTimeout(url) {
|
||
|
|
if (timeouts === false)
|
||
|
|
return customFetch(url);
|
||
|
|
const {
|
||
|
|
warning = 1e3,
|
||
|
|
failure = 2e3
|
||
|
|
} = timeouts;
|
||
|
|
let warned = false;
|
||
|
|
const timer = setTimeout(() => {
|
||
|
|
console.warn(`[unocss] Fetching web fonts: ${url}`);
|
||
|
|
warned = true;
|
||
|
|
}, warning);
|
||
|
|
return await Promise.race([
|
||
|
|
customFetch(url),
|
||
|
|
new Promise((_, reject) => {
|
||
|
|
setTimeout(() => reject(new Error(`[unocss] Fetch web fonts timeout.`)), failure);
|
||
|
|
})
|
||
|
|
]).then((res) => {
|
||
|
|
if (warned)
|
||
|
|
console.info(`[unocss] Web fonts fetched.`);
|
||
|
|
return res;
|
||
|
|
}).finally(() => clearTimeout(timer));
|
||
|
|
}
|
||
|
|
async function importUrl(url) {
|
||
|
|
if (inlineImports) {
|
||
|
|
if (!importCache[url]) {
|
||
|
|
importCache[url] = fetchWithTimeout(url).catch((e) => {
|
||
|
|
console.error(`[unocss] Failed to fetch web fonts: ${url}`);
|
||
|
|
console.error(e);
|
||
|
|
if (typeof process !== "undefined" && process.env.CI)
|
||
|
|
throw e;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return await importCache[url];
|
||
|
|
} else {
|
||
|
|
return `@import url('${url}');`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const enabledProviders = Array.from(new Set(fonts.map((i) => i.provider)));
|
||
|
|
async function getCSSDefault(fonts2, providers) {
|
||
|
|
const preflights = [];
|
||
|
|
for (const provider of providers) {
|
||
|
|
const fontsForProvider = fonts2.filter((i) => i.provider.name === provider.name);
|
||
|
|
if (provider.getImportUrl) {
|
||
|
|
const url = provider.getImportUrl(fontsForProvider);
|
||
|
|
if (url)
|
||
|
|
preflights.push(await importUrl(url));
|
||
|
|
}
|
||
|
|
preflights.push(await provider.getPreflight?.(fontsForProvider));
|
||
|
|
}
|
||
|
|
const css = preflights.filter(Boolean).join("\n");
|
||
|
|
return css;
|
||
|
|
}
|
||
|
|
const preset = {
|
||
|
|
name: "@unocss/preset-web-fonts",
|
||
|
|
preflights: [
|
||
|
|
{
|
||
|
|
async getCSS() {
|
||
|
|
let css;
|
||
|
|
for (const processor of processors) {
|
||
|
|
const result = await processor.getCSS?.(fonts, enabledProviders, getCSSDefault);
|
||
|
|
if (result) {
|
||
|
|
css = result;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!css) {
|
||
|
|
css = await getCSSDefault(
|
||
|
|
fonts,
|
||
|
|
enabledProviders
|
||
|
|
);
|
||
|
|
}
|
||
|
|
for (const processor of processors)
|
||
|
|
css = await processor.transformCSS?.(css) || css;
|
||
|
|
return css;
|
||
|
|
},
|
||
|
|
layer: inlineImports ? void 0 : LAYER_IMPORTS
|
||
|
|
}
|
||
|
|
]
|
||
|
|
};
|
||
|
|
if (extendTheme) {
|
||
|
|
preset.extendTheme = (theme) => {
|
||
|
|
if (!theme[themeKey])
|
||
|
|
theme[themeKey] = {};
|
||
|
|
const obj = Object.fromEntries(
|
||
|
|
Object.entries(fontObject).map(([name, fonts2]) => [name, fonts2.map((f) => f.provider.getFontName?.(f) ?? `"${f.name}"`)])
|
||
|
|
);
|
||
|
|
for (const key of Object.keys(obj)) {
|
||
|
|
if (typeof theme[themeKey][key] === "string")
|
||
|
|
theme[themeKey][key] = obj[key].map((i) => `${i},`).join("") + theme[themeKey][key];
|
||
|
|
else
|
||
|
|
theme[themeKey][key] = obj[key].join(",");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return preset;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const userAgentWoff2 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
||
|
|
const defaultFetch = async (url) => (await import('ofetch')).$fetch(url, { headers: { "User-Agent": userAgentWoff2 }, retry: 3 });
|
||
|
|
const presetWebFonts = definePreset(createWebFontPreset(defaultFetch));
|
||
|
|
|
||
|
|
export { createGoogleCompatibleProvider as createGoogleProvider, presetWebFonts as default, normalizedFontMeta, presetWebFonts };
|