/************************************************************* * * Copyright (c) 2018-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 startup module that allows dynamically * loaded components to register themselves, and then * creates MathJax methods for typesetting and converting * math based on the registered components. * * @author dpvc@mathjax.org (Davide Cervone) */ import { MathJax as MJGlobal, MathJaxObject as MJObject, MathJaxConfig as MJConfig, combineWithMathJax, combineDefaults, GLOBAL as global, } from './global.js'; import { MathDocument } from '../core/MathDocument.js'; import { MmlNode } from '../core/MmlTree/MmlNode.js'; import { Handler } from '../core/Handler.js'; import { InputJax } from '../core/InputJax.js'; import { OutputJax } from '../core/OutputJax.js'; import { CommonOutputJax } from '../output/common.js'; import { DOMAdaptor } from '../core/DOMAdaptor.js'; import { PrioritizedList } from '../util/PrioritizedList.js'; import { OptionList, OPTIONS } from '../util/Options.js'; import { context } from '../util/context.js'; import { TeX } from '../input/tex.js'; /** * Update the configuration structure to include the startup configuration */ /* prettier-ignore */ export interface MathJaxConfig extends MJConfig { startup?: { input?: string[]; // The names of the input jax to use output?: string; // The name for the output jax to use handler?: string; // The handler to register adaptor?: string; // The name for the DOM adaptor to use document?: any; // The document (or fragment or string) to work in elements?: any[]; // The elements to typeset (default is document body) typeset?: boolean; // Perform initial typeset? ready?: () => void; // Function to perform when components are ready pageReady?: () => void; // Function to perform when page is ready invalidOption?: 'fatal' | 'warn'; // Do invalid options produce a warning, or throw an error? optionError?: (message: string, key: string) => void, // Function to report invalid options loadAllFontFiles: false; // true means force all dynamic font files to load initially [name: string]: any; // Other configuration blocks }; } /** * Generic types for the standard MathJax objects */ export type MATHDOCUMENT = MathDocument & { menu?: { loadingPromise: Promise }; }; export type HANDLER = Handler; export type DOMADAPTOR = DOMAdaptor; export type INPUTJAX = InputJax; export type OUTPUTJAX = OutputJax; /* prettier-ignore */ export type COMMONJAX = CommonOutputJax; export type TEX = TeX; /** * Array of InputJax also with keys using name of jax */ export type JAXARRAY = INPUTJAX[] & { [name: string]: INPUTJAX }; /** * A function to extend a handler class */ export type HandlerExtension = (handler: HANDLER) => HANDLER; /** * Update the MathJax object to inclide the startup information */ export interface MathJaxObject extends MJObject { config: MathJaxConfig; startup: { constructors: { [name: string]: any }; input: JAXARRAY; output: OUTPUTJAX; handler: HANDLER; adaptor: DOMADAPTOR; elements: any[]; document: MATHDOCUMENT; promise: Promise; registerConstructor(name: string, constructor: any): void; useHandler(name: string, force?: boolean): void; useAdaptor(name: string, force?: boolean): void; useOutput(name: string, force?: boolean): void; useInput(name: string, force?: boolean): void; extendHandler(extend: HandlerExtension): void; toMML(node: MmlNode): string; defaultReady(): void; defaultPageReady(): Promise; defaultOptionError(message: string, key: string): void; getComponents(): void; makeMethods(): void; makeTypesetMethods(): void; makeOutputMethods(iname: string, oname: string, input: INPUTJAX): void; makeMmlMethods(name: string, input: INPUTJAX): void; makeResetMethod(name: string, input: INPUTJAX): void; getInputJax(): JAXARRAY; getOutputJax(): OUTPUTJAX; getAdaptor(): DOMADAPTOR; getHandler(): HANDLER; }; [name: string]: any; // Needed for the methods created by the startup module } /** * The implementation of the startup module */ export abstract class Startup { /** * The array of handler extensions */ static extensions = new PrioritizedList(); static visitor: any; // the visitor for toMML(); static mathjax: any; // the mathjax variable from mathjax.js /** * The constructors (or other data) registered by the loaded packages */ public static constructors: { [name: string]: any } = {}; /** * The array of InputJax instances (created after everything is loaded) */ public static input: JAXARRAY = [] as JAXARRAY; /** * The OutputJax instance (created after everything is loaded) */ public static output: OUTPUTJAX = null; /** * The Handler instance (created after everything is loaded) */ public static handler: HANDLER = null; /** * The DOMAdaptor instance (created after everything is loaded) */ public static adaptor: DOMADAPTOR = null; /** * The elements to process (set when typeset or conversion method is called) */ public static elements: any[] = null; /** * The MathDocument instance being used (based on the browser DOM or configuration value) */ public static document: MATHDOCUMENT = null; /** * The function that resolves the first promise defined below * (called in the defaultReady() function when MathJax is finished with * its initial typesetting) */ public static promiseResolve: (value?: any) => any; /** * The function that rejects the first promise defined below * (called in the defaultReady() function when MathJax's initial * typesetting fails) */ public static promiseReject: (reason: any) => void; /** * The promise for the startup process (the initial typesetting). * It is resolved or rejected in the ready() function. */ public static promise = new Promise((resolve, reject) => { Startup.promiseResolve = resolve; Startup.promiseReject = reject; }); /** * A promise that is resolved when the page contents are available * for processing. */ public static pagePromise = new Promise((resolve, _reject) => { const doc = global.document; if ( !doc || !doc.readyState || doc.readyState === 'complete' || doc.readyState === 'interactive' ) { resolve(); } else { const listener = () => resolve(); doc.defaultView.addEventListener('load', listener, true); doc.defaultView.addEventListener('DOMContentLoaded', listener, true); } }); /** * This is true when MathJax.typeset() or MathJax.typesetPromise() have been called * (used by the menu code to tell if a rerender action is needed when a component is * loaded dynamically). */ public static hasTypeset: boolean = false; /** * @param {MmlNode} node The root of the tree to convert to serialized MathML * @returns {string} The serialized MathML from the tree */ public static toMML(node: MmlNode): string { return Startup.visitor.visitTree(node, this.document); } /** * @param {string} name The identifier for the constructor * @param {any} constructor The constructor function for the named object */ public static registerConstructor(name: string, constructor: any) { Startup.constructors[name] = constructor; } /** * @param {string} name The identifier for the Handler to use * @param {boolean} force True to force the Handler to be used even if one is already registered */ public static useHandler(name: string, force: boolean = false) { if (!CONFIG.handler || force) { CONFIG.handler = name; } } /** * @param {string} name The identifier for the DOMAdaptor to use * @param {boolean} force True to force the DOMAdaptor to be used even if one is already registered */ public static useAdaptor(name: string, force: boolean = false) { if (!CONFIG.adaptor || force) { CONFIG.adaptor = name; } } /** * @param {string} name The identifier for the InputJax to use * @param {boolean} force True to force the InputJax to be used even if the configuration already * included an array of input jax */ public static useInput(name: string, force: boolean = false) { if (!inputSpecified || force) { CONFIG.input.push(name); } } /** * @param {string} name The identifier for the OutputJax to use * @param {boolean} force True to force the OutputJax to be used even if one is already registered */ public static useOutput(name: string, force: boolean = false) { if (!CONFIG.output || force) { CONFIG.output = name; } } /** * @param {HandlerExtension} extend A function to extend the handler class * @param {number} priority The priority of the extension */ public static extendHandler(extend: HandlerExtension, priority: number = 10) { Startup.extensions.add(extend, priority); } /** * The default ready() function called when all the packages have been loaded, * which creates the various objects needed by MathJax, creates the methods * based on the loaded components, and does the initial typesetting. * * Setting MathJax.startup.ready in the configuration will * override this, but you can call MathJax.startup.defaultReady() * within your own ready function if needed, or can use the * individual methods below to perform portions of the default * startup actions. */ public static defaultReady() { Startup.getComponents(); Startup.makeMethods(); Startup.pagePromise .then(() => CONFIG.pageReady()) // usually the initial typesetting call .then(() => Startup.promiseResolve()) .catch((err) => Startup.promiseReject(err)); } /** * The default pageReady() function called when the page is ready to be processed, * which returns a promise that does any initial font loading, plus the initial * typesetting, if needed. * * Setting Mathjax.startup.pageReady in the configuration will override this. * * @returns {Promise} Promise resolving when page is ready to process. */ public static defaultPageReady(): Promise { return ( CONFIG.loadAllFontFiles && (Startup.output as COMMONJAX).font ? (Startup.output as COMMONJAX).font.loadDynamicFiles() : Promise.resolve() ) .then(() => Startup.document.menu?.loadingPromise) .then( CONFIG.typeset && MathJax.typesetPromise ? () => Startup.typesetPromise(CONFIG.elements) : Promise.resolve() ) .then(() => Startup.promiseResolve()); } /** * The default OptionError function */ public static defaultOptionError = OPTIONS.optionError; /** * Perform the typesetting with handling of retries * * @param {any[]} elements The list of elements to typeset * @returns {Promise} The promise that resolves when elements are typeset */ public static typesetPromise(elements: any[]): Promise { this.hasTypeset = true; return Startup.document.whenReady(async () => { Startup.document.options.elements = elements; Startup.document.reset(); await Startup.document.renderPromise(); }); } /** * Create the instances of the registered components */ public static getComponents() { Startup.visitor = new MathJax._.core.MmlTree.SerializedMmlVisitor.SerializedMmlVisitor(); Startup.mathjax = MathJax._.mathjax.mathjax; Startup.input = Startup.getInputJax(); Startup.output = Startup.getOutputJax(); Startup.adaptor = Startup.getAdaptor(); if (Startup.handler) { Startup.mathjax.handlers.unregister(Startup.handler); } Startup.handler = Startup.getHandler(); if (Startup.handler) { Startup.mathjax.handlers.register(Startup.handler); Startup.document = Startup.getDocument(); } } /** * Make the typeset and conversion methods based on the registered components * * If there are both input and output jax, * Make Typeset() and TypesetPromise() methods using the given jax, * and TypesetClear() to clear the existing math items * For each input jax * Make input2mml() and input2mmlPromise() conversion methods and inputReset() method * If there is a registered output jax * Make input2output() and input2outputPromise conversion methods and outputStylesheet() method * Create the MathJax.done() method. * Create the MathJax.whenReady() method. */ public static makeMethods() { if (Startup.input && Startup.output) { Startup.makeTypesetMethods(); } const oname = Startup.output ? Startup.output.name.toLowerCase() : ''; for (const jax of Startup.input) { const iname = jax.name.toLowerCase(); Startup.makeMmlMethods(iname, jax); Startup.makeResetMethod(iname, jax); if (Startup.output) { Startup.makeOutputMethods(iname, oname, jax); } } MathJax.done = () => Startup.document.done(); MathJax.whenReady = (action: () => any) => Startup.document.whenReady(action); } /** * Create the Typeset(elements?), TypesetPromise(elements?), and TypesetClear() methods. * * The first two call the document's render() function, the latter * wrapped in handleRetriesFor() and returning the resulting promise. * * TypeseClear() clears all the MathItems from the document. */ public static makeTypesetMethods() { MathJax.typeset = (elements: any[] = null) => { this.hasTypeset = true; Startup.document.options.elements = elements; Startup.document.reset(); Startup.document.render(); }; MathJax.typesetPromise = (elements: any[] = null) => { return Startup.typesetPromise(elements); }; MathJax.typesetClear = (elements: any[] = null) => { if (elements) { Startup.document.clearMathItemsWithin(elements); } else { Startup.document.clear(); } }; } /** * Make the input2output(math, options?) and input2outputPromise(math, options?) methods, * and outputStylesheet() method, where "input" and "output" are replaced by the * jax names (e.g., tex2chtml() and chtmlStyleSheet()). * * The first two perform the document's convert() call, with the Promise version wrapped in * handlerRetriesFor() and returning the resulting promise. The return value is the * DOM object for the converted math. Use MathJax.startup.adaptor.outerHTML(result) * to get the serialized string version of the output. * * The outputStylesheet() method returns the styleSheet object for the output. * Use MathJax.startup.adaptor.innerHTML(MathJax.outputStylesheet()) to get the serialized * version of the stylesheet. * The getMetricsFor(node, display) method returns the metric data for the given node * * @param {string} iname The name of the input jax * @param {string} oname The name of the output jax * @param {INPUTJAX} input The input jax instance */ public static makeOutputMethods( iname: string, oname: string, input: INPUTJAX ) { const name = iname + '2' + oname; MathJax[name] = (math: string, options: OptionList = {}) => { options = { ...options, format: input.name }; return Startup.document.convert(math, options); }; MathJax[name + 'Promise'] = (math: string, options: OptionList = {}) => { options = { ...options, format: input.name }; return Startup.document.convertPromise(math, options); }; MathJax[oname + 'Stylesheet'] = () => Startup.output.styleSheet(Startup.document); if ('getMetricsFor' in Startup.output) { MathJax.getMetricsFor = (node: any, display: boolean) => { return (Startup.output as COMMONJAX).getMetricsFor(node, display); }; } } /** * Make the input2mml(math, options?) and input2mmlPromise(math, options?) methods, * where "input" is replaced by the name of the input jax (e.g., "tex2mml"). * * These convert the math to its serialized MathML representation. * The second wraps the conversion in handleRetriesFor() and * returns the resulting promise. * * @param {string} name The name of the input jax * @param {INPUTJAX} input The input jax itself */ public static makeMmlMethods(name: string, input: INPUTJAX) { const STATE = MathJax._.core.MathItem.STATE; MathJax[name + '2mml'] = (math: string, options: OptionList = {}) => { options = { ...options, end: STATE.CONVERT, format: input.name }; return Startup.toMML(Startup.document.convert(math, options)); }; MathJax[name + '2mmlPromise'] = async ( math: string, options: OptionList = {} ) => { options = { ...options, end: STATE.CONVERT, format: input.name }; const node = await Startup.document.convertPromise(math, options); return Startup.toMML(node); }; } /** * Creates the inputReset() method, where "input" is replaced by the input jax name (e.g., "texReset()). * * The texReset() method clears the equation numbers and labels * * @param {string} name The name of the input jax * @param {INPUTJAX} input The input jax itself */ public static makeResetMethod(name: string, input: INPUTJAX) { MathJax[name + 'Reset'] = (...args: any[]) => input.reset(...args); } /** * @returns {JAXARRAY} The array of instances of the registered input jax */ public static getInputJax(): JAXARRAY { const jax = [] as JAXARRAY; for (const name of CONFIG.input) { const inputClass = Startup.constructors[name]; if (inputClass) { jax[name] = new inputClass(MathJax.config[name]); jax.push(jax[name]); } else { throw Error( 'Input Jax "' + name + '" is not defined (has it been loaded?)' ); } } return jax; } /** * @returns {OUTPUTJAX} The instance of the registered output jax */ public static getOutputJax(): OUTPUTJAX { const name = CONFIG.output; if (!name) return null; const outputClass = Startup.constructors[name]; if (!outputClass) { throw Error( 'Output Jax "' + name + '" is not defined (has it been loaded?)' ); } return new outputClass(MathJax.config[name]); } /** * @returns {DOMADAPTOR} The instance of the registered DOMAdator (the registered constructor * in this case is a function that creates the adaptor, not a class) */ public static getAdaptor(): DOMADAPTOR { const name = CONFIG.adaptor; if (!name || name === 'none') return null; const adaptor = Startup.constructors[name]; if (!adaptor) { throw Error( 'DOMAdaptor "' + name + '" is not defined (has it been loaded?)' ); } return adaptor(MathJax.config[name]); } /** * @returns {HANDLER} The instance of the registered Handler, extended by the registered extensions */ public static getHandler(): HANDLER { const name = CONFIG.handler; if (!name || name === 'none' || !Startup.adaptor) return null; const handlerClass = Startup.constructors[name]; if (!handlerClass) { throw Error( 'Handler "' + name + '" is not defined (has it been loaded?)' ); } let handler = new handlerClass(Startup.adaptor, 5); for (const extend of Startup.extensions) { handler = extend.item(handler); } return handler; } /** * Create the document with the given input and output jax * * @param {any=} root The Document to use as the root document (or null to use the configured document) * @returns {MathDocument} The MathDocument with the configured input and output jax */ public static getDocument(root: any = null): MathDocument { return Startup.mathjax.document(root || CONFIG.document, { ...MathJax.config.options, InputJax: Startup.input, OutputJax: Startup.output, }); } } /** * Export the global MathJax object for convenience */ export const MathJax = MJGlobal as MathJaxObject; /* * If the startup module hasn't been added to the MathJax variable, * Add the startup configuration and data objects, and * set the method for handling invalid options, if provided. */ if (typeof MathJax._.startup === 'undefined') { combineDefaults(MathJax.config, 'startup', { input: [], output: '', handler: null, adaptor: null, document: context.document || '', elements: null, typeset: true, ready: Startup.defaultReady.bind(Startup), pageReady: Startup.defaultPageReady.bind(Startup), polyfillHasOwn: true, // Can be removed with ES2024 implementation of Object.hasown }); combineWithMathJax({ startup: Startup, options: {}, }); if (MathJax.config.startup.invalidOption) { OPTIONS.invalidOption = MathJax.config.startup.invalidOption; } if (MathJax.config.startup.optionError) { OPTIONS.optionError = MathJax.config.startup.optionError; } } /** * Export the startup configuration for convenience */ export const CONFIG = MathJax.config.startup; /* * Tells if the user configuration included input jax or not */ const inputSpecified = CONFIG.input.length !== 0;