.. _custom-extension:
==================
A Custom Extension
==================
Making a custom extension is very similar to making a custom combined
component. The main difference is that the extension may rely on
other components, so you need to tell the build system about that so
that it doesn't include the code from those other components. You
also don't load the extension file directly (like you do the combined
component above), but instead include it in the :data:`load` array of the
:data:`loader` configuration block, and MathJax loads it itself, as
discussed below.
For this example, we make a custom TeX extension that defines new TeX
commands implemented by javascript functions.
The commands implemented here provide the ability to generate MathML
token elements from within TeX by hand. This allows more control over
the content and attributes of the elements produced. The macros are
``\mi``, ``\mo``, ``\mn``, ``\ms``, and ``\mtext``, and they each take
an argument that is the text to be used as the content of the
corresponding MathML element. The text is not further processed by
TeX, but the extension does convert sequences of the form ``\UNNNN``
or ``\U{NNNN}`` (where the ``N`` are hexadecimal digits, exactly four
in the first case, or between 1 and 6 in the second) into the
corresponding unicode character; e.g., ``\mi{\U2460}`` would produce
U+2460, a circled digit 1, as the content of an ``mi`` element.
-----
The Extension File
==================
After downloading a copy of MathJax as described in the section on
:ref:`getting-ready`, create a directory for the extension named
``custom-extension`` and ``cd`` to it. Then create the file ``mml.js``
containing the following text:
.. code-block:: javascript
:caption: mml.js
:linenos:
import {HandlerType, ConfigurationType} from '@mathjax/src/js/input/tex/HandlerTypes.js';
import {Configuration} from '@mathjax/src/js/input/tex/Configuration.js';
import {CommandMap} from '@mathjax/src/js/input/tex/TokenMap.js';
import TexError from '@mathjax/src/js/input/tex/TexError.js';
import {replaceUnicode} from '@mathjax/src/js/util/string.js';
import {VERSION} from '@mathjax/src/js/components/version.js';
import {Loader} from '@mathjax/src/js/components/loader.js';
/**
* Check that we are loaded from the right version of MathJax
*/
Loader.checkVersion('[custom]/mml.min.js', VERSION, 'tex-extension');
/**
* This function prevents multi-letter mi elements from being
* interpreted as TEXCLASS.OP
*/
function classORD(node) {
this.getPrevClass(node);
return this;
}
/**
* Allowed attributes on any token element other than the ones with default values
*/
const ALLOWED = new Set(['style', 'href', 'id', 'class']);
/**
* Parse a string as a set of attribute="value" pairs.
*/
function parseAttributes(text, type) {
const attr = {};
if (text) {
let match;
while ((match = text.match(/^\s*((?:data-)?[a-z][-a-z]*)\s*=\s*(?:"([^"]*)"|(.*?))(?:\s+|,\s*|$)/i))) {
const name = match[1];
const value = match[2] || match[3];
if (Object.hasOwn(type.defaults, name) || ALLOWED.has(name) || name.substr(0,5) === 'data-') {
attr[name] = replaceUnicode(value);
} else {
throw new TexError('BadAttribute', 'Unknown attribute "%1"', name);
}
text = text.substr(match[0].length);
}
if (text.length) {
throw new TexError('BadAttributeList', "Can't parse as attributes: %1", text);
}
}
return attr;
}
/**
* Create a MathML token element of the given type
*/
function mmlToken(parser, name, type) {
const typeClass = parser.configuration.nodeFactory.mmlFactory.getNodeClass(type);
const def = parseAttributes(parser.GetBrackets(name), typeClass);
const text = replaceUnicode(parser.GetArgument(name));
const mml = parser.create('token', type, def, text);
if (type === 'mi') {
mml.setTeXclass = classORD;
}
parser.Push(mml);
}
/**
* The mapping of control sequence to function calls
*/
const MmlMap = new CommandMap('mmlMap', {
mi: [mmlToken, 'mi'],
mo: [mmlToken, 'mo'],
mn: [mmlToken, 'mn'],
ms: [mmlToken, 'ms'],
mtext: [mmlToken, 'mtext']
});
/**
* The configuration used to enable the MathML macros
*/
const MmlConfiguration = Configuration.create(
'mml', {
[ConfigurationType.HANDLER]: {
[HandlerType.MACRO]: ['mmlMap']
}
}
);
The comments explain what this code is doing. The main piece needed
to make it a TeX extension is the ``Configuration`` created in the
last few lines. It creates a TeX package named ``mml`` that handles
macros through a ``CommandMap`` named ``mmlMap`` that is defined just
above it. That command map defines five macros described at the
beginning of this section, each of which is tied to a function named
``mmlToken`` defined previously and the name of the MathML token
element to create. The ``mmlToken`` function is the one that is
called by the TeX parser when the ``\mi`` and other macros are called;
it gets the argument to the macro from the TeX string, and any
optional attributes, and creates the MathML element with those
attributes, using the argument as the text of the element.
.. note::
This file uses ES6 ``import`` commands to load the MathJax modules.
It is possible to use ES5 ``require()`` calls instead, if you wish.
For example,
.. code-block:: javascript
import {Configuration} from '@mathjax/src/js/input/tex/Configuration.js';
could be replaced by
.. code-block:: javascript
const {Configuration} = require('@mathjax/src/js/input/tex/Configuration.js');
and similarly for the other ``import`` commands. Note that the
MathJax ``package.json`` file is set up to route
``@mathjax/src/js`` to the MathJax ``mjs`` directory when used in
an ``import`` command, and to the ``cjs`` directory when used in a
``require()`` statement, so you can use the same path in either
case. Similarly ``@mathjax/src/components/js`` maps either to the
``components/mjs`` or ``components/cjs`` directory based on whether
``import`` or ``require()`` is used.
The Extension Configuration File
================================
Next, create a file ``config.json`` that includes the
following:
.. code-block:: json
:caption: config.json
{
"webpack": {
"name": "mml",
"libs": [
"components/js/core/lib",
"components/js/startup/lib",
"components/js/input/tex-base/lib"
],
"dist": "."
}
}
This file gives the name that will be used for this component (``mml``
in this case), an array of components that we assume are already
loaded when this one is loaded (the ``core``, ``startup``, and
``tex-base`` components in this case), and the directory where we want
the final packaged extension to go (``"."`` means the directory
containing the ``config.json`` file). When the directory is the same
as the one containing the extension file, the packed extension file
will end in ``.min.js`` rather than just ``.js``.
Most of the real work is done by the
``@mathjax/src/components/webpack.config.mjs`` file, which will be
called automatically by the commands in the following section.
Building the Extension
======================
Once these two files are ready, you are ready to build the component.
First, make sure that you have obtained the needed tools as described
in :ref:`getting-ready` above. Then you should be able to use the
command
.. code-block:: shell
node ../node_modules/@mathjax/src/components/bin/makeAll
to process your custom build. You should end up with a file
``mml.min.js`` in the directory with the other files. If
you put this on your web server, you can load it as a component by
putting it in the :data:`load` array of the :data:`loader` block of your
configuration, as described in the next section.
.. note::
If you have changed the ``import`` commands to ``require()``, then
you will need to use the command
.. code-block:: shell
node ../node_modules/@mathjax/src/components/bin/makeAll --cjs
in order to tell ``makeAll`` to use MathJax's
``webpack.config.cjs`` file rather than the ``webpack.config.mjs``
one.
Loading the Extension
=====================
To load your custom extension, you will need to tell MathJax where it
is located, and include it in the list of files to be loaded on
startup. MathJax allows you to define paths to locations where your
extensions are stored, and then you can refer to the extensions in
that location by using a prefix that represents that location.
MathJax has a pre-defined prefix, ``mathjax`` that is the default
prefix when none is specified explicitly, and it refers to the
location where the main MathJax file was loaded (e.g., the file
``tex-svg.js``, or ``startup.js``).
You can define your own prefix to point to the location of your
extensions by using the :data:`paths` object in the :data:`loader`
block of your configuration. In our case (see code below), we add a
``custom`` prefix, and have it point to the URL of our extension (in
this case, the same directory as the HTML file that loads it,
represented by the URL ``.``). We use the ``custom`` prefix to
specify ``[custom]/mml.min.js`` in the :data:`load` array so that our
extension will be loaded.
Finally, we add the ``mml`` extension to the :data:`packages` array in
the :data:`tex` block of our configuration via the special notation
``{'[+]': [...]}`` that tells MathJax to append the given array to the
existing :data:`packages` array that is already in the configuration by
default. So this uses all the packages that were already specified,
plus our new ``mml`` package that is defined in our extension.
The configuration and loading of MathJax now looks something like this:
.. code-block:: html
You should change the ``custom: '.'`` line to point to the actual URL for
your server.
This example loads the ``tex-chtml.js`` combined component, so the TeX
input is already loaded when our extension is loaded. If you are
using ``startup.js`` instead, and including ``input/tex`` in the
``load`` array, you will need to tell MathJax that your extension
depends on the ``input/tex`` extension so that it waits to load your
extension until after the TeX input jax is loaded. To do that, add a
``dependencies`` block to your configuration like the following:
.. code-block:: html
This example can be seen live in the `MathJax web demos
`__
repository.
|-----|