advanced-font-manager
Version:
Advanced typescript Font Loader & Manager for Web
641 lines (630 loc) • 17.7 kB
JavaScript
"use strict";
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, {
FontLoader: () => FontLoader,
normalizeFamilyName: () => normalizeFamilyName,
normalizeFontFaceDescriptors: () => normalizeFontFaceDescriptors,
normalizePath: () => normalizePath,
normalizeUrl: () => normalizeUrl,
sanitizeFamilyName: () => sanitizeFamilyName,
sanitizePath: () => sanitizePath,
sanitizeStyle: () => sanitizeStyle,
sanitizeUrl: () => sanitizeUrl
});
module.exports = __toCommonJS(index_exports);
// src/loader/fontLoader.ts
var import_events = require("events");
// src/loader/baseLoader.ts
var op = __toESM(require("opentype.js"));
// src/errors/FontErrorHandler.ts
var FontErrorHandler = class {
failedFonts;
constructor() {
this.failedFonts = /* @__PURE__ */ new Map();
}
addFailedFont(fontName, error) {
this.failedFonts.set(fontName, this.categorizeError(error));
}
getError(fontFamily) {
return this.failedFonts.get(fontFamily) || null;
}
hasError(fontFamily) {
const error = this.failedFonts.get(fontFamily);
return error ? true : false;
}
destroy() {
this.failedFonts.clear();
}
categorizeError(error) {
const errorMessage = error?.message || String(error);
if (errorMessage.includes("timeout")) {
return {
type: "timeout",
message: "Font loading timed out",
details: [errorMessage]
};
}
if (errorMessage.includes("illegal string")) {
return {
type: "DOMException",
message: "DOM Exception while loading font",
details: [errorMessage],
originalError: error
};
}
if (errorMessage.includes("network") || errorMessage.includes("fetch")) {
return {
type: "network",
message: "Network error while loading font",
details: [errorMessage]
};
}
if (errorMessage.includes("format") || errorMessage.includes("invalid font")) {
return {
type: "format",
message: "Ivalid font format or corrupted font",
details: [errorMessage]
};
}
if (errorMessage.includes("security") || errorMessage.includes("CORS")) {
return {
type: "security",
message: "Security error while loading font",
details: [errorMessage]
};
}
if (errorMessage.includes("validation")) {
return {
type: "validation",
message: "Font failed validation checks",
details: ["The font file is invalid"],
originalError: error
};
}
if (errorMessage.includes("sanitizer")) {
return {
type: "security",
message: "Font was blocked for security reasons",
details: ["The font file appears to be corrupted or invalid."],
originalError: error
};
}
return {
type: "unknown",
message: "An unknown error occurred while loading font",
details: [errorMessage],
originalError: error
};
}
};
// src/mixins/normalize.mixin.ts
function normalizeFontFaceDescriptors(descriptors) {
const normalizedDescriptors = {};
const allowedStyles = ["normal", "italic", "oblique"];
const allowedWeights = [
"normal",
"bold",
"lighter",
"bolder",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"900"
];
const allowedStretches = [
"normal",
"ultra-condensed",
"extra-condensed",
"condensed",
"semi-condensed",
"semi-expanded",
"expanded",
"extra-expanded",
"ultra-expanded"
];
const allowedDisplay = [
"auto",
"block",
"swap",
"fallback",
"optional"
];
const allowedOverrides = ["normal", "inherit"];
const normalize = (value, allowed, fallback) => value && allowed.includes(value) ? value : fallback ?? void 0;
if (descriptors.display) {
normalizedDescriptors.display = normalize(descriptors.display, allowedDisplay, "auto");
}
if (descriptors.style) {
normalizedDescriptors.style = normalize(descriptors.style, allowedStyles);
}
if (descriptors.weight) {
normalizedDescriptors.weight = normalize(descriptors.weight, allowedWeights);
}
if (descriptors.stretch) {
normalizedDescriptors.stretch = normalize(descriptors.stretch, allowedStretches);
}
if (descriptors.unicodeRange) {
normalizedDescriptors.unicodeRange = descriptors.unicodeRange ?? void 0;
}
if (descriptors.featureSettings) {
normalizedDescriptors.featureSettings = descriptors.featureSettings ?? void 0;
}
if (descriptors.ascentOverride) {
normalizedDescriptors.ascentOverride = normalize(descriptors.ascentOverride, allowedOverrides);
}
if (descriptors.descentOverride) {
normalizedDescriptors.descentOverride = normalize(descriptors.descentOverride, allowedOverrides);
}
if (descriptors.lineGapOverride) {
normalizedDescriptors.lineGapOverride = normalize(descriptors.lineGapOverride, allowedOverrides);
}
return normalizedDescriptors;
}
function normalizeFamilyName() {
}
function normalizeUrl() {
}
function normalizePath() {
}
// src/mixins/sanitize.mixin.ts
function sanitizeFamilyName(familyName) {
if (typeof familyName !== "string") {
return null;
}
let sanitized = familyName.replace(/[^\w\s-]/g, "");
sanitized = sanitized.trim().replace(/\s+/g, " ");
return sanitized;
}
function sanitizeUrl(url) {
const allowedProtocols = ["http:", "https:"];
const allowedExtensions = [".woff", ".woff2", ".ttf", ".otf", ".eot", ".svg"];
try {
const parsedUrl = new URL(url);
if (!allowedProtocols.includes(parsedUrl.protocol)) {
return null;
}
const hasValidExtension = allowedExtensions.some(
(extension) => parsedUrl.pathname.endsWith(extension)
);
if (!hasValidExtension) {
return null;
}
return parsedUrl.href;
} catch {
return null;
}
}
function sanitizePath() {
}
function sanitizeStyle() {
}
// src/utils/logger.ts
var Logger = class {
config;
colors = {
info: "\x1B[32m",
warn: "\x1B[33m",
error: "\x1B[31m",
debug: "\x1B[36m",
reset: "\x1B[0m"
};
constructor(config) {
this.config = {
timestamp: true,
colors: true,
...config
};
}
getTimestamp() {
if (!this.config.timestamp) return "";
return `[${(/* @__PURE__ */ new Date()).toISOString()}]`;
}
getPrefix() {
if (!this.config.prefix) return "";
return `[${this.config.prefix}]`;
}
formatMessage(level, message) {
const timestamp = this.getTimestamp();
const prefix = this.getPrefix();
const levelStr = `[${level.toUpperCase()}]`;
if (this.config.colors) {
return `${timestamp} ${prefix} ${this.colors[level]}${levelStr} ${message}${this.colors.reset}`;
}
return `${timestamp} ${prefix} ${levelStr} ${message}`;
}
shouldLog(messageLevel) {
const levels = ["info", "warn", "error", "debug"];
return levels.indexOf(messageLevel) >= levels.indexOf(this.config.minLevel);
}
debug(message, ...args) {
if (!this.shouldLog("debug")) return;
console.debug(this.formatMessage("debug", message), ...args);
}
info(message, ...args) {
if (!this.shouldLog("info")) return;
console.info(this.formatMessage("info", message), ...args);
}
warn(message, ...args) {
if (!this.shouldLog("warn")) return;
console.warn(this.formatMessage("warn", message), ...args);
}
error(message, ...args) {
if (!this.shouldLog("error")) return;
console.error(this.formatMessage("error", message), ...args);
}
};
// src/loader/baseLoader.ts
var BaseLoader = class {
originalBuffers;
logger;
/**
* List of font faces that have been parsed
* @type {Map<string, op.Font>}
* @private
* @memberof FontLoader
* */
parsedFontFaces;
errorHandler;
constructor(debugLevel) {
this.originalBuffers = /* @__PURE__ */ new Map();
this.parsedFontFaces = /* @__PURE__ */ new Map();
this.errorHandler = new FontErrorHandler();
this.logger = new Logger({
minLevel: debugLevel || "info",
prefix: "Loader",
timestamp: true,
colors: true
});
}
async load(fontFaces, params) {
if (!fontFaces || !fontFaces.length) {
return;
}
const promises = fontFaces.map(async (fontFace) => {
const loadedFont = await Promise.race([
fontFace.load(),
new Promise(
(_, reject) => {
setTimeout(() => reject(new Error("Font loading timeout")), params?.timeOut || 5e3);
this.logger.warn(`Font loading timeout: ${fontFace.family}`);
}
)
]);
try {
document.fonts.add(loadedFont);
} catch (error) {
this.logger.error(`Failed add font to the DOM: ${fontFace.family}`);
}
});
try {
await Promise.all(promises);
} catch (error) {
this.logger.error(`Failed to load fonts: ${error}`);
}
this.logger.info("Fonts loaded");
}
unload(font) {
try {
document.fonts.delete(font);
} catch (error) {
this.logger.error(`Failed to remove font: ${font}`);
}
}
createFromUrl(family, url, options) {
try {
let fontUrl, fontFamily, fontDescriptors;
fontUrl = sanitizeUrl(url);
if (!fontUrl) {
this.errorHandler.addFailedFont(family, "Invalid URL");
return null;
}
fontFamily = sanitizeFamilyName(family);
if (!fontFamily) {
this.errorHandler.addFailedFont(family, "Invalid Family");
return null;
}
if (options) {
fontDescriptors = normalizeFontFaceDescriptors(options);
if (!fontDescriptors) {
fontDescriptors = {};
}
}
const fontFace = new FontFace(
fontFamily,
`url(${fontUrl})`,
fontDescriptors
);
this.setOriginalBuffer(family, fontUrl);
return fontFace;
} catch (error) {
this.errorHandler.addFailedFont(family, error);
return null;
}
}
async createFromFile(file, family, options) {
const buffer = await file.arrayBuffer();
const fontFace = this.createFromBuffer(buffer, family, options);
return fontFace;
}
createFromBuffer(buffer, family, options) {
try {
let fontBuffer, fontFamily, fontDescriptors;
fontBuffer = buffer;
if (!buffer) {
this.errorHandler.addFailedFont(family, "Invalid URL");
return null;
}
fontFamily = sanitizeFamilyName(family);
if (!fontFamily) {
this.errorHandler.addFailedFont(family, "Invalid Family");
return null;
}
if (options) {
fontDescriptors = normalizeFontFaceDescriptors(options);
if (!fontDescriptors) {
fontDescriptors = {};
}
}
const fontFace = new FontFace(
fontFamily,
fontBuffer,
fontDescriptors
);
this.setOriginalBuffer(family, buffer);
return fontFace;
} catch (error) {
this.errorHandler.addFailedFont(family, error);
return null;
}
}
destroy() {
this.originalBuffers.clear();
this.parsedFontFaces.clear();
this.errorHandler.destroy();
}
parse(family) {
try {
const arrayBuffer = this.originalBuffers.get(family);
if (!arrayBuffer) {
this.logger.error(`Failed to parse font: ${family}`);
return;
}
const font = op.parse(arrayBuffer);
this.parsedFontFaces.set(family, font);
this.logger.info(`Font parsed : ${family}`);
} catch (error) {
this.logger.error(`Failed to parse fonts: ${error}`);
}
}
async setOriginalBuffer(familyName, font) {
if (typeof font === "string") {
const url = font;
const response = await fetch(url);
const buffer = await response.arrayBuffer();
this.originalBuffers.set(familyName, buffer);
} else {
this.originalBuffers.set(familyName, font);
}
}
hasBeenParsed(family) {
return this.parsedFontFaces.has(family);
}
};
// src/defaults/loader.default.ts
var DEFAULT_LOADER_OPTIONS = {
useResolvers: false,
disableWarnings: false,
useCache: false,
enableDebug: false
};
// src/loader/fontLoader.ts
var FontLoader = class extends BaseLoader {
/**
* List of font faces to load
* @type {FontFace[]}
* @private
* @memberof FontLoader
*/
fontFaces;
/**
* List of font faces that have been loaded
* @type {Set<string>}
* @private
* @memberof FontLoader
* */
loadedFontFaces;
/**
* Options for the font loader
* @type {FontLoaderOptions}
* @private
* @memberof FontLoader
* */
options;
/**
* Event emitter for the font loader
* @type {EventEmitter}
* @private
* @memberof FontLoader
* */
eventEmitter;
constructor(options, rules) {
super(options?.debugLevel);
this.fontFaces = [];
this.loadedFontFaces = /* @__PURE__ */ new Set();
this.options = options ? { ...options, ...DEFAULT_LOADER_OPTIONS } : DEFAULT_LOADER_OPTIONS;
this.eventEmitter = new import_events.EventEmitter();
}
async loadFromUrl(args) {
const { fonts, params } = args;
fonts.forEach((font) => {
const { url, family, options } = font;
const fontFace = this.createFromUrl(family, url, options);
if (fontFace) {
this.fontFaces.push(fontFace);
}
});
try {
await this.load(this.fontFaces, params);
} catch (error) {
if (this.options.useResolvers) {
console.error("should call resolver", error);
} else {
console.error("should not call resolver", error);
}
}
}
async loadFromFile(args) {
const { fonts, params } = args;
fonts.forEach(async (font) => {
const { file, family, options } = font;
const fontFace = await this.createFromFile(file, family, options);
if (fontFace) {
this.fontFaces.push(fontFace);
}
});
try {
await this.load(this.fontFaces, params);
} catch (error) {
console.error(error);
}
}
async loadFromBuffer(args) {
const { fonts, params } = args;
fonts.forEach((font) => {
const { buffer, family, options } = font;
const fontFace = this.createFromBuffer(buffer, family, options);
if (fontFace) {
this.fontFaces.push(fontFace);
}
});
try {
await this.load(this.fontFaces, params);
} catch (error) {
console.error(error);
}
}
on(event, listener) {
}
off(event, listener) {
}
/**
* Get the list of font faces
* @returns {FontFace[]}
*/
getFontFaces() {
return this.fontFaces;
}
/**
* Get the list of loaded font faces
* @returns {Set<string>}
*/
getLoadedFontFaces() {
return this.loadedFontFaces;
}
/**
* Get the list of successfully parsed font faces
* @returns {FontFace[]}
*/
getParsedFontFaces() {
return this.parsedFontFaces.keys();
}
/**
* Get the list of failed font faces
* @returns {FontFace[]}
*/
getFailedFontFaces() {
return this.fontFaces.filter((fontFace) => this.isErrored(fontFace.family));
}
/**
* Get the list of font face errors for a given family
* @param {string} family
* @returns {FontLoadError}
*/
getFontFaceErrors(family) {
return this.errorHandler.getError(family) || null;
}
isLoaded(family) {
return this.loadedFontFaces.has(family);
}
isParsed(family) {
return this.hasBeenParsed(family);
}
isErrored(family) {
return this.errorHandler.hasError(family);
}
unloadFontFaces() {
this.fontFaces.forEach((fontFace) => {
this.unload(fontFace.family);
});
}
unloadFontFace(family) {
this.unload(family);
}
unloadFontFaceByURL(url) {
this.unload(url);
}
/**
* Unload a font face by file (TODO: Implement)
* @param file
* @deprecated
*/
unloadFontFaceByFile(file) {
}
/**
* Unload a font face by buffer (TODO: Implement)
* @param buffer
* @deprecated
*/
unloadFontFaceByBuffer(buffer) {
}
parseFontFace(family) {
this.parseFontFace(family);
}
destroy() {
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FontLoader,
normalizeFamilyName,
normalizeFontFaceDescriptors,
normalizePath,
normalizeUrl,
sanitizeFamilyName,
sanitizePath,
sanitizeStyle,
sanitizeUrl
});