UNPKG

advanced-font-manager

Version:

Advanced typescript Font Loader & Manager for Web

641 lines (630 loc) 17.7 kB
"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 });