Files
Tiber365/node_modules/unifont/dist/index.js
2025-07-24 18:46:24 +02:00

773 lines
26 KiB
JavaScript

import { hash } from 'ohash';
import { findAll, parse, generate } from 'css-tree';
import { ofetch } from 'ofetch';
const version = "0.5.2";
function memoryStorage() {
const cache = /* @__PURE__ */ new Map();
return {
getItem(key) {
return cache.get(key);
},
setItem(key, value) {
cache.set(key, value);
}
};
}
const ONE_WEEK = 1e3 * 60 * 60 * 24 * 7;
function createAsyncStorage(storage) {
return {
async getItem(key, init) {
const now = Date.now();
const res = await storage.getItem(key);
if (res && res.expires > now && res.version === version) {
return res.data;
}
if (!init) {
return null;
}
const data = await init();
await storage.setItem(key, { expires: now + ONE_WEEK, version, data });
return data;
},
async setItem(key, data) {
await storage.setItem(key, { expires: Date.now() + ONE_WEEK, version, data });
}
};
}
const extractableKeyMap = {
"src": "src",
"font-display": "display",
"font-weight": "weight",
"font-style": "style",
"font-feature-settings": "featureSettings",
"font-variations-settings": "variationSettings",
"unicode-range": "unicodeRange"
};
const formatMap = {
woff2: "woff2",
woff: "woff",
otf: "opentype",
ttf: "truetype",
eot: "embedded-opentype",
svg: "svg"
};
const formatPriorityList = Object.values(formatMap);
function extractFontFaceData(css, family) {
const fontFaces = [];
for (const node of findAll(parse(css), (node2) => node2.type === "Atrule" && node2.name === "font-face")) {
if (node.type !== "Atrule" || node.name !== "font-face") {
continue;
}
if (family) {
const isCorrectFontFace = node.block?.children.some((child) => {
if (child.type !== "Declaration" || child.property !== "font-family") {
return false;
}
const value = extractCSSValue(child);
const slug = family.toLowerCase();
if (typeof value === "string" && value.toLowerCase() === slug) {
return true;
}
if (Array.isArray(value) && value.length > 0 && value.some((v) => v.toLowerCase() === slug)) {
return true;
}
return false;
});
if (!isCorrectFontFace) {
continue;
}
}
const data = {};
for (const child of node.block?.children || []) {
if (child.type === "Declaration" && child.property in extractableKeyMap) {
const value = extractCSSValue(child);
data[extractableKeyMap[child.property]] = ["src", "unicode-range"].includes(child.property) && !Array.isArray(value) ? [value] : value;
}
}
if (!data.src) {
continue;
}
fontFaces.push(data);
}
return mergeFontSources(fontFaces);
}
function processRawValue(value) {
return value.split(",").map((v) => v.trim().replace(/^(?<quote>['"])(.*)\k<quote>$/, "$2"));
}
function extractCSSValue(node) {
if (node.value.type === "Raw") {
return processRawValue(node.value.value);
}
const values = [];
let buffer = "";
for (const child of node.value.children) {
if (child.type === "Function") {
if (child.name === "local" && child.children.first?.type === "String") {
values.push({ name: child.children.first.value });
}
if (child.name === "format") {
if (child.children.first?.type === "String") {
values.at(-1).format = child.children.first.value;
} else if (child.children.first?.type === "Identifier") {
values.at(-1).format = child.children.first.name;
}
}
if (child.name === "tech") {
if (child.children.first?.type === "String") {
values.at(-1).tech = child.children.first.value;
} else if (child.children.first?.type === "Identifier") {
values.at(-1).tech = child.children.first.name;
}
}
}
if (child.type === "Url") {
values.push({ url: child.value });
}
if (child.type === "Identifier") {
buffer = buffer ? `${buffer} ${child.name}` : child.name;
}
if (child.type === "String") {
values.push(child.value);
}
if (child.type === "Dimension") {
const dimensionValue = child.value + child.unit;
buffer = buffer ? `${buffer} ${dimensionValue}` : dimensionValue;
}
if (child.type === "Operator" && child.value === "," && buffer) {
values.push(buffer);
buffer = "";
}
if (child.type === "UnicodeRange") {
values.push(child.value);
}
if (child.type === "Number") {
values.push(Number(child.value));
}
}
if (buffer) {
values.push(buffer);
}
if (values.length === 1) {
return values[0];
}
return values;
}
function mergeFontSources(data) {
const mergedData = [];
for (const face of data) {
const keys = Object.keys(face).filter((k) => k !== "src");
const existing = mergedData.find((f) => Object.keys(f).length === keys.length + 1 && keys.every((key) => f[key]?.toString() === face[key]?.toString()));
if (existing) {
for (const s of face.src) {
if (existing.src.every((src) => "url" in src ? !("url" in s) || s.url !== src.url : !("name" in s) || s.name !== src.name)) {
existing.src.push(s);
}
}
} else {
mergedData.push(face);
}
}
for (const face of mergedData) {
face.src.sort((a, b) => {
const aIndex = "format" in a ? formatPriorityList.indexOf(a.format || "woff2") : -2;
const bIndex = "format" in b ? formatPriorityList.indexOf(b.format || "woff2") : -2;
return aIndex - bIndex;
});
}
return mergedData;
}
function mini$fetch(url, options) {
const retries = options?.retries ?? 3;
const retryDelay = options?.retryDelay ?? 1e3;
return ofetch(url, {
baseURL: options?.baseURL,
query: options?.query,
responseType: options?.responseType ?? "text",
headers: options?.headers,
retry: false
}).catch((err) => {
if (retries <= 0) {
throw err;
}
console.warn(`Could not fetch from \`${(options?.baseURL ?? "") + url}\`. Will retry in \`${retryDelay}ms\`. \`${retries}\` retries left.`);
return new Promise((resolve) => setTimeout(resolve, retryDelay)).then(() => mini$fetch(url, { ...options, retries: retries - 1 }));
});
}
const $fetch = Object.assign(mini$fetch, {
create: (defaults) => (url, options) => mini$fetch(url, {
...defaults,
...options
})
});
function defineFontProvider(name, provider) {
return (options) => Object.assign(provider.bind(null, options || {}), { _name: name });
}
function prepareWeights({
inputWeights,
weights,
hasVariableWeights
}) {
const collectedWeights = [];
for (const weight of inputWeights) {
if (weight.includes(" ")) {
if (hasVariableWeights) {
collectedWeights.push(weight);
continue;
}
const [min, max] = weight.split(" ");
collectedWeights.push(
...weights.filter((_w) => {
const w = Number(_w);
return w >= Number(min) && w <= Number(max);
}).map((w) => String(w))
);
continue;
}
if (weights.includes(weight)) {
collectedWeights.push(weight);
}
}
return [...new Set(collectedWeights)].map((weight) => ({
weight,
variable: weight.includes(" ")
}));
}
const fontCSSAPI = $fetch.create({ baseURL: "https://use.typekit.net" });
async function getAdobeFontMeta(id) {
const { kit } = await $fetch(`https://typekit.com/api/v1/json/kits/${id}/published`, { responseType: "json" });
return kit;
}
const KIT_REFRESH_TIMEOUT = 5 * 60 * 1e3;
const adobe = defineFontProvider("adobe", async (options, ctx) => {
if (!options.id) {
return;
}
const familyMap = /* @__PURE__ */ new Map();
const notFoundFamilies = /* @__PURE__ */ new Set();
const fonts = {
kits: []
};
let lastRefreshKitTime;
const kits = typeof options.id === "string" ? [options.id] : options.id;
await fetchKits();
async function fetchKits(bypassCache = false) {
familyMap.clear();
notFoundFamilies.clear();
fonts.kits = [];
await Promise.all(kits.map(async (id) => {
let meta;
const key = `adobe:meta-${id}.json`;
if (bypassCache) {
meta = await getAdobeFontMeta(id);
await ctx.storage.setItem(key, meta);
} else {
meta = await ctx.storage.getItem(key, () => getAdobeFontMeta(id));
}
if (!meta) {
throw new TypeError("No font metadata found in adobe response.");
}
fonts.kits.push(meta);
for (const family of meta.families) {
familyMap.set(family.name, family.id);
}
}));
}
async function getFontDetails(family, options2) {
options2.weights = options2.weights.map(String);
for (const kit of fonts.kits) {
const font = kit.families.find((f) => f.name === family);
if (!font) {
continue;
}
const weights = prepareWeights({
inputWeights: options2.weights,
hasVariableWeights: false,
weights: font.variations.map((v) => `${v.slice(-1)}00`)
}).map((w) => w.weight);
const styles = [];
for (const style of font.variations) {
if (style.includes("i") && !options2.styles.includes("italic")) {
continue;
}
if (!weights.includes(String(`${style.slice(-1)}00`))) {
continue;
}
styles.push(style);
}
if (styles.length === 0) {
continue;
}
const css = await fontCSSAPI(`/${kit.id}.css`);
const cssName = font.css_names[0] ?? family.toLowerCase().split(" ").join("-");
return extractFontFaceData(css, cssName).filter((font2) => {
const [lowerWeight, upperWeight] = Array.isArray(font2.weight) ? font2.weight : [0, 0];
return (!options2.styles || !font2.style || options2.styles.includes(font2.style)) && (!weights || !font2.weight || Array.isArray(font2.weight) ? weights.some((weight) => Number(weight) <= upperWeight || Number(weight) >= lowerWeight) : weights.includes(String(font2.weight)));
});
}
return [];
}
return {
listFonts() {
return [...familyMap.keys()];
},
async resolveFont(family, options2) {
if (notFoundFamilies.has(family)) {
return;
}
if (!familyMap.has(family)) {
const lastRefetch = lastRefreshKitTime || 0;
const now = Date.now();
if (now - lastRefetch > KIT_REFRESH_TIMEOUT) {
lastRefreshKitTime = Date.now();
await fetchKits(true);
}
}
if (!familyMap.has(family)) {
notFoundFamilies.add(family);
return;
}
const fonts2 = await ctx.storage.getItem(`adobe:${family}-${hash(options2)}-data.json`, () => getFontDetails(family, options2));
return { fonts: fonts2 };
}
};
});
const fontAPI$2 = $fetch.create({ baseURL: "https://fonts.bunny.net" });
const bunny = defineFontProvider("bunny", async (_options, ctx) => {
const familyMap = /* @__PURE__ */ new Map();
const fonts = await ctx.storage.getItem("bunny:meta.json", () => fontAPI$2("/list", { responseType: "json" }));
for (const [id, family] of Object.entries(fonts)) {
familyMap.set(family.familyName, id);
}
async function getFontDetails(family, options) {
const id = familyMap.get(family);
const font = fonts[id];
const weights = prepareWeights({
inputWeights: options.weights,
hasVariableWeights: false,
weights: font.weights.map(String)
});
const styleMap = {
italic: "i",
oblique: "i",
normal: ""
};
const styles = new Set(options.styles.map((i) => styleMap[i]));
if (weights.length === 0 || styles.size === 0)
return [];
const resolvedVariants = weights.flatMap((w) => [...styles].map((s) => `${w.weight}${s}`));
const css = await fontAPI$2("/css", {
query: {
family: `${id}:${resolvedVariants.join(",")}`
}
});
return extractFontFaceData(css);
}
return {
listFonts() {
return [...familyMap.keys()];
},
async resolveFont(fontFamily, defaults) {
if (!familyMap.has(fontFamily)) {
return;
}
const fonts2 = await ctx.storage.getItem(`bunny:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults));
return { fonts: fonts2 };
}
};
});
const fontAPI$1 = $fetch.create({ baseURL: "https://api.fontshare.com/v2" });
const fontshare = defineFontProvider("fontshare", async (_options, ctx) => {
const fontshareFamilies = /* @__PURE__ */ new Set();
const fonts = await ctx.storage.getItem("fontshare:meta.json", async () => {
const fonts2 = [];
let offset = 0;
let chunk;
do {
chunk = await fontAPI$1("/fonts", {
responseType: "json",
query: {
offset,
limit: 100
}
});
fonts2.push(...chunk.fonts);
offset++;
} while (chunk.has_more);
return fonts2;
});
for (const font of fonts) {
fontshareFamilies.add(font.name);
}
async function getFontDetails(family, options) {
const font = fonts.find((f) => f.name === family);
const numbers = [];
const weights = prepareWeights({
inputWeights: options.weights,
hasVariableWeights: false,
weights: font.styles.map((s) => String(s.weight.weight))
}).map((w) => w.weight);
for (const style of font.styles) {
if (style.is_italic && !options.styles.includes("italic")) {
continue;
}
if (!style.is_italic && !options.styles.includes("normal")) {
continue;
}
if (!weights.includes(String(style.weight.weight))) {
continue;
}
numbers.push(style.weight.number);
}
if (numbers.length === 0)
return [];
const css = await fontAPI$1(`/css?f[]=${`${font.slug}@${numbers.join(",")}`}`);
return extractFontFaceData(css);
}
return {
listFonts() {
return [...fontshareFamilies];
},
async resolveFont(fontFamily, defaults) {
if (!fontshareFamilies.has(fontFamily)) {
return;
}
const fonts2 = await ctx.storage.getItem(`fontshare:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults));
return { fonts: fonts2 };
}
};
});
const fontAPI = $fetch.create({ baseURL: "https://api.fontsource.org/v1" });
const fontsource = defineFontProvider("fontsource", async (_options, ctx) => {
const fonts = await ctx.storage.getItem("fontsource:meta.json", () => fontAPI("/fonts", { responseType: "json" }));
const familyMap = /* @__PURE__ */ new Map();
for (const meta of fonts) {
familyMap.set(meta.family, meta);
}
async function getFontDetails(family, options) {
const font = familyMap.get(family);
const weights = prepareWeights({
inputWeights: options.weights,
hasVariableWeights: font.variable,
weights: font.weights.map(String)
});
const styles = options.styles.filter((style) => font.styles.includes(style));
const subsets = options.subsets ? options.subsets.filter((subset) => font.subsets.includes(subset)) : [font.defSubset];
if (weights.length === 0 || styles.length === 0)
return [];
const fontDetail = await fontAPI(`/fonts/${font.id}`, { responseType: "json" });
const fontFaceData = [];
for (const subset of subsets) {
for (const style of styles) {
for (const { weight, variable } of weights) {
if (variable) {
try {
const variableAxes = await ctx.storage.getItem(`fontsource:${font.family}-axes.json`, () => fontAPI(`/variable/${font.id}`, { responseType: "json" }));
if (variableAxes && variableAxes.axes.wght) {
fontFaceData.push({
style,
weight: [Number(variableAxes.axes.wght.min), Number(variableAxes.axes.wght.max)],
src: [
{ url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: "woff2" }
],
unicodeRange: fontDetail.unicodeRange[subset]?.split(",")
});
}
} catch {
console.error(`Could not download variable axes metadata for \`${font.family}\` from \`fontsource\`. \`unifont\` will not be able to inject variable axes for ${font.family}.`);
}
continue;
}
const variantUrl = fontDetail.variants[weight][style][subset].url;
fontFaceData.push({
style,
weight,
src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })),
unicodeRange: fontDetail.unicodeRange[subset]?.split(",")
});
}
}
}
return fontFaceData;
}
return {
listFonts() {
return [...familyMap.keys()];
},
async resolveFont(fontFamily, options) {
if (!familyMap.has(fontFamily)) {
return;
}
const fonts2 = await ctx.storage.getItem(`fontsource:${fontFamily}-${hash(options)}-data.json`, () => getFontDetails(fontFamily, options));
return { fonts: fonts2 };
}
};
});
function splitCssIntoSubsets(input) {
const data = [];
const comments = [];
const nodes = findAll(
parse(input, {
positions: true,
// Comments are not part of the tree. We rely on the positions to infer the subset
onComment(value, loc) {
comments.push({ value: value.trim(), endLine: loc.end.line });
}
}),
(node) => node.type === "Atrule" && node.name === "font-face"
);
if (comments.length === 0) {
return [{ subset: null, css: input }];
}
for (const node of nodes) {
const comment = comments.filter((comment2) => comment2.endLine < node.loc.start.line).at(-1);
if (!comment)
continue;
data.push({ subset: comment.value, css: generate(node) });
}
return data;
}
const google = defineFontProvider("google", async (_options = {}, ctx) => {
const googleFonts = await ctx.storage.getItem("google:meta.json", () => $fetch("https://fonts.google.com/metadata/fonts", { responseType: "json" }).then((r) => r.familyMetadataList));
const styleMap = {
italic: "1",
oblique: "1",
normal: "0"
};
const userAgents = {
woff2: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
ttf: "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16"
// eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)',
// woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0',
// svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3',
};
async function getFontDetails(family, options) {
const font = googleFonts.find((font2) => font2.family === family);
const styles = [...new Set(options.styles.map((i) => styleMap[i]))].sort();
const glyphs = _options.experimental?.glyphs?.[family]?.join("");
const weights = prepareWeights({
inputWeights: options.weights,
hasVariableWeights: font.axes.some((a) => a.tag === "wght"),
weights: Object.keys(font.fonts)
}).map((v) => v.variable ? {
weight: v.weight.replace(" ", ".."),
variable: v.variable
} : v);
if (weights.length === 0 || styles.length === 0)
return [];
const resolvedAxes = [];
let resolvedVariants = [];
for (const axis of ["wght", "ital", ...Object.keys(_options?.experimental?.variableAxis?.[family] ?? {})].sort(googleFlavoredSorting)) {
const axisValue = {
wght: weights.map((v) => v.weight),
ital: styles
}[axis] ?? _options.experimental.variableAxis[family][axis].map((v) => Array.isArray(v) ? `${v[0]}..${v[1]}` : v);
if (resolvedVariants.length === 0) {
resolvedVariants = axisValue;
} else {
resolvedVariants = resolvedVariants.flatMap((v) => [...axisValue].map((o) => [v, o].join(","))).sort();
}
resolvedAxes.push(axis);
}
let priority = 0;
const resolvedFontFaceData = [];
for (const extension in userAgents) {
const rawCss = await $fetch("/css2", {
baseURL: "https://fonts.googleapis.com",
headers: {
"user-agent": userAgents[extension]
},
query: {
family: `${family}:${resolvedAxes.join(",")}@${resolvedVariants.join(
";"
)}`,
...glyphs && { text: glyphs }
}
});
const groups = splitCssIntoSubsets(rawCss).filter((group) => group.subset ? options.subsets.includes(group.subset) : true);
for (const group of groups) {
const data = extractFontFaceData(group.css);
data.map((f) => {
f.meta ??= {};
f.meta.priority = priority;
return f;
});
resolvedFontFaceData.push(...data);
}
priority++;
}
return resolvedFontFaceData;
}
return {
listFonts() {
return googleFonts.map((font) => font.family);
},
async resolveFont(fontFamily, options) {
if (!googleFonts.some((font) => font.family === fontFamily)) {
return;
}
const fonts = await ctx.storage.getItem(`google:${fontFamily}-${hash(options)}-data.json`, () => getFontDetails(fontFamily, options));
return { fonts };
}
};
});
function googleFlavoredSorting(a, b) {
const isALowercase = a.charAt(0) === a.charAt(0).toLowerCase();
const isBLowercase = b.charAt(0) === b.charAt(0).toLowerCase();
if (isALowercase !== isBLowercase) {
return Number(isBLowercase) - Number(isALowercase);
} else {
return a.localeCompare(b);
}
}
const googleicons = defineFontProvider("googleicons", async (_options, ctx) => {
const googleIcons = await ctx.storage.getItem("googleicons:meta.json", async () => {
const response = JSON.parse((await $fetch(
"https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true"
)).split("\n").slice(1).join("\n"));
return response.families;
});
const userAgents = {
woff2: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
ttf: "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16"
// eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)',
// woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0',
// svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3',
};
async function getFontDetails(family) {
const iconNames = _options.experimental?.glyphs?.[family]?.sort().join(",");
let css = "";
for (const extension in userAgents) {
if (family.includes("Icons")) {
css += await $fetch("/icon", {
baseURL: "https://fonts.googleapis.com",
headers: { "user-agent": userAgents[extension] },
query: {
family
}
});
} else {
css += await $fetch("/css2", {
baseURL: "https://fonts.googleapis.com",
headers: { "user-agent": userAgents[extension] },
query: {
family: `${family}:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200`,
...iconNames && { icon_names: iconNames }
}
});
}
}
return extractFontFaceData(css);
}
return {
listFonts() {
return googleIcons;
},
async resolveFont(fontFamily, options) {
if (!googleIcons.includes(fontFamily)) {
return;
}
const fonts = await ctx.storage.getItem(`googleicons:${fontFamily}-${hash(options)}-data.json`, () => getFontDetails(fontFamily));
return { fonts };
}
};
});
const providers = {
__proto__: null,
adobe: adobe,
bunny: bunny,
fontshare: fontshare,
fontsource: fontsource,
google: google,
googleicons: googleicons
};
const defaultResolveOptions = {
weights: ["400"],
styles: ["normal", "italic"],
subsets: [
"cyrillic-ext",
"cyrillic",
"greek-ext",
"greek",
"vietnamese",
"latin-ext",
"latin"
]
};
async function createUnifont(providers2, options) {
const stack = {};
const unifontContext = {
storage: createAsyncStorage(options?.storage ?? memoryStorage())
};
for (const provider of providers2) {
stack[provider._name] = void 0;
}
await Promise.all(providers2.map(async (provider) => {
try {
const initializedProvider = await provider(unifontContext);
if (initializedProvider)
stack[provider._name] = initializedProvider;
} catch (err) {
console.error(`Could not initialize provider \`${provider._name}\`. \`unifont\` will not be able to process fonts provided by this provider.`, err);
}
if (!stack[provider._name]?.resolveFont) {
delete stack[provider._name];
}
}));
const allProviders = Object.keys(stack);
async function resolveFont(fontFamily, options2, providers3 = allProviders) {
const mergedOptions = { ...defaultResolveOptions, ...options2 };
for (const id of providers3) {
const provider = stack[id];
try {
const result = await provider?.resolveFont(fontFamily, mergedOptions);
if (result) {
return {
provider: id,
...result
};
}
} catch (err) {
console.error(`Could not resolve font face for \`${fontFamily}\` from \`${id}\` provider.`, err);
}
}
return { fonts: [] };
}
async function listFonts(providers3 = allProviders) {
let names;
for (const id of providers3) {
const provider = stack[id];
try {
const result = await provider?.listFonts?.();
if (result) {
names ??= [];
names.push(...result);
}
} catch (err) {
console.error(`Could not list names from \`${id}\` provider.`, err);
}
}
return names;
}
return {
resolveFont,
// TODO: remove before v1
resolveFontFace: resolveFont,
listFonts
};
}
export { createUnifont, defaultResolveOptions, defineFontProvider, providers };