/************************************************************* * * Copyright (c) 2022-2025 The MathJax Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @file Implements a mixin for node-based adaptors that overrides * the methods that obtain DOM node sizes, when those aren't * available from the DOM itself. * * @author dpvc@mathjax.org (Davide Cervone) */ import { DOMAdaptor, minWorker } from '../core/DOMAdaptor.js'; import { userOptions, defaultOptions, OptionList } from '../util/Options.js'; import { asyncLoad } from '../util/AsyncLoad.js'; /** * A minimal worker thread interface */ export interface WebWorker { on(kind: string, listener: (event: Event) => void): void; postMessage(msg: any): void; terminate(): void; } /** * A constructor for a given class * * @template T The class to construct */ export type Constructor = new (...args: any[]) => T; /** * The type of an Adaptor class */ export type AdaptorConstructor = Constructor>; /** * The options to the NodeMixin */ /* prettier-ignore */ export const NodeMixinOptions: OptionList = { badCSS: true, // getComputedStyles() is not implemented in the DOM badSizes: true, // element sizes (e.g., ClientWidth, etc.) are not implemented in the DOM }; /** * @param {A} Base The base constructor for the adaptor * @param {NodeMixinOptions} options The options * @returns {A} The NodeAdaptor mixin class * * @template N The HTMLElement node class * @template T The Text node class * @template D The Document class * @template A Extension of AdaptorConstructor */ export function NodeMixin>( Base: A, options: typeof NodeMixinOptions = {} ): A { options = userOptions(defaultOptions({}, NodeMixinOptions), options); return class NodeAdaptor extends Base { /** * The default options */ /* prettier-ignore */ public static OPTIONS: OptionList = { ...(options.badCSS ? { fontSize: 16, // We can't compute the font size, so always use this fontFamily: 'Times', // We can't compute the font family, so always use this } : {}), ...(options.badSizes ? { cjkCharWidth: 1, // Width (in em units) of full width characters unknownCharWidth: .6, // Width (in em units) of unknown (non-full-width) characters unknownCharHeight: .8, // Height (in em units) of unknown characters } : {}) }; /** * Pattern to identify CJK (i.e., full-width) characters */ public static cjkPattern = new RegExp( [ '[', '\u1100-\u115F', // Hangul Jamo '\u2329\u232A', // LEFT-POINTING ANGLE BRACKET, RIGHT-POINTING ANGLE BRACKET '\u2E80-\u303E', // CJK Radicals Supplement ... CJK Symbols and Punctuation '\u3040-\u3247', // Hiragana ... Enclosed CJK Letters and Months '\u3250-\u4DBF', // Enclosed CJK Letters and Months ... CJK Unified Ideographs Extension A '\u4E00-\uA4C6', // CJK Unified Ideographs ... Yi Radicals '\uA960-\uA97C', // Hangul Jamo Extended-A '\uAC00-\uD7A3', // Hangul Syllables '\uF900-\uFAFF', // CJK Compatibility Ideographs '\uFE10-\uFE19', // Vertical Forms '\uFE30-\uFE6B', // CJK Compatibility Forms ... Small Form Variants '\uFF01-\uFF60\uFFE0-\uFFE6', // Halfwidth and Fullwidth Forms '\u{1B000}-\u{1B001}', // Kana Supplement '\u{1F200}-\u{1F251}', // Enclosed Ideographic Supplement '\u{20000}-\u{3FFFD}', // CJK Unified Ideographs Extension B ... Tertiary Ideographic Plane ']', ].join(''), 'gu' ); /** * The node adaptors can't measure DOM node sizes */ public canMeasureNodes: boolean = false; /** * The options for the instance */ public options: OptionList; /** * @param {...any} args Parameters for the mixin class, where the first is * the window to work with and the second are the options for the adaptor * @class */ constructor(...args: any[]) { super(args[0]); const CLASS = this.constructor as typeof NodeAdaptor; this.options = userOptions(defaultOptions({}, CLASS.OPTIONS), args[1]); } /** * For DOMs that don't handle CSS well, use the font size from the options * * @override */ public fontSize(node: N) { return options.badCSS ? this.options.fontSize : super.fontSize(node); } /** * For DOMs that don't handle CSS well, use the font family from the options * * @override */ public fontFamily(node: N) { return options.badCSS ? this.options.fontFamily : super.fontFamily(node); } /** * @override */ public nodeSize(node: N, em: number = 1, local: boolean = null) { if (!options.badSizes) { return super.nodeSize(node, em, local); } const text = this.textContent(node); const non = Array.from(text.replace(NodeAdaptor.cjkPattern, '')).length; // # of non-CJK chars const CJK = Array.from(text).length - non; // # of cjk chars return [ CJK * this.options.cjkCharWidth + non * this.options.unknownCharWidth, this.options.unknownCharHeight, ] as [number, number]; } /** * @override */ public nodeBBox(node: N) { return options.badSizes ? { left: 0, right: 0, top: 0, bottom: 0 } : super.nodeBBox(node); } /** * @override */ public async createWorker( listener: (event: any) => void, options: OptionList ): Promise { const { Worker } = await asyncLoad('node:worker_threads'); class LiteWorker { protected worker: WebWorker; constructor(url: string, options: OptionList = {}) { this.worker = new Worker(url, options); } addEventListener(kind: string, listener: (event: any) => void) { this.worker.on(kind, listener); } postMessage(msg: any) { this.worker.postMessage({ data: msg }); } terminate() { this.worker.terminate(); } } const { path, maps } = options; const url = `${path}/${options.worker}`; const worker = new LiteWorker(url, { type: 'module', workerData: { maps }, }); worker.addEventListener('message', listener); return worker; } }; }