feat: init
This commit is contained in:
409
node_modules/google-fonts-helper/dist/index.mjs
generated
vendored
Normal file
409
node_modules/google-fonts-helper/dist/index.mjs
generated
vendored
Normal file
@@ -0,0 +1,409 @@
|
||||
import { withHttps, withQuery, resolveURL } from 'ufo';
|
||||
import { existsSync, readFileSync, copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { resolve, dirname, extname, posix } from 'node:path';
|
||||
import { ofetch } from 'ofetch';
|
||||
import { Hookable } from 'hookable';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
const GOOGLE_FONTS_DOMAIN = "fonts.googleapis.com";
|
||||
function isValidDisplay(display) {
|
||||
return ["auto", "block", "swap", "fallback", "optional"].includes(display);
|
||||
}
|
||||
function parseStyle(style) {
|
||||
if (["wght", "normal", "regular"].includes(style.toLowerCase())) {
|
||||
return "wght";
|
||||
}
|
||||
if (["ital", "italic", "i"].includes(style.toLowerCase())) {
|
||||
return "ital";
|
||||
}
|
||||
return style;
|
||||
}
|
||||
function cartesianProduct(...a) {
|
||||
return a.length < 2 ? a : a.reduce((a2, b) => a2.flatMap((d) => b.map((e) => [d, e].flat())));
|
||||
}
|
||||
function parseFamilyName(name) {
|
||||
return decodeURIComponent(name).replace(/\+/g, " ");
|
||||
}
|
||||
|
||||
function constructURL({
|
||||
families,
|
||||
display,
|
||||
subsets,
|
||||
text
|
||||
} = {}) {
|
||||
const _subsets = (Array.isArray(subsets) ? subsets : [subsets]).filter(Boolean);
|
||||
const family = convertFamiliesToArray(families ?? {});
|
||||
if (family.length < 1) {
|
||||
return false;
|
||||
}
|
||||
const query = {
|
||||
family
|
||||
};
|
||||
if (display && isValidDisplay(display)) {
|
||||
query.display = display;
|
||||
}
|
||||
if (_subsets.length > 0) {
|
||||
query.subset = _subsets.join(",");
|
||||
}
|
||||
if (text) {
|
||||
query.text = text;
|
||||
}
|
||||
return withHttps(withQuery(resolveURL(GOOGLE_FONTS_DOMAIN, "css2"), query));
|
||||
}
|
||||
function convertFamiliesToArray(families) {
|
||||
const result = [];
|
||||
Object.entries(families).forEach(([name, values]) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
name = parseFamilyName(name);
|
||||
if (typeof values === "string" && String(values).includes("..")) {
|
||||
result.push(`${name}:wght@${values}`);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(values) && values.length > 0) {
|
||||
result.push(`${name}:wght@${values.join(";")}`);
|
||||
return;
|
||||
}
|
||||
if (Object.keys(values).length > 0) {
|
||||
const axes = {};
|
||||
let italicWeights = [];
|
||||
Object.entries(values).sort(([styleA], [styleB]) => styleA.localeCompare(styleB)).forEach(([style, weight]) => {
|
||||
const parsedStyle = parseStyle(style);
|
||||
if (parsedStyle === "ital") {
|
||||
axes[parsedStyle] = ["0", "1"];
|
||||
if (weight === true || weight === 400 || weight === 1) {
|
||||
italicWeights = ["*"];
|
||||
} else {
|
||||
italicWeights = Array.isArray(weight) ? weight.map((w) => String(w)) : [weight];
|
||||
}
|
||||
} else {
|
||||
axes[parseStyle(style)] = Array.isArray(weight) ? weight.map((w) => String(w)) : [weight];
|
||||
}
|
||||
});
|
||||
const strictlyItalic = [];
|
||||
if (Object.keys(axes).length === 1 && Object.hasOwn(axes, "ital")) {
|
||||
if (!(italicWeights.includes("*") || italicWeights.length === 1 && italicWeights.includes("400"))) {
|
||||
axes.wght = italicWeights;
|
||||
strictlyItalic.push(...italicWeights);
|
||||
}
|
||||
} else if (Object.hasOwn(axes, "wght") && !italicWeights.includes("*")) {
|
||||
strictlyItalic.push(...italicWeights.filter((w) => !axes.wght.includes(w)));
|
||||
axes.wght = [.../* @__PURE__ */ new Set([...axes.wght, ...italicWeights])];
|
||||
}
|
||||
const axisTagList = Object.keys(axes).sort((axisA, axisB) => {
|
||||
const isLowerA = axisA[0] === axisA[0].toLowerCase();
|
||||
const isLowerB = axisB[0] === axisB[0].toLowerCase();
|
||||
if (isLowerA && !isLowerB) {
|
||||
return -1;
|
||||
}
|
||||
if (!isLowerA && isLowerB) {
|
||||
return 1;
|
||||
}
|
||||
return axisA.localeCompare(axisB);
|
||||
});
|
||||
if (axisTagList.length === 1 && axisTagList.includes("ital")) {
|
||||
result.push(`${name}:ital@1`);
|
||||
return;
|
||||
}
|
||||
let axisTupleArrays = cartesianProduct(...axisTagList.map((tag) => axes[tag]), [[]]);
|
||||
const italicIndex = axisTagList.findIndex((i) => i === "ital");
|
||||
if (italicIndex !== -1) {
|
||||
const weightIndex = axisTagList.findIndex((i) => i === "wght");
|
||||
if (weightIndex !== -1) {
|
||||
axisTupleArrays = axisTupleArrays.filter((axisTuple) => axisTuple[italicIndex] === "0" && !strictlyItalic.includes(axisTuple[weightIndex]) || axisTuple[italicIndex] === "1" && italicWeights.includes(axisTuple[weightIndex]));
|
||||
}
|
||||
}
|
||||
const axisTupleList = axisTupleArrays.sort((axisTupleA, axisTupleB) => {
|
||||
for (let i = 0; i < axisTupleA.length; i++) {
|
||||
const compareResult = parseInt(axisTupleA[i]) - parseInt(axisTupleB[i]);
|
||||
if (compareResult !== 0) {
|
||||
return compareResult;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}).map((axisTuple) => axisTuple.join(",")).join(";");
|
||||
result.push(`${name}:${axisTagList.join(",")}@${axisTupleList}`);
|
||||
return;
|
||||
}
|
||||
if (values) {
|
||||
result.push(name);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function isValidURL(url) {
|
||||
return RegExp(GOOGLE_FONTS_DOMAIN).test(url);
|
||||
}
|
||||
|
||||
var __defProp = Object.defineProperty;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __publicField = (obj, key, value) => {
|
||||
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||||
return value;
|
||||
};
|
||||
class Downloader extends Hookable {
|
||||
constructor(url, options) {
|
||||
super();
|
||||
this.url = url;
|
||||
__publicField(this, "config");
|
||||
this.config = {
|
||||
base64: false,
|
||||
overwriting: false,
|
||||
outputDir: "./",
|
||||
stylePath: "fonts.css",
|
||||
fontsDir: "fonts",
|
||||
fontsPath: "./fonts",
|
||||
headers: [["user-agent", [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||
"Chrome/98.0.4758.102 Safari/537.36"
|
||||
].join(" ")]],
|
||||
...options
|
||||
};
|
||||
}
|
||||
async execute() {
|
||||
if (!isValidURL(this.url)) {
|
||||
throw new Error("Invalid Google Fonts URL");
|
||||
}
|
||||
const { outputDir, stylePath, headers, fontsPath } = this.config;
|
||||
const cssPath = resolve(outputDir, stylePath);
|
||||
let overwriting = this.config.overwriting;
|
||||
if (!overwriting && existsSync(cssPath)) {
|
||||
const currentCssContent = readFileSync(cssPath, "utf-8");
|
||||
const currentUrl = (currentCssContent.split(/\r?\n/, 1).shift() || "").replace("/*", "").replace("*/", "").trim();
|
||||
if (currentUrl === this.url) {
|
||||
return false;
|
||||
}
|
||||
overwriting = true;
|
||||
}
|
||||
await this.callHook("download:start");
|
||||
const { searchParams } = new URL(this.url);
|
||||
const subsets = searchParams.get("subset") ? searchParams.get("subset")?.split(",") : void 0;
|
||||
await this.callHook("download-css:before", this.url);
|
||||
const _css = await ofetch(this.url, { headers });
|
||||
const { fonts: fontsFromCss, css: cssContent } = parseFontsFromCss(_css, fontsPath, subsets);
|
||||
await this.callHook("download-css:done", this.url, cssContent, fontsFromCss);
|
||||
const fonts = await this.downloadFonts(fontsFromCss);
|
||||
await this.callHook("write-css:before", cssPath, cssContent, fonts);
|
||||
const newContent = this.writeCss(cssPath, `/* ${this.url} */
|
||||
${cssContent}`, fonts);
|
||||
await this.callHook("write-css:done", cssPath, newContent, cssContent);
|
||||
await this.callHook("download:complete");
|
||||
return true;
|
||||
}
|
||||
async downloadFonts(fonts) {
|
||||
const { headers, base64, outputDir, fontsDir } = this.config;
|
||||
const downloadedFonts = [];
|
||||
const _fonts = [];
|
||||
for (const font of fonts) {
|
||||
const downloadedFont = downloadedFonts.find((f) => f.inputFont === font.inputFont);
|
||||
if (downloadedFont) {
|
||||
if (base64) {
|
||||
font.outputText = downloadedFont.outputText;
|
||||
} else {
|
||||
copyFileSync(
|
||||
resolve(outputDir, fontsDir, downloadedFont.outputFont),
|
||||
resolve(outputDir, fontsDir, font.outputFont)
|
||||
);
|
||||
}
|
||||
_fonts.push(font);
|
||||
continue;
|
||||
}
|
||||
await this.callHook("download-font:before", font);
|
||||
const response = await ofetch.raw(font.inputFont, { headers, responseType: "arrayBuffer" });
|
||||
if (!response?._data) {
|
||||
_fonts.push(font);
|
||||
continue;
|
||||
}
|
||||
const buffer = Buffer.from(response?._data);
|
||||
if (base64) {
|
||||
const mime = response.headers.get("content-type") ?? "font/woff2";
|
||||
font.outputText = `url('data:${mime};base64,${buffer.toString("base64")}')`;
|
||||
} else {
|
||||
const fontPath = resolve(outputDir, fontsDir, font.outputFont);
|
||||
mkdirSync(dirname(fontPath), { recursive: true });
|
||||
writeFileSync(fontPath, buffer);
|
||||
}
|
||||
_fonts.push(font);
|
||||
await this.callHook("download-font:done", font);
|
||||
downloadedFonts.push(font);
|
||||
}
|
||||
return _fonts;
|
||||
}
|
||||
writeCss(path, content, fonts) {
|
||||
for (const font of fonts) {
|
||||
content = content.replace(font.inputText, font.outputText);
|
||||
}
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, content, "utf-8");
|
||||
return content;
|
||||
}
|
||||
}
|
||||
function parseFontsFromCss(content, fontsPath, subsets) {
|
||||
const css = [];
|
||||
const fonts = [];
|
||||
const re = {
|
||||
face: /\s*(?:\/\*\s*(.*?)\s*\*\/)?[^@]*?@font-face\s*{(?:[^}]*?)}\s*/gi,
|
||||
family: /font-family\s*:\s*(?:'|")?([^;]*?)(?:'|")?\s*;/i,
|
||||
style: /font-style\s*:\s*([^;]*?)\s*;/i,
|
||||
weight: /font-weight\s*:\s*([^;]*?)\s*;/i,
|
||||
url: /url\s*\(\s*(?:'|")?\s*([^]*?)\s*(?:'|")?\s*\)\s*?/gi
|
||||
};
|
||||
let match1;
|
||||
while ((match1 = re.face.exec(content)) !== null) {
|
||||
const [fontface, subset] = match1;
|
||||
const familyRegExpArray = re.family.exec(fontface);
|
||||
const family = familyRegExpArray ? familyRegExpArray[1] : "";
|
||||
const styleRegExpArray = re.style.exec(fontface);
|
||||
const style = styleRegExpArray ? styleRegExpArray[1] : "";
|
||||
const weightRegExpArray = re.weight.exec(fontface);
|
||||
const weight = weightRegExpArray ? weightRegExpArray[1] : "";
|
||||
if (subsets && subsets.length && !subsets.includes(subset)) {
|
||||
continue;
|
||||
}
|
||||
css.push(fontface);
|
||||
let match2;
|
||||
while ((match2 = re.url.exec(fontface)) !== null) {
|
||||
const [forReplace, url] = match2;
|
||||
const ext = extname(url).replace(/^\./, "") || "woff2";
|
||||
const newFilename = formatFontFileName("{family}-{style}-{weight}-{subset}.{ext}", {
|
||||
family: family.replace(/\s+/g, "_"),
|
||||
style: style.replace(/\s+/g, "_") || "normal",
|
||||
weight: weight.replace(/\s+/g, "_") || "",
|
||||
subset: subset || "text",
|
||||
ext
|
||||
}).replace(/\.$/, "");
|
||||
fonts.push({
|
||||
inputFont: url,
|
||||
outputFont: newFilename,
|
||||
inputText: forReplace,
|
||||
outputText: `url('${posix.join(fontsPath, newFilename)}')`
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
css: css.join("\n"),
|
||||
fonts
|
||||
};
|
||||
}
|
||||
function formatFontFileName(template, values) {
|
||||
return Object.entries(values).filter(([key]) => /^[a-z0-9_-]+$/gi.test(key)).map(([key, value]) => [new RegExp(`([^{]|^){${key}}([^}]|$)`, "g"), `$1${value}$2`]).reduce((str, [regexp, replacement]) => str.replace(regexp, String(replacement)), template).replace(/({|}){2}/g, "$1");
|
||||
}
|
||||
|
||||
function download(url, options) {
|
||||
return new Downloader(url, options);
|
||||
}
|
||||
|
||||
function merge(...fonts) {
|
||||
return deepmerge.all(fonts);
|
||||
}
|
||||
|
||||
function parse(url) {
|
||||
const result = {};
|
||||
if (!isValidURL(url)) {
|
||||
return result;
|
||||
}
|
||||
const { searchParams, pathname } = new URL(url);
|
||||
if (!searchParams.has("family")) {
|
||||
return result;
|
||||
}
|
||||
const families = convertFamiliesObject(searchParams.getAll("family"), pathname.endsWith("2"));
|
||||
if (Object.keys(families).length < 1) {
|
||||
return result;
|
||||
}
|
||||
result.families = families;
|
||||
const display = searchParams.get("display");
|
||||
if (display && isValidDisplay(display)) {
|
||||
result.display = display;
|
||||
}
|
||||
const subsets = searchParams.get("subset");
|
||||
if (subsets) {
|
||||
result.subsets = subsets.split(",");
|
||||
}
|
||||
const text = searchParams.get("text");
|
||||
if (text) {
|
||||
result.text = text;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function convertFamiliesObject(families, v2 = true) {
|
||||
const result = {};
|
||||
families.flatMap((family) => family.split("|")).forEach((family) => {
|
||||
if (!family) {
|
||||
return;
|
||||
}
|
||||
if (!family.includes(":")) {
|
||||
result[family] = true;
|
||||
return;
|
||||
}
|
||||
const parts = family.split(":");
|
||||
if (!parts[1]) {
|
||||
return;
|
||||
}
|
||||
const values = {};
|
||||
if (!v2) {
|
||||
parts[1].split(",").forEach((style) => {
|
||||
const styleParsed = parseStyle(style);
|
||||
if (styleParsed === "wght") {
|
||||
values.wght = true;
|
||||
}
|
||||
if (styleParsed === "ital") {
|
||||
values.ital = true;
|
||||
}
|
||||
if (styleParsed === "bold" || styleParsed === "b") {
|
||||
values.wght = 700;
|
||||
}
|
||||
if (styleParsed === "bolditalic" || styleParsed === "bi") {
|
||||
values.ital = 700;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (v2) {
|
||||
let [styles, weights] = parts[1].split("@");
|
||||
if (!weights) {
|
||||
weights = String(styles).replace(",", ";");
|
||||
styles = "wght";
|
||||
}
|
||||
styles.split(",").forEach((style) => {
|
||||
const styleParsed = parseStyle(style);
|
||||
values[styleParsed] = weights.split(";").map((weight) => {
|
||||
if (/^\+?\d+$/.test(weight)) {
|
||||
return parseInt(weight);
|
||||
}
|
||||
const [pos, w] = weight.split(",");
|
||||
const index = styleParsed === "wght" ? 0 : 1;
|
||||
if (!w) {
|
||||
return weight;
|
||||
}
|
||||
if (parseInt(pos) !== index) {
|
||||
return 0;
|
||||
}
|
||||
if (/^\+?\d+$/.test(w)) {
|
||||
return parseInt(w);
|
||||
}
|
||||
return w;
|
||||
}).filter((v) => parseInt(v.toString()) > 0 || v.toString().includes(".."));
|
||||
if (!values[styleParsed].length) {
|
||||
values[styleParsed] = true;
|
||||
return;
|
||||
}
|
||||
if (values[styleParsed].length > 1) {
|
||||
return;
|
||||
}
|
||||
const first = values[styleParsed][0];
|
||||
if (String(first).includes("..")) {
|
||||
values[styleParsed] = first;
|
||||
}
|
||||
if (first === 1 || first === true) {
|
||||
values[styleParsed] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
result[parseFamilyName(parts[0])] = values;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export { Downloader, constructURL, download, isValidURL, merge, parse };
|
||||
Reference in New Issue
Block a user