startup.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements a startup module that allows dynamically
  19. * loaded components to register themselves, and then
  20. * creates MathJax methods for typesetting and converting
  21. * math based on the registered components.
  22. *
  23. * @author dpvc@mathjax.org (Davide Cervone)
  24. */
  25. import {MathJax as MJGlobal, MathJaxObject as MJObject,
  26. MathJaxConfig as MJConfig, combineWithMathJax, combineDefaults} from './global.js';
  27. import {MathDocument} from '../core/MathDocument.js';
  28. import {MmlNode} from '../core/MmlTree/MmlNode.js';
  29. import {Handler} from '../core/Handler.js';
  30. import {InputJax} from '../core/InputJax.js';
  31. import {OutputJax} from '../core/OutputJax.js';
  32. import {CommonOutputJax} from '../output/common/OutputJax.js';
  33. import {DOMAdaptor} from '../core/DOMAdaptor.js';
  34. import {PrioritizedList} from '../util/PrioritizedList.js';
  35. import {OptionList, OPTIONS} from '../util/Options.js';
  36. import {TeX} from '../input/tex.js';
  37. /**
  38. * Update the configuration structure to include the startup configuration
  39. */
  40. export interface MathJaxConfig extends MJConfig {
  41. startup?: {
  42. input?: string[]; // The names of the input jax to use
  43. output?: string; // The name for the output jax to use
  44. handler?: string; // The handler to register
  45. adaptor?: string; // The name for the DOM adaptor to use
  46. document?: any; // The document (or fragment or string) to work in
  47. elements?: any[]; // The elements to typeset (default is document body)
  48. typeset?: boolean; // Perform initial typeset?
  49. ready?: () => void; // Function to perform when components are ready
  50. pageReady?: () => void; // Function to perform when page is ready
  51. invalidOption?: 'fatal' | 'warn'; // Do invalid options produce a warning, or throw an error?
  52. optionError?: (message: string, key: string) => void, // Function to report invalid options
  53. [name: string]: any; // Other configuration blocks
  54. };
  55. }
  56. /**
  57. * Generic types for the standard MathJax objects
  58. */
  59. export type MATHDOCUMENT = MathDocument<any, any, any>;
  60. export type HANDLER = Handler<any, any, any>;
  61. export type DOMADAPTOR = DOMAdaptor<any, any, any>;
  62. export type INPUTJAX = InputJax<any, any, any>;
  63. export type OUTPUTJAX = OutputJax<any, any, any>;
  64. export type COMMONJAX = CommonOutputJax<any, any, any, any, any, any, any>;
  65. export type TEX = TeX<any, any, any>;
  66. /**
  67. * A function to extend a handler class
  68. */
  69. export type HandlerExtension = (handler: HANDLER) => HANDLER;
  70. /**
  71. * Update the MathJax object to inclide the startup information
  72. */
  73. export interface MathJaxObject extends MJObject {
  74. config: MathJaxConfig;
  75. startup: {
  76. constructors: {[name: string]: any};
  77. input: INPUTJAX[];
  78. output: OUTPUTJAX;
  79. handler: HANDLER;
  80. adaptor: DOMADAPTOR;
  81. elements: any[];
  82. document: MATHDOCUMENT;
  83. promise: Promise<void>;
  84. /* tslint:disable:jsdoc-require */
  85. registerConstructor(name: string, constructor: any): void;
  86. useHandler(name: string, force?: boolean): void;
  87. useAdaptor(name: string, force?: boolean): void;
  88. useOutput(name: string, force?: boolean): void;
  89. useInput(name: string, force?: boolean): void;
  90. extendHandler(extend: HandlerExtension): void;
  91. toMML(node: MmlNode): string;
  92. defaultReady(): void;
  93. defaultPageReady(): Promise<void>;
  94. getComponents(): void;
  95. makeMethods(): void;
  96. makeTypesetMethods(): void;
  97. makeOutputMethods(iname: string, oname: string, input: INPUTJAX): void;
  98. makeMmlMethods(name: string, input: INPUTJAX): void;
  99. makeResetMethod(name: string, input: INPUTJAX): void;
  100. getInputJax(): INPUTJAX[];
  101. getOutputJax(): OUTPUTJAX;
  102. getAdaptor(): DOMADAPTOR;
  103. getHandler(): HANDLER;
  104. /* tslint:enable */
  105. };
  106. [name: string]: any; // Needed for the methods created by the startup module
  107. }
  108. /*
  109. * Access to the browser document
  110. */
  111. declare var global: {document: Document};
  112. /**
  113. * The implementation of the startup module
  114. */
  115. export namespace Startup {
  116. /**
  117. * The array of handler extensions
  118. */
  119. const extensions = new PrioritizedList<HandlerExtension>();
  120. let visitor: any; // the visitor for toMML();
  121. let mathjax: any; // the mathjax variable from mathjax.js
  122. /**
  123. * The constructors (or other data) registered by the loaded packages
  124. */
  125. export const constructors: {[name: string]: any} = {};
  126. /**
  127. * The array of InputJax instances (created after everything is loaded)
  128. */
  129. export let input: INPUTJAX[] = [];
  130. /**
  131. * The OutputJax instance (created after everything is loaded)
  132. */
  133. export let output: OUTPUTJAX = null;
  134. /**
  135. * The Handler instance (created after everything is loaded)
  136. */
  137. export let handler: HANDLER = null;
  138. /**
  139. * The DOMAdaptor instance (created after everything is loaded)
  140. */
  141. export let adaptor: DOMADAPTOR = null;
  142. /**
  143. * The elements to process (set when typeset or conversion method is called)
  144. */
  145. export let elements: any[] = null;
  146. /**
  147. * The MathDocument instance being used (based on the browser DOM or configuration value)
  148. */
  149. export let document: MATHDOCUMENT = null;
  150. /**
  151. * The function that resolves the first promise defined below
  152. * (called in the defaultReady() function when MathJax is finished with
  153. * its initial typesetting)
  154. */
  155. export let promiseResolve: () => void;
  156. /**
  157. * The function that rejects the first promise defined below
  158. * (called in the defaultReady() function when MathJax's initial
  159. * typesetting fails)
  160. */
  161. export let promiseReject: (reason: any) => void;
  162. /**
  163. * The promise for the startup process (the initial typesetting).
  164. * It is resolves or rejected in the ready() function.
  165. */
  166. export let promise = new Promise<void>((resolve, reject) => {
  167. promiseResolve = resolve;
  168. promiseReject = reject;
  169. });
  170. /**
  171. * A promise that is resolved when the page contents are available
  172. * for processing.
  173. */
  174. export let pagePromise = new Promise<void>((resolve, _reject) => {
  175. const doc = global.document;
  176. if (!doc || !doc.readyState || doc.readyState === 'complete' || doc.readyState === 'interactive') {
  177. resolve();
  178. } else {
  179. const listener = () => resolve();
  180. doc.defaultView.addEventListener('load', listener, true);
  181. doc.defaultView.addEventListener('DOMContentLoaded', listener, true);
  182. }
  183. });
  184. /**
  185. * @param {MmlNode} node The root of the tree to convert to serialized MathML
  186. * @return {string} The serialized MathML from the tree
  187. */
  188. export function toMML(node: MmlNode): string {
  189. return visitor.visitTree(node, document);
  190. }
  191. /**
  192. * @param {string} name The identifier for the constructor
  193. * @param {any} constructor The constructor function for the named object
  194. */
  195. export function registerConstructor(name: string, constructor: any) {
  196. constructors[name] = constructor;
  197. }
  198. /**
  199. * @param {string} name The identifier for the Handler to use
  200. * @param {boolean} force True to force the Handler to be used even if one is already registered
  201. */
  202. export function useHandler(name: string, force: boolean = false) {
  203. if (!CONFIG.handler || force) {
  204. CONFIG.handler = name;
  205. }
  206. }
  207. /**
  208. * @param {string} name The identifier for the DOMAdaptor to use
  209. * @param {boolean} force True to force the DOMAdaptor to be used even if one is already registered
  210. */
  211. export function useAdaptor(name: string, force: boolean = false) {
  212. if (!CONFIG.adaptor || force) {
  213. CONFIG.adaptor = name;
  214. }
  215. }
  216. /**
  217. * @param {string} name The identifier for the InputJax to use
  218. * @param {boolean} force True to force the InputJax to be used even if the configuration already
  219. * included an array of input jax
  220. */
  221. export function useInput(name: string, force: boolean = false) {
  222. if (!inputSpecified || force) {
  223. CONFIG.input.push(name);
  224. }
  225. }
  226. /**
  227. * @param {string} name The identifier for the OutputJax to use
  228. * @param {boolean} force True to force the OutputJax to be used even if one is already registered
  229. */
  230. export function useOutput(name: string, force: boolean = false) {
  231. if (!CONFIG.output || force) {
  232. CONFIG.output = name;
  233. }
  234. }
  235. /**
  236. * @param {HandlerExtension} extend A function to extend the handler class
  237. * @param {number} priority The priority of the extension
  238. */
  239. export function extendHandler(extend: HandlerExtension, priority: number = 10) {
  240. extensions.add(extend, priority);
  241. }
  242. /**
  243. * The default ready() function called when all the packages have been loaded,
  244. * which creates the various objects needed by MathJax, creates the methods
  245. * based on the loaded components, and does the initial typesetting.
  246. *
  247. * Setting MathJax.startup.ready in the configuration will
  248. * override this, but you can call MathJax.startup.defaultReady()
  249. * within your own ready function if needed, or can use the
  250. * individual methods below to perform portions of the default
  251. * startup actions.
  252. */
  253. export function defaultReady() {
  254. getComponents();
  255. makeMethods();
  256. pagePromise
  257. .then(() => CONFIG.pageReady()) // usually the initial typesetting call
  258. .then(() => promiseResolve())
  259. .catch((err) => promiseReject(err));
  260. }
  261. /**
  262. * The default pageReady() function called when the page is ready to be processed,
  263. * which returns the function that performs the initial typesetting, if needed.
  264. *
  265. * Setting Mathjax.startup.pageReady in the configuration will override this.
  266. */
  267. export function defaultPageReady() {
  268. return (CONFIG.typeset && MathJax.typesetPromise ?
  269. MathJax.typesetPromise(CONFIG.elements) as Promise<void> :
  270. Promise.resolve());
  271. }
  272. /**
  273. * Create the instances of the registered components
  274. */
  275. export function getComponents() {
  276. visitor = new MathJax._.core.MmlTree.SerializedMmlVisitor.SerializedMmlVisitor();
  277. mathjax = MathJax._.mathjax.mathjax;
  278. input = getInputJax();
  279. output = getOutputJax();
  280. adaptor = getAdaptor();
  281. if (handler) {
  282. mathjax.handlers.unregister(handler);
  283. }
  284. handler = getHandler();
  285. if (handler) {
  286. mathjax.handlers.register(handler);
  287. document = getDocument();
  288. }
  289. }
  290. /**
  291. * Make the typeset and conversion methods based on the registered components
  292. *
  293. * If there are both input and output jax,
  294. * Make Typeset() and TypesetPromise() methods using the given jax,
  295. * and TypesetClear() to clear the existing math items
  296. * For each input jax
  297. * Make input2mml() and input2mmlPromise() conversion methods and inputReset() method
  298. * If there is a registered output jax
  299. * Make input2output() and input2outputPromise conversion methods and outputStylesheet() method
  300. */
  301. export function makeMethods() {
  302. if (input && output) {
  303. makeTypesetMethods();
  304. }
  305. const oname = (output ? output.name.toLowerCase() : '');
  306. for (const jax of input) {
  307. const iname = jax.name.toLowerCase();
  308. makeMmlMethods(iname, jax);
  309. makeResetMethod(iname, jax);
  310. if (output) {
  311. makeOutputMethods(iname, oname, jax);
  312. }
  313. }
  314. }
  315. /**
  316. * Create the Typeset(elements?), TypesetPromise(elements?), and TypesetClear() methods.
  317. *
  318. * The first two call the document's render() function, the latter
  319. * wrapped in handleRetriesFor() and returning the resulting promise.
  320. *
  321. * TypeseClear() clears all the MathItems from the document.
  322. */
  323. export function makeTypesetMethods() {
  324. MathJax.typeset = (elements: any[] = null) => {
  325. document.options.elements = elements;
  326. document.reset();
  327. document.render();
  328. };
  329. MathJax.typesetPromise = (elements: any[] = null) => {
  330. document.options.elements = elements;
  331. document.reset();
  332. return mathjax.handleRetriesFor(() => {
  333. document.render();
  334. });
  335. };
  336. MathJax.typesetClear = (elements: any[] = null) => {
  337. if (elements) {
  338. document.clearMathItemsWithin(elements);
  339. } else {
  340. document.clear();
  341. }
  342. };
  343. }
  344. /**
  345. * Make the input2output(math, options?) and input2outputPromise(math, options?) methods,
  346. * and outputStylesheet() method, where "input" and "output" are replaced by the
  347. * jax names (e.g., tex2chtml() and chtmlStyleSheet()).
  348. *
  349. * The first two perform the document's convert() call, with the Promise version wrapped in
  350. * handlerRetriesFor() and returning the resulting promise. The return value is the
  351. * DOM object for the converted math. Use MathJax.startup.adaptor.outerHTML(result)
  352. * to get the serialized string version of the output.
  353. *
  354. * The outputStylesheet() method returns the styleSheet object for the output.
  355. * Use MathJax.startup.adaptor.innerHTML(MathJax.outputStylesheet()) to get the serialized
  356. * version of the stylesheet.
  357. * The getMetricsFor(node, display) method returns the metric data for the given node
  358. *
  359. * @param {string} iname The name of the input jax
  360. * @param {string} oname The name of the output jax
  361. * @param {INPUTJAX} input The input jax instance
  362. */
  363. export function makeOutputMethods(iname: string, oname: string, input: INPUTJAX) {
  364. const name = iname + '2' + oname;
  365. MathJax[name] =
  366. (math: string, options: OptionList = {}) => {
  367. options.format = input.name;
  368. return document.convert(math, options);
  369. };
  370. MathJax[name + 'Promise'] =
  371. (math: string, options: OptionList = {}) => {
  372. options.format = input.name;
  373. return mathjax.handleRetriesFor(() => document.convert(math, options));
  374. };
  375. MathJax[oname + 'Stylesheet'] = () => output.styleSheet(document);
  376. if ('getMetricsFor' in output) {
  377. MathJax.getMetricsFor = (node: any, display: boolean) => {
  378. return (output as COMMONJAX).getMetricsFor(node, display);
  379. };
  380. }
  381. }
  382. /**
  383. * Make the input2mml(math, options?) and input2mmlPromise(math, options?) methods,
  384. * where "input" is replaced by the name of the input jax (e.g., "tex2mml").
  385. *
  386. * These convert the math to its serialized MathML representation.
  387. * The second wraps the conversion in handleRetriesFor() and
  388. * returns the resulting promise.
  389. *
  390. * @param {string} name The name of the input jax
  391. * @param {INPUTJAX} input The input jax itself
  392. */
  393. export function makeMmlMethods(name: string, input: INPUTJAX) {
  394. const STATE = MathJax._.core.MathItem.STATE;
  395. MathJax[name + '2mml'] =
  396. (math: string, options: OptionList = {}) => {
  397. options.end = STATE.CONVERT;
  398. options.format = input.name;
  399. return toMML(document.convert(math, options));
  400. };
  401. MathJax[name + '2mmlPromise'] =
  402. (math: string, options: OptionList = {}) => {
  403. options.end = STATE.CONVERT;
  404. options.format = input.name;
  405. return mathjax.handleRetriesFor(() => toMML(document.convert(math, options)));
  406. };
  407. }
  408. /**
  409. * Creates the inputReset() method, where "input" is replaced by the input jax name (e.g., "texReset()).
  410. *
  411. * The texReset() method clears the equation numbers and labels
  412. *
  413. * @param {string} name The name of the input jax
  414. * @param {INPUTJAX} input The input jax itself
  415. */
  416. export function makeResetMethod(name: string, input: INPUTJAX) {
  417. MathJax[name + 'Reset'] = (...args: any[]) => input.reset(...args);
  418. }
  419. /**
  420. * @return {INPUTJAX[]} The array of instances of the registered input jax
  421. */
  422. export function getInputJax(): INPUTJAX[] {
  423. const jax = [] as INPUTJAX[];
  424. for (const name of CONFIG.input) {
  425. const inputClass = constructors[name];
  426. if (inputClass) {
  427. jax.push(new inputClass(MathJax.config[name]));
  428. } else {
  429. throw Error('Input Jax "' + name + '" is not defined (has it been loaded?)');
  430. }
  431. }
  432. return jax;
  433. }
  434. /**
  435. * @return {OUTPUTJAX} The instance of the registered output jax
  436. */
  437. export function getOutputJax(): OUTPUTJAX {
  438. const name = CONFIG.output;
  439. if (!name) return null;
  440. const outputClass = constructors[name];
  441. if (!outputClass) {
  442. throw Error('Output Jax "' + name + '" is not defined (has it been loaded?)');
  443. }
  444. return new outputClass(MathJax.config[name]);
  445. }
  446. /**
  447. * @return {DOMADAPTOR} The instance of the registered DOMAdator (the registered constructor
  448. * in this case is a function that creates the adaptor, not a class)
  449. */
  450. export function getAdaptor(): DOMADAPTOR {
  451. const name = CONFIG.adaptor;
  452. if (!name || name === 'none') return null;
  453. const adaptor = constructors[name];
  454. if (!adaptor) {
  455. throw Error('DOMAdaptor "' + name + '" is not defined (has it been loaded?)');
  456. }
  457. return adaptor(MathJax.config[name]);
  458. }
  459. /**
  460. * @return {HANDLER} The instance of the registered Handler, extended by the registered extensions
  461. */
  462. export function getHandler(): HANDLER {
  463. const name = CONFIG.handler;
  464. if (!name || name === 'none' || !adaptor) return null;
  465. const handlerClass = constructors[name];
  466. if (!handlerClass) {
  467. throw Error('Handler "' + name + '" is not defined (has it been loaded?)');
  468. }
  469. let handler = new handlerClass(adaptor, 5);
  470. for (const extend of extensions) {
  471. handler = extend.item(handler);
  472. }
  473. return handler;
  474. }
  475. /**
  476. * Create the document with the given input and output jax
  477. *
  478. * @param {any=} root The Document to use as the root document (or null to use the configured document)
  479. * @returns {MathDocument} The MathDocument with the configured input and output jax
  480. */
  481. export function getDocument(root: any = null): MathDocument<any, any, any> {
  482. return mathjax.document(root || CONFIG.document, {
  483. ...MathJax.config.options,
  484. InputJax: input,
  485. OutputJax: output
  486. });
  487. }
  488. }
  489. /**
  490. * Export the global MathJax object for convenience
  491. */
  492. export const MathJax = MJGlobal as MathJaxObject;
  493. /*
  494. * If the startup module hasn't been added to the MathJax variable,
  495. * Add the startup configuration and data objects, and
  496. * set the method for handling invalid options, if provided.
  497. */
  498. if (typeof MathJax._.startup === 'undefined') {
  499. combineDefaults(MathJax.config, 'startup', {
  500. input: [],
  501. output: '',
  502. handler: null,
  503. adaptor: null,
  504. document: (typeof document === 'undefined' ? '' : document),
  505. elements: null,
  506. typeset: true,
  507. ready: Startup.defaultReady.bind(Startup),
  508. pageReady: Startup.defaultPageReady.bind(Startup)
  509. });
  510. combineWithMathJax({
  511. startup: Startup,
  512. options: {}
  513. });
  514. if (MathJax.config.startup.invalidOption) {
  515. OPTIONS.invalidOption = MathJax.config.startup.invalidOption;
  516. }
  517. if (MathJax.config.startup.optionError) {
  518. OPTIONS.optionError = MathJax.config.startup.optionError;
  519. }
  520. }
  521. /**
  522. * Export the startup configuration for convenience
  523. */
  524. export const CONFIG = MathJax.config.startup;
  525. /*
  526. * Tells if the user configuration included input jax or not
  527. */
  528. const inputSpecified = CONFIG.input.length !== 0;