fantasticon
Version:
Icon font generation tool
671 lines (639 loc) • 21.8 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
ASSET_TYPES: () => ASSET_TYPES,
FontAssetType: () => FontAssetType,
OtherAssetType: () => OtherAssetType,
default: () => generateFonts,
generateFonts: () => generateFonts
});
module.exports = __toCommonJS(index_exports);
// src/types/misc.ts
var FontAssetType = /* @__PURE__ */ ((FontAssetType2) => {
FontAssetType2["EOT"] = "eot";
FontAssetType2["WOFF2"] = "woff2";
FontAssetType2["WOFF"] = "woff";
FontAssetType2["TTF"] = "ttf";
FontAssetType2["SVG"] = "svg";
return FontAssetType2;
})(FontAssetType || {});
var OtherAssetType = /* @__PURE__ */ ((OtherAssetType3) => {
OtherAssetType3["CSS"] = "css";
OtherAssetType3["SCSS"] = "scss";
OtherAssetType3["SASS"] = "sass";
OtherAssetType3["HTML"] = "html";
OtherAssetType3["JSON"] = "json";
OtherAssetType3["TS"] = "ts";
return OtherAssetType3;
})(OtherAssetType || {});
var ASSET_TYPES_WITH_TEMPLATE = [
"css" /* CSS */,
"html" /* HTML */,
"scss" /* SCSS */,
"sass" /* SASS */
];
var ASSET_TYPES = { ...FontAssetType, ...OtherAssetType };
// src/constants.ts
var import_path3 = require("path");
// src/utils/path.ts
var import_fs = require("fs");
var import_path = require("path");
var import_url = require("url");
var import_meta = {};
var MAX_LOOKUP_DEPTH = 5;
var getFileName = () => typeof __filename !== "undefined" ? __filename : (0, import_url.fileURLToPath)(import_meta.url);
var getDirName = () => (0, import_path.dirname)(getFileName());
var removeExtension = (path) => path.includes(".") ? path.split(".").slice(0, -1).join(".") : path;
var splitSegments = (path) => (0, import_path.normalize)(path).split(/\/|\\/).filter((part) => part && part !== ".");
var getRoot = () => {
let current = getDirName();
for (let i = 0; i < MAX_LOOKUP_DEPTH; i++) {
const pkg = (0, import_path.join)(current, "package.json");
if ((0, import_fs.existsSync)(pkg)) return (0, import_path.resolve)(current);
const parent = (0, import_path.dirname)(current);
current = parent;
}
throw new Error("Module root not found");
};
// src/utils/icon-id.ts
var import_slugify = __toESM(require("slugify"), 1);
var getIconId = ({ relativeFilePath }) => (0, import_slugify.default)(removeExtension(relativeFilePath).replace(/(\/|\\|\.)+/g, "-"), {
replacement: "-",
remove: /['"`]/g
});
// src/constants.ts
var TEMPLATES_DIR = (0, import_path3.resolve)(getRoot(), "templates");
var DEFAULT_OPTIONS = {
name: "icons",
fontTypes: ["eot" /* EOT */, "woff2" /* WOFF2 */, "woff" /* WOFF */],
assetTypes: [
"css" /* CSS */,
"html" /* HTML */,
"json" /* JSON */,
"ts" /* TS */
],
formatOptions: { json: { indent: 4 } },
pathOptions: {},
templates: {},
codepoints: {},
round: void 0,
fontHeight: 300,
descent: void 0,
normalize: void 0,
selector: null,
tag: "i",
prefix: "icon",
fontsUrl: void 0,
getIconId
};
var DEFAULT_START_CODEPOINT = 61697;
// src/utils/assets.ts
var import_glob = require("glob");
var import_path5 = require("path");
// src/utils/fs-async.ts
var import_util = require("util");
var fs = __toESM(require("fs"), 1);
var readFile2 = (0, import_util.promisify)(fs.readFile);
var writeFile2 = (0, import_util.promisify)(fs.writeFile);
var stat2 = (0, import_util.promisify)(fs.stat);
var checkPath = async (filepath, type) => {
try {
const result = await stat2(filepath);
if (type) {
return type === "directory" ? result.isDirectory() : result.isFile();
}
return true;
} catch (err) {
return false;
}
};
// src/utils/assets.ts
var ASSETS_EXTENSION = "svg";
var loadPaths = async (dir) => {
const globPath = (0, import_path5.join)(dir, `**/*.${ASSETS_EXTENSION}`);
const files = await (0, import_glob.glob)(globPath, {});
if (!files.length) {
throw new Error(`No SVGs found in ${dir}`);
}
return files;
};
var failForConflictingId = ({ relativePath: pathA, id }, { relativePath: pathB }) => {
throw new Error(
`Conflicting result from 'getIconId': '${id}' - conflicting input files:
` + [pathA, pathB].map((fpath) => ` - ${fpath}`).join("\n")
);
};
var loadAssets = async ({
inputDir,
getIconId: getIconId2
}) => {
const paths = await loadPaths(inputDir);
const out = {};
let index = 0;
for (const path of paths) {
const relativePath = (0, import_path5.relative)((0, import_path5.resolve)(inputDir), (0, import_path5.resolve)(path));
const parts = splitSegments(relativePath);
const basename = removeExtension(parts.pop());
const absolutePath = (0, import_path5.resolve)(path);
const iconId = getIconId2({
basename,
relativeDirPath: (0, import_path5.join)(...parts),
absoluteFilePath: absolutePath,
relativeFilePath: relativePath,
index
});
const result = { id: iconId, relativePath, absolutePath };
if (out[iconId]) {
failForConflictingId(out[iconId], result);
}
out[iconId] = result;
index++;
}
return out;
};
var writeAssets = async (assets, { name, pathOptions = {}, outputDir }) => {
const results = [];
for (const ext of Object.keys(assets)) {
const filename = [name, ext].join(".");
const writePath = pathOptions[ext] || (0, import_path5.join)(outputDir, filename);
results.push({ content: assets[ext], writePath });
await writeFile2(writePath, assets[ext]);
}
return results;
};
// src/generators/generator-options.ts
var import_path7 = require("path");
// src/utils/codepoints.ts
var getCodepoints = (assets, predefined = {}, start = DEFAULT_START_CODEPOINT) => {
const out = {};
const used = Object.values(predefined);
let current = start;
const getNextCodepoint = () => {
while (used.includes(current)) {
current++;
}
const res = current;
current++;
return res;
};
for (const id of Object.keys(assets)) {
if (!predefined[id]) {
out[id] = getNextCodepoint();
}
}
return { ...predefined, ...out };
};
var getHexCodepoint = (decimalCodepoint) => decimalCodepoint.toString(16);
// src/generators/generator-options.ts
var getGeneratorOptions = (options, assets) => ({
...options,
codepoints: getCodepoints(assets, options.codepoints),
formatOptions: prefillOptions(
Object.values(ASSET_TYPES),
options.formatOptions,
(assetType) => DEFAULT_OPTIONS.formatOptions[assetType] || {}
),
templates: prefillOptions(
ASSET_TYPES_WITH_TEMPLATE,
options.templates,
(assetType) => (0, import_path7.join)(TEMPLATES_DIR, `${assetType}.hbs`)
),
assets
});
var prefillOptions = (keys, userOptions = {}, getDefault) => keys.reduce(
(cur = {}, type) => ({
...cur,
[type]: mergeOptions(userOptions[type], getDefault(type))
}),
{}
);
var mergeOptions = (input, defaultVal) => {
if (typeof defaultVal === "object") {
return { ...defaultVal, ...input };
}
return input || defaultVal;
};
// src/utils/validation.ts
var parseNumeric = (value) => {
const out = Number(value);
if (typeof value !== "number" && typeof value !== "string" || Number.isNaN(out)) {
throw new Error(`${value} is not a valid number`);
}
return out;
};
var parseString = (value) => {
if (typeof value !== "string") {
throw new Error(`${value} is not a string`);
}
return value;
};
var parseFunction = (value) => {
if (typeof value !== "function") {
throw new Error(`${value} is not a function`);
}
return value;
};
var listMembersParser = (allowedValues) => (values) => {
for (const value of values) {
if (!allowedValues.includes(value)) {
throw new Error(
[
`${value} is not valid`,
`accepted values are: ${allowedValues.join(", ")}`
].join(" - ")
);
}
}
return values;
};
var parseBoolean = (val) => {
if (typeof val === "string" && ["1", "0", "true", "false"].includes(val)) {
return val === "true" || val === "1";
} else if (typeof val === "number" && [0, 1].includes(val)) {
return val === 1;
} else if (typeof val === "boolean") {
return val;
}
throw new Error(`must be a boolean value`);
};
var skipIfMatching = (match) => (fn) => (val) => val === match ? match : fn(val);
var parseDir = async (dirname2) => {
if (await checkPath(dirname2, "directory") === false) {
throw new Error(`${dirname2} is not a directory`);
}
return dirname2;
};
var nullable = skipIfMatching(null);
var optional = skipIfMatching(void 0);
// src/core/config-parser.ts
var CONFIG_VALIDATORS = {
inputDir: [optional(parseString), optional(parseDir)],
outputDir: [optional(parseString), optional(parseDir)],
name: [optional(parseString)],
fontTypes: [listMembersParser(Object.values(FontAssetType))],
assetTypes: [listMembersParser(Object.values(OtherAssetType))],
formatOptions: [],
pathOptions: [],
templates: [],
codepoints: [],
fontHeight: [optional(parseNumeric)],
descent: [optional(parseNumeric)],
normalize: [optional(parseBoolean)],
round: [optional(parseNumeric)],
selector: [nullable(parseString)],
tag: [parseString],
prefix: [parseString],
fontsUrl: [optional(parseString)],
getIconId: [optional(parseFunction)]
};
var parseConfig = async (input = {}) => {
const options = { ...DEFAULT_OPTIONS, ...input };
const out = {};
const allkeys = [
.../* @__PURE__ */ new Set([...Object.keys(options), ...Object.keys(CONFIG_VALIDATORS)])
];
for (const key of allkeys) {
const validators = CONFIG_VALIDATORS[key];
if (!validators) {
throw new Error(`The option '${key}' is not recognised`);
}
let val = options[key];
try {
for (const fn of validators) {
val = await fn(val, val);
}
} catch (err) {
throw new Error(`Invalid option ${key}: ${err.message}`);
}
out[key] = val;
}
return out;
};
// src/generators/asset-types/svg.ts
var import_fs2 = require("fs");
var import_svgicons2svgfont = require("svgicons2svgfont");
var generator = {
generate: ({
name: fontName,
fontHeight,
descent,
normalize: normalize2,
assets,
codepoints,
formatOptions: { svg } = {}
}) => new Promise((resolve5) => {
let font = Buffer.alloc(0);
const fontStream = new import_svgicons2svgfont.SVGIcons2SVGFontStream({
fontName,
fontHeight,
descent,
normalize: normalize2,
// log: () => null,
...svg
}).on("data", (data) => font = Buffer.concat([font, Buffer.from(data)])).on("end", () => resolve5(font.toString()));
for (const { id, absolutePath } of Object.values(assets)) {
const glyph = (0, import_fs2.createReadStream)(absolutePath);
const unicode = String.fromCharCode(codepoints[id]);
glyph.metadata = { name: id, unicode: [unicode] };
fontStream.write(glyph);
}
fontStream.end();
})
};
var svg_default = generator;
// src/generators/asset-types/ttf.ts
var import_svg2ttf = __toESM(require("svg2ttf"), 1);
var generator2 = {
dependsOn: "svg" /* SVG */,
async generate({ formatOptions }, svg) {
const font = (0, import_svg2ttf.default)(svg, { ts: 0, ...formatOptions?.ttf || {} });
return Buffer.from(font.buffer);
}
};
var ttf_default = generator2;
// src/generators/asset-types/woff.ts
var import_ttf2woff = __toESM(require("ttf2woff"), 1);
var generator3 = {
dependsOn: "ttf" /* TTF */,
async generate({ formatOptions }, ttf) {
const font = (0, import_ttf2woff.default)(new Uint8Array(ttf), formatOptions?.woff);
return Buffer.from(font.buffer);
}
};
var woff_default = generator3;
// src/generators/asset-types/woff2.ts
var generator4 = {
dependsOn: "ttf" /* TTF */,
async generate(_options, ttf) {
const font = (await import("ttf2woff2")).default(ttf);
return Buffer.from(font.buffer);
}
};
var woff2_default = generator4;
// src/generators/asset-types/eot.ts
var import_ttf2eot = __toESM(require("ttf2eot"), 1);
var generator5 = {
dependsOn: "ttf" /* TTF */,
async generate(_options, ttf) {
const font = (0, import_ttf2eot.default)(new Uint8Array(ttf));
return Buffer.from(font.buffer);
}
};
var eot_default = generator5;
// src/utils/template.ts
var import_handlebars = __toESM(require("handlebars"), 1);
var import_path8 = require("path");
var TEMPLATE_HELPERS = {
codepoint: getHexCodepoint
};
var renderTemplate = async (templatePath, context, options = {}) => {
const absoluteTemplatePath = (0, import_path8.isAbsolute)(templatePath) ? templatePath : (0, import_path8.resolve)(process.cwd(), templatePath);
const template = await readFile2(absoluteTemplatePath, "utf8");
return import_handlebars.default.compile(template)(context, {
...options,
helpers: { ...TEMPLATE_HELPERS, ...options.helpers || {} }
});
};
// src/utils/hash.ts
var import_crypto = __toESM(require("crypto"), 1);
var getHash = (...contents) => {
const hash = import_crypto.default.createHash("md5");
for (const content of contents) {
hash.update(content);
}
return hash.digest("hex");
};
// src/utils/css.ts
var renderSrcOptions = {
["eot" /* EOT */]: {
formatValue: "embedded-opentype",
getSuffix: () => "#iefix"
},
["woff2" /* WOFF2 */]: { formatValue: "woff2" },
["woff" /* WOFF */]: { formatValue: "woff" },
["ttf" /* TTF */]: { formatValue: "truetype" },
["svg" /* SVG */]: { formatValue: "svg", getSuffix: (name) => `#${name}` }
};
var renderSrcAttribute = ({ name, fontTypes, fontsUrl }, font, { inline = false } = {}) => fontTypes.map((fontType) => {
const { formatValue, getSuffix } = renderSrcOptions[fontType];
const hash = getHash(font.toString("utf8"));
const suffix = getSuffix ? getSuffix(name) : "";
return `url("${fontsUrl || "."}/${name}.${fontType}?${hash}${suffix}") format("${formatValue}")`;
}).join(inline ? ", " : ",\n");
// src/generators/asset-types/css.ts
var generator6 = {
dependsOn: "svg" /* SVG */,
generate: (options, svg) => renderTemplate(options.templates.css, {
...options,
fontSrc: renderSrcAttribute(options, svg)
})
};
var css_default = generator6;
// src/generators/asset-types/html.ts
var generator7 = {
generate: (options) => renderTemplate(options.templates.html, options)
};
var html_default = generator7;
// src/generators/asset-types/json.ts
var generator8 = {
generate: async ({ formatOptions: { json } = {}, codepoints }) => JSON.stringify(codepoints, null, json?.indent)
};
var json_default = generator8;
// src/generators/asset-types/ts.ts
var import_case = __toESM(require("case"), 1);
var generateEnumKeys = (assetKeys) => assetKeys.map((name) => {
const enumName = import_case.default.pascal(name);
const prefix = enumName.match(/^\d/) ? "i" : "";
return {
[name]: `${prefix}${enumName}`
};
}).reduce((prev, curr) => Object.assign(prev, curr), {});
var generateEnums = (enumName, enumKeys, quote = '"') => [
`export enum ${enumName} {`,
...Object.entries(enumKeys).map(
([enumValue, enumKey]) => ` ${enumKey} = ${quote}${enumValue}${quote},`
),
"}\n"
].join("\n");
var generateConstant = ({
constantName,
enumName,
literalIdName,
literalKeyName,
enumKeys,
codepoints,
quote = '"',
kind = {}
}) => {
let varType = ": Record<string, string>";
if (kind.enum) {
varType = `: { [key in ${enumName}]: string }`;
} else if (kind.literalId) {
varType = `: { [key in ${literalIdName}]: string }`;
} else if (kind.literalKey) {
varType = `: { [key in ${literalKeyName}]: string }`;
}
return [
`export const ${constantName}${varType} = {`,
Object.entries(enumKeys).map(([enumValue, enumKey]) => {
const key = kind.enum ? `[${enumName}.${enumKey}]` : `${quote}${enumValue}${quote}`;
return ` ${key}: ${quote}${codepoints[enumValue]}${quote},`;
}).join("\n"),
"};\n"
].join("\n");
};
var generateStringLiterals = (typeName, literals, quote = '"') => [
`export type ${typeName} =`,
`${literals.map((key) => ` | ${quote}${key}${quote}`).join("\n")};
`
].join("\n");
var generator9 = {
generate: async ({
name,
codepoints,
assets,
formatOptions: { ts } = {}
}) => {
const quote = Boolean(ts?.singleQuotes) ? "'" : '"';
const generateKind = (Boolean(ts?.types?.length) ? ts.types : ["enum", "constant", "literalId", "literalKey"]).map((kind) => ({ [kind]: true })).reduce((prev, curr) => Object.assign(prev, curr), {});
const enumName = ts?.enumName || import_case.default.pascal(name);
const constantName = ts?.constantName || `${import_case.default.constant(name)}_CODEPOINTS`;
const literalIdName = ts?.literalIdName || `${import_case.default.pascal(name)}Id`;
const literalKeyName = ts?.literalKeyName || `${import_case.default.pascal(name)}Key`;
const names = { enumName, constantName, literalIdName, literalKeyName };
const enumKeys = generateEnumKeys(Object.keys(assets));
const stringLiteralId = generateKind.literalId ? generateStringLiterals(literalIdName, Object.keys(enumKeys), quote) : null;
const stringLiteralKey = generateKind.literalKey ? generateStringLiterals(literalKeyName, Object.values(enumKeys), quote) : null;
const enums = generateKind.enum ? generateEnums(enumName, enumKeys, quote) : null;
const constant = generateKind.constant ? generateConstant({
...names,
enumKeys,
codepoints,
quote,
kind: generateKind
}) : null;
return [stringLiteralId, stringLiteralKey, enums, constant].filter(Boolean).join("\n");
}
};
var ts_default = generator9;
// src/generators/asset-types/sass.ts
var generator10 = {
dependsOn: "svg" /* SVG */,
generate: (options, svg) => renderTemplate(options.templates.sass, {
...options,
fontSrc: renderSrcAttribute(options, svg, { inline: true })
})
};
var sass_default = generator10;
// src/generators/asset-types/scss.ts
var generator11 = {
dependsOn: "svg" /* SVG */,
generate: (options, svg) => renderTemplate(
options.templates.scss,
{ ...options, fontSrc: renderSrcAttribute(options, svg) },
{ helpers: { codepoint: (str) => str.toString(16) } }
)
};
var scss_default = generator11;
// src/generators/asset-types/index.ts
var generators = {
["svg" /* SVG */]: svg_default,
["ttf" /* TTF */]: ttf_default,
["woff" /* WOFF */]: woff_default,
["woff2" /* WOFF2 */]: woff2_default,
["eot" /* EOT */]: eot_default,
["css" /* CSS */]: css_default,
["html" /* HTML */]: html_default,
["json" /* JSON */]: json_default,
["ts" /* TS */]: ts_default,
["sass" /* SASS */]: sass_default,
["scss" /* SCSS */]: scss_default
};
var asset_types_default = generators;
// src/generators/generate-assets.ts
var generateAssets = async (options) => {
const generated = {};
const generateTypes = [...options.fontTypes, ...options.assetTypes];
const generateAsset = async (type) => {
if (generated[type]) {
return generated[type];
}
const generator12 = asset_types_default[type];
const dependsOn = "dependsOn" in generator12 && generator12.dependsOn;
if (dependsOn && !generated[dependsOn]) {
generated[dependsOn] = await generateAsset(dependsOn);
}
return generated[type] = await generator12.generate(
options,
dependsOn ? generated[dependsOn] : null
);
};
for (const type of generateTypes) {
await generateAsset(type);
}
return generateTypes.reduce(
(out, type) => ({ ...out, [type]: generated[type] }),
{}
);
};
// src/core/runner.ts
var sanitiseOptions = (userOptions) => parseConfig({
...DEFAULT_OPTIONS,
...userOptions
});
var generateFonts = async (userOptions, mustWrite = false) => {
const options = await sanitiseOptions(userOptions);
const { outputDir, inputDir } = options;
if (!inputDir) {
throw new Error("You must specify an input directory");
}
if (mustWrite && !outputDir) {
throw new Error("You must specify an output directory");
}
const assetsIn = await loadAssets(options);
const generatorOptions = getGeneratorOptions(options, assetsIn);
const assetsOut = await generateAssets(generatorOptions);
const writeResults = outputDir ? await writeAssets(assetsOut, options) : [];
const { codepoints } = generatorOptions;
return {
options,
assetsIn,
assetsOut,
writeResults,
codepoints
};
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ASSET_TYPES,
FontAssetType,
OtherAssetType,
generateFonts
});