-
Notifications
You must be signed in to change notification settings - Fork 212
/
Copy pathstartup.ts
597 lines (545 loc) · 20.1 KB
/
startup.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
/*************************************************************
*
* Copyright (c) 2018-2023 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.
*/
/**
* @fileoverview 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 [email protected] (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 {TeX} from '../input/tex.js';
/**
* Update the configuration structure to include the startup configuration
*/
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<any, any, any>;
export type HANDLER = Handler<any, any, any>;
export type DOMADAPTOR = DOMAdaptor<any, any, any>;
export type INPUTJAX = InputJax<any, any, any>;
export type OUTPUTJAX = OutputJax<any, any, any>;
export type COMMONJAX = CommonOutputJax<any, any, any, any, any, any, any, any, any, any, any>;
export type TEX = TeX<any, any, any>;
/**
* 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<void>;
/* tslint:disable:jsdoc-require */
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<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;
/* tslint:enable */
};
[name: string]: any; // Needed for the methods created by the startup module
}
/**
* The implementation of the startup module
*/
export namespace Startup {
/**
* The array of handler extensions
*/
const extensions = new PrioritizedList<HandlerExtension>();
let visitor: any; // the visitor for toMML();
let mathjax: any; // the mathjax variable from mathjax.js
/**
* The constructors (or other data) registered by the loaded packages
*/
export const constructors: {[name: string]: any} = {};
/**
* The array of InputJax instances (created after everything is loaded)
*/
export let input: JAXARRAY = [] as JAXARRAY;
/**
* The OutputJax instance (created after everything is loaded)
*/
export let output: OUTPUTJAX = null;
/**
* The Handler instance (created after everything is loaded)
*/
export let handler: HANDLER = null;
/**
* The DOMAdaptor instance (created after everything is loaded)
*/
export let adaptor: DOMADAPTOR = null;
/**
* The elements to process (set when typeset or conversion method is called)
*/
export let elements: any[] = null;
/**
* The MathDocument instance being used (based on the browser DOM or configuration value)
*/
export let 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)
*/
export let promiseResolve: () => void;
/**
* The function that rejects the first promise defined below
* (called in the defaultReady() function when MathJax's initial
* typesetting fails)
*/
export let promiseReject: (reason: any) => void;
/**
* The promise for the startup process (the initial typesetting).
* It is resolves or rejected in the ready() function.
*/
export let promise = new Promise<void>((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
/**
* A promise that is resolved when the page contents are available
* for processing.
*/
export let pagePromise = new Promise<void>((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);
}
});
/**
* @param {MmlNode} node The root of the tree to convert to serialized MathML
* @return {string} The serialized MathML from the tree
*/
export function toMML(node: MmlNode): string {
return visitor.visitTree(node, document);
}
/**
* @param {string} name The identifier for the constructor
* @param {any} constructor The constructor function for the named object
*/
export function registerConstructor(name: string, constructor: any) {
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
*/
export function 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
*/
export function 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
*/
export function 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
*/
export function 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
*/
export function extendHandler(extend: HandlerExtension, priority: number = 10) {
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.
*/
export function defaultReady() {
getComponents();
makeMethods();
pagePromise
.then(() => CONFIG.pageReady()) // usually the initial typesetting call
.then(() => promiseResolve())
.catch((err) => promiseReject(err));
}
/**
* The default pageReady() function called when the page is ready to be processed,
* which returns the function that performs the initial typesetting, if needed.
*
* Setting Mathjax.startup.pageReady in the configuration will override this.
*/
export function defaultPageReady() {
return (CONFIG.loadAllFontFiles && (output as COMMONJAX).font ?
(output as COMMONJAX).font.loadDynamicFiles() : Promise.resolve())
.then(CONFIG.typeset && MathJax.typesetPromise ?
typesetPromise(CONFIG.elements) :
Promise.resolve());
}
/**
* Perform the typesetting with handling of retries
*/
export function typesetPromise(elements: any[]) {
document.options.elements = elements;
document.reset();
return mathjax.handleRetriesFor(() => {
document.render();
});
}
/**
* Create the instances of the registered components
*/
export function getComponents() {
visitor = new MathJax._.core.MmlTree.SerializedMmlVisitor.SerializedMmlVisitor();
mathjax = MathJax._.mathjax.mathjax;
input = getInputJax();
output = getOutputJax();
adaptor = getAdaptor();
if (handler) {
mathjax.handlers.unregister(handler);
}
handler = getHandler();
if (handler) {
mathjax.handlers.register(handler);
document = 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
*/
export function makeMethods() {
if (input && output) {
makeTypesetMethods();
}
const oname = (output ? output.name.toLowerCase() : '');
for (const jax of input) {
const iname = jax.name.toLowerCase();
makeMmlMethods(iname, jax);
makeResetMethod(iname, jax);
if (output) {
makeOutputMethods(iname, oname, jax);
}
}
}
/**
* 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.
*/
export function makeTypesetMethods() {
MathJax.typeset = (elements: any[] = null) => {
document.options.elements = elements;
document.reset();
document.render();
};
MathJax.typesetPromise = (elements: any[] = null) => {
promise = promise.then(() => typesetPromise(elements));
return promise;
};
MathJax.typesetClear = (elements: any[] = null) => {
if (elements) {
document.clearMathItemsWithin(elements);
} else {
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
*/
export function makeOutputMethods(iname: string, oname: string, input: INPUTJAX) {
const name = iname + '2' + oname;
MathJax[name] =
(math: string, options: OptionList = {}) => {
options.format = input.name;
return document.convert(math, options);
};
MathJax[name + 'Promise'] =
(math: string, options: OptionList = {}) => {
promise = promise.then(() => {
options.format = input.name;
return mathjax.handleRetriesFor(() => document.convert(math, options));
});
return promise;
};
MathJax[oname + 'Stylesheet'] = () => output.styleSheet(document);
if ('getMetricsFor' in output) {
MathJax.getMetricsFor = (node: any, display: boolean) => {
return (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
*/
export function makeMmlMethods(name: string, input: INPUTJAX) {
const STATE = MathJax._.core.MathItem.STATE;
MathJax[name + '2mml'] =
(math: string, options: OptionList = {}) => {
options.end = STATE.CONVERT;
options.format = input.name;
return toMML(document.convert(math, options));
};
MathJax[name + '2mmlPromise'] =
(math: string, options: OptionList = {}) => {
promise = promise.then(() => {
options.end = STATE.CONVERT;
options.format = input.name;
return mathjax.handleRetriesFor(() => toMML(document.convert(math, options)));
});
return promise;
};
}
/**
* 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
*/
export function makeResetMethod(name: string, input: INPUTJAX) {
MathJax[name + 'Reset'] = (...args: any[]) => input.reset(...args);
}
/**
* @return {JAXARRAY} The array of instances of the registered input jax
*/
export function getInputJax(): JAXARRAY {
const jax = [] as JAXARRAY;
for (const name of CONFIG.input) {
const inputClass = 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;
}
/**
* @return {OUTPUTJAX} The instance of the registered output jax
*/
export function getOutputJax(): OUTPUTJAX {
const name = CONFIG.output;
if (!name) return null;
const outputClass = constructors[name];
if (!outputClass) {
throw Error('Output Jax "' + name + '" is not defined (has it been loaded?)');
}
return new outputClass(MathJax.config[name]);
}
/**
* @return {DOMADAPTOR} The instance of the registered DOMAdator (the registered constructor
* in this case is a function that creates the adaptor, not a class)
*/
export function getAdaptor(): DOMADAPTOR {
const name = CONFIG.adaptor;
if (!name || name === 'none') return null;
const adaptor = constructors[name];
if (!adaptor) {
throw Error('DOMAdaptor "' + name + '" is not defined (has it been loaded?)');
}
return adaptor(MathJax.config[name]);
}
/**
* @return {HANDLER} The instance of the registered Handler, extended by the registered extensions
*/
export function getHandler(): HANDLER {
const name = CONFIG.handler;
if (!name || name === 'none' || !adaptor) return null;
const handlerClass = constructors[name];
if (!handlerClass) {
throw Error('Handler "' + name + '" is not defined (has it been loaded?)');
}
let handler = new handlerClass(adaptor, 5);
for (const extend of 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
*/
export function getDocument(root: any = null): MathDocument<any, any, any> {
return mathjax.document(root || CONFIG.document, {
...MathJax.config.options,
InputJax: input,
OutputJax: 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: (typeof document === 'undefined' ? '' : document),
elements: null,
typeset: true,
ready: Startup.defaultReady.bind(Startup),
pageReady: Startup.defaultPageReady.bind(Startup)
});
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;