Implement component-based typing for the MathJax variable - #1554
Conversation
Example of "simple" use of MathJax in typescript (node-only)import { init } from '@mathjax/src';
const components = ['input/tex'] as const;
type COMPONENTS = (typeof components)[number];
const MathJax = (await init<COMPONENTS>({
loader: { load: [...components] },
}));
console.log(await MathJax.tex2mmlPromise(process.argv[2] || ''));Here, the Note that If you haven't included the node types in your project, you can add declare const process: { argv: string[] };to get that to be defined. |
Example of using MathJax components in Typescriptimport type { MATHJAX_OBJECT } from '@mathjax/src/js/types/dom/lite.js';
import { MathJax as MJX } from '@mathjax/src/js/components/global.js';
const MathJax = MJX as MATHJAX_OBJECT<['adaptors/liteDOM', 'tex-svg']>;
MathJax.config = {
loader: {
load: ['adaptors/liteDOM'],
paths: {mathjax: '@mathjax/src/bundle'},
require: (file) => import(file)
},
options: {
sre: {
locale: process.argv[3] || 'en',
braille: (process.argv[4] as any) || 'nemeth'
}
},
output: {
linebreaks: {
inline: false,
}
},
};
await import('@mathjax/src/bundle/tex-svg.js');
await MathJax.startup.promise;
/**
* Convert a TeX expression to an SVG image
*
* @param {string} math The TeX expression
* @param {boolean} display True for display mode, false for in-line mode
* @returns {Promise<string | void>} The SVG image
*/
async function typeset(math: string, display: boolean = true): Promise<string | void> {
const node = (await MathJax.tex2svgPromise(math, {
display: display,
em: 16, // size of an em in pixels
ex: 8, // size of an ex in pixels
containerWidth: 80 * 16 // width of container for linebreaking of displayed equations
}));
const adaptor = MathJax.startup.adaptor;
return(adaptor.serializeXML(adaptor.tags(node, 'svg')[0]));
}
const math = process.argv[2] || '';
const svg = await typeset(math);
console.log(svg);
MathJax.done();Here we load the We import Once The rest of the code is standard usage of MathJax, but now with full typing being available. |
Example of using mixed direct/component loading in Typescriptimport {mathjax as mjx} from '@mathjax/src/js/mathjax.js';
import {TeX} from '@mathjax/src/js/input/tex.js';
import TexError from '@mathjax/src/js/input/tex/TexError.js';
import {CHTML} from '@mathjax/src/js/output/chtml.js';
import {liteAdaptor} from '@mathjax/src/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from '@mathjax/src/js/handlers/html.js';
import {Loader} from '@mathjax/src/js/components/loader.js';
import {Package} from '@mathjax/src/js/components/package.js';
import '@mathjax/src/components/js/startup/init.js';
import '@mathjax/src/components/js/core/core-lib.js';
import '@mathjax/src/components/js/input/tex/tex.js';
import '@mathjax/src/components/js/output/chtml/chtml.js';
import type { MATHJAX_OBJECT, N, T, D } from '@mathjax/src/js/types/dom/lite.js';
import { MathJax as MJX } from '@mathjax/src/js/components/global.js';
type MATHJAX = MATHJAX_OBJECT<'adaptors/liteDOM' | 'input/tex' | 'output/chtml' | 'startup'>;
const MathJax = MJX as MATHJAX;
const mathjax = mjx as MATHJAX['startup']['mathjax'];
Loader.preLoaded(
'loader', 'startup',
'core',
'input/tex',
'output/chtml',
);
const EM = 16; // size of an em in pixels
const EX = 8; // size of an ex in pixels
const WIDTH = 80 * EM; // width of container for linebreaking
MathJax.config.loader.require = (file) => import(file);
mathjax.asyncLoad = (file) => import(Package.resolvePath(file));
const adaptor = liteAdaptor({fontSize: EM});
RegisterHTMLHandler(adaptor);
const texOptions: MATHJAX['config']['tex'] = {
formatError(_jax: TeX<N, T, D>, err: TexError) {throw err},
...(MathJax.config.tex || {})
};
const chtmlOptions: MATHJAX['config']['chtml'] = {
...(MathJax.config.output || {}),
...(MathJax.config.chtml || {}),
fontURL: 'https://cdn.jsdelivr.net/npm/@mathjax/mathjax-newcm-font/chtml/woff2',
};
const html = mathjax.document('', {
InputJax: new TeX<N, T, D>(texOptions),
OutputJax: new CHTML<N, T, D>(chtmlOptions),
...(MathJax.config.options || {})
});
html.convertPromise(process.argv[2] || '', {
display: true,
em: EM,
ex: EX,
containerWidth: WIDTH
}).then((node) => {
//
// Generate a JSON object with the CHTML output and needed CSS
//
console.log(JSON.stringify({
math: adaptor.outerHTML(node as N),
css: adaptor.cssText(html.outputJax.styleSheet(html))
}));
}).catch((err) => console.error('Error: ' + err.message));Here, we are loading some MathJax modules directly (in the top set of Here we import Here, however, we create We use The rest is standard MathJax, but note that we need to bass the |
Example of direct linking to MathJax modules in Typescript.import { TeX } from '@mathjax/src/js/input/tex.js';
import { CHTML } from '@mathjax/src/js/output/chtml.js';
import { mathjax as mjx} from '@mathjax/src/js/mathjax.js';
import { RegisterHTMLHandler } from '@mathjax/src/js/handlers/html.js'
import { SpeechHandler } from '@mathjax/src/js/a11y/speech.js';
import { MathML } from '@mathjax/src/js/input/mathml.js';
import { liteAdaptor } from '@mathjax/src/js/adaptors/liteAdaptor.js';
import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
import '@mathjax/src/js/input/tex/newcommand/NewcommandConfiguration.js';
import '@mathjax/src/js/input/tex/configmacros/ConfigMacrosConfiguration.js';
import '@mathjax/src/js/util/asyncLoad/esm.js';
import {MathJaxTermesFont} from '@mathjax/mathjax-termes-font/js/chtml.js';
import type { MATHJAX_OBJECT, N, T, D } from '@mathjax/src/js/types/dom/lite.js';
type MATHJAX = MATHJAX_OBJECT<[
'adaptors/liteDOM',
'input/tex-base',
'[tex]/ams',
'[tex]/newcommand',
'[tex]/configmacros',
'output/chtml',
'a11y/speech',
'startup',
]>;
const mathjax = mjx as any as MATHJAX['startup']['mathjax'];
SpeechHandler(RegisterHTMLHandler(liteAdaptor()), new MathML());
const texConfig: MATHJAX['config']['tex'] = {
packages: { '[+]': ['ams', 'newcommand', 'configmacros'] },
macros: { RR: '\\mathbf{R}' },
tags: 'none',
};
const chtmlConfig: MATHJAX['config']['chtml'] = {
fontData: MathJaxTermesFont
};
const html = mathjax.document('', {
InputJax: new TeX<N, T, D>(texConfig),
OutputJax: new CHTML<N, T, D>(chtmlConfig),
enableBraille: false,
});
const node = await html.convertPromise(process.argv[2] || '', {display: false}) as N;
console.log(html.adaptor.outerHTML(node));
console.log(html.adaptor.getAttribute(node, 'data-semantic-speech-none'));
html.done();This example shows how to use In this case, we use In this example, we show how to use an alternative font, and that we can include speech generation without Braille. |
1a88363 to
1bedf4b
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1554 +/- ##
===========================================
+ Coverage 87.01% 87.12% +0.11%
===========================================
Files 392 392
Lines 88279 89098 +819
Branches 3356 3356
===========================================
+ Hits 76814 77628 +814
- Misses 11465 11470 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
zorkow
left a comment
There was a problem hiding this comment.
Another long one...
I found one typo. Not too bad, as the module is no longer used anyway.
Should we consider creating tests for the types/options? I am not sure how one would go about that, though.
|
Wow, I was in the middle of writing up more details about this one. You saved me some work!
Yes, indeed. But 105 of the 173 files were the new
I was wondering about that myself, but don't see how to do it, as these are compile-time issues, and jest is about run-time errors. The only thing I can think of is to have a test spawn a Thanks for going through this long PR! |
Co-authored-by: Volker Sorge <v.sorge@mathjax.org>
This PR implements a mechanism for providing Typescript types for the
MathJaxglobal object. This is complicated by several factors. First, theMathJaxobject is used for initially for configuration and, then is altered by the startup process to include the appropriate commands based on the input and output jax that are loaded, and the types are different for those two uses. Second, the final form of theMathJaxobject depends on the components that are loaded, and Typescript doesn't know this at compile time (theloader.loadarray gives the components, by their code isn't loaded until run time), and so the typing needed for the initialMathJaxas a configuration object can't be determined (theloader.loadis part of that object, but the types need to be known before that object's value is given).To handle these difficulties, you need to tell MathJax what components you are going to load as parameters to a type that can be used to specify the
MathJaxobject, as well as list them in theladder.loadarray. It is possible to list them only once, however, as shown in the examples that I will post in separate comments below. It is probably best to look at these first, before diving into the code changes, in order to see how the typing are to be used in practice.The typing mechanism can be used in all four of the main ways of using MathJax illustrated in the MathJax-demos-node repository: the simple node-only method of using
node-main, the component-based approach, the direct linking to MathJax modules, and the mixed direct-component approach, though each uses its own technique. They are all based on the same typing code, however, just used in different ways.The typing works by adding explicit typing for the
optionsobjects for all the components that allow option settings in theMathJaxconfiguration object. That involves nearly all the components, and so this PR touches a lot of files, and adds some new.d.tsfiles for the component definition files. Fortunately, however, most of the changes follow a common pattern, so that should help with the review.The changes in the
a11yanduidirectories are the most complicated (as they are the ones that have to extend the MathDocument class and its options), so it is probably best to start with thecoredirectory, with the input and output jax, and the MathDocument first. Then the input jax subclasses, their support files, and the TeX and MathML extensions. Then the output jax, and finally thea11yanduicomponents.The main idea is to export an
OPTIONStype (e.g.,TEX_OPTIONS,CONFIGMACROS_OPTIONS,CHTML_OPTIONS, etc.) that specify the types for the options of the class that is being defined, then aconst options: CHTML_OPTIONS = {...}(for example) that lists the default options. That is done as a separate object so that full type checking is performed (if it was done aspublic OPTIONS = { ... } as CHTML_OPTIONS, that would flag any incorrect or missing options, but would not flag extra ones, and we can't dopublic OPTIONS: CHTML_OPTIONS = {...}as the options from the parent class are given through...Base.OPTIONS, and so aren't listed explicitly, and get flagged. As a separate object, it must match the option types exactly, and we can either use it directly as theOPTIONSvalue, or use...optionsto include it as part of a largerOPTIONSobject. (You'll see how it works in the code below).For the various Handler objects, like the
a11yanduifiles that extend theMathItemandMathDocumentclasses, the situation is a little more complicated, as they have to add their options into the existing ones for the document class that they extend, and it is not always clear what that base class is going to be. So, for example, the menu code needs to be aware of the explorer options as well as the complexity options, so it has to build on several other document classes. For these, ther his anOPTIONStype that lists the new option types for the updated MathDocument subclass, with an associatedoptionsthat gives the default values for those options (fully checked), as usual, plus an additional interface that extends the base document class's options with the newOPTIONS, and that is used to set the subclass'optionsproperty's type. A bit complicated, but it does the trick.The SRE and A11Y options pose a similar challenge, as they get extended in several cases as well. A similar process is used (newline the new options, then extends an interface with the new options being added to the old).
The building of the
MathJaxobject typings is done using the files in the newts/stypesdirectory. TheTypes.tsfile has the main machinery for combing the various types for the individual components into a larger single type that is whatMathJaxneeds based on the components being loaded. Components get aCOMPONENT_DEFthat describes the configuration options and any properties that get added to theMathJaxobject, plus whether the component defines an input or output jax (since those produce conversion methods inMathJax). These objects get combined in thetype2mjxtypes near the bottom of the file. This has some fancy type definitions to handle things like creating theinput2outoutconversion functions (which rely on both the input and output components), and turing the config options from being required pro0erties into optional ones, and putting everything into the correct place in the finalMathJaxobject type definition. I'm actually astonished that this can be made to work.The
Components.tsfile creates a type that lists all the components and created theCOMPONENT_DEFobject for each. This file could potentially be generated automatically as part of the MathJax build process, but currently that isn't being done. These component definitions are combined later to create the type for theMathJaxobject. That is done using the definitions in themjx.tsfile. That allows you to use a list of component names to produce the combination of the component definitions for those types, and then use the utilities inTypes.tsto merge those into the finalMathJaxtype.Because the types depend on the DOM node types (the
N,T, andDtemplate values in the code), there is ats/types/domdirectory that has dom-specific versions of the typing macros that include the proper DOM types. Thehtml.tsfile has the ones for in browser use (or with one of the adaptors that uses HTMLElements, like the jsdom and linkedom adaptors), andlite.tsfor the LiteDOM adaptor. That means that the results of the output conversions, for example, will be properly typed, and things likeMathJax.chtmlStylesheet()will return anHTMLElementorLiteElementappropriately (rather thanany).There is a lot of pretty complicated Typescript in these files that we may want to look at together. I will write up more details later, but want to get this PR posted first. Look to the example files below to see how this can be used in practice.
Using MathJax in a Typescript project can involve loading MathJax component file (either from the
componentsdirectory for the source versions, or frombundlefor the packed versions), typescript will complain about the fact that there are no.d.tsfiles for these.jsfiles. So this PR includes new.d.tsfiles for all the.jsfiles incomponents/mjs, and modifies the build tools to generate.d.tsfiles in thebundledirectory. Very few of thecomponents/mjsfiles actually export anything, so most of those.d.tsfiles are justexport {}in order to appease Typescript, but a few of them do include the actual exports for those files. The only really interesting one is forcomponents/mjs/node-main, which exports theinit()function. This is defined to take the list of components that you are loading and return theMathJaxobject properly typed for those, so that makes that node-only "simple" approach very straight-forward.