Painful Upgrade to MathJax 3

Posted: Mar 29, 2020. Last updated: Nov 29, 2020.
Tags:

The upgrade experience to MathJax 3 was far from smooth.

Contents

Update (2020-11-29): Added information for kramdown v2.2 and GitHub Pages v207.

Writing Math in Markdown

Jekyll kramdown

Jekyll uses the kramdown converter to compile Markdown into HTML. As part of the conversion process, kramdown looks for “math blocks” that start and end with $$. The double-dollar-sign is the only math block delimiter that kramdown supports. A math block that is both preceded and follwed by a newline is considered “display math.” Any non-display math block is called “inline math.”

There is one caveat: $$-enclosed text that appears between an explicit pair of opening and closing HTML tags in the Markdown file might need special treatment. See the kramdown documentation on markdown="span" and markdown="block" attributes.

The math_engine option in Jekyll’s _config.yml determines what kramdown does with each math block. The following table shows how kramdown converts math blocks in Markdown into HTML with the default math_engine: mathjax option.

Type Markdown syntax HTML output (v2.2+) HTML output (v1 - v2.1)
display math [newline] $$...$$ [newline] \[...\] <script type="math/tex; mode=display">...</script>
inline math [text] $$...$$ [text] \(...\) <script type="math/tex">...</script>

The HTML output <script> tags used by kramdown v1 through v2.1 are appropriate for MathJax 2.7, which renders them into math notation. However, MathJax 3 no longer uses the <script> tags. This is explained in more detail below.

Another important note is that kramdown treats the \ character as an escape character. In order to have \( or \[ appear in the generated HTML output, the Markdown file must have \\( or \\[.

MathJax

MathJax is a JavaScript library that parses HTML documents, identifies math expressions, and renders them in proper math notation. In both versions 2.7 and 3, MathJax looks for certain math delimiters:

The default math delimiters are $$...$$ and \[...\] for displayed mathematics, and \(...\) for in-line mathematics. Note in particular that the $...$ in-line delimiters are not used by default.

See MathJax 2.7 and 3 documentation

Note that these delimiters are customizable. See MathJax 2.7 and 3.

In version 2.7, MathJax ran a preprocessor on HTML files that converted math delimiters into <script> tags, similar to the process described for kramdown above. Then, a MathJax “input jax” would translate each <script> tag into the internal MathJax format which is then rendered by an “output jax” based on the output format chosen by the user (e.g., HTML-CSS, SVG, or NativeMML).

However, MathJax 3 changes the paradigm by skipping <script> tags altogether. Instead, it directly parses math delimiters in HTML documents and renders math expressions. As a result, MathJax 3 by default no longer accepts the <script> tags that kramdown (before v2.2) outputs using math_engine: mathjax option. As a workaround, MathJax 3 can also be extended to work with HTML documents that already have <script> tags by adding an action to its renderActions option. The first element in the list corresponding to the findScript key is a number indicating the priority that the action runs. For reference, MathJax 3’s standard parsing is done using the find action with a priority of 10.

window.MathJax = {
  options: {
    renderActions: {
      findScript: [9, function (doc) {
        for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
          const display = !!node.type.match(/; *mode=display/);
          const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
          const text = document.createTextNode('');
          node.parentNode.replaceChild(text, node);
          math.start = {node: text, delim: '', n: 0};
          math.end = {node: text, delim: '', n: 0};
          doc.math.push(math);
        }
        doc.findMath();
      }, '']
    }
  }
};

Other sources

  • MathJax 3 in Jekyll and Kramdown: describes the kramdown-MathJax 3 integration problem and proposes a different JavaScript solution to deal with the <script> tags that kramdown generates.

GitHub Pages

GitHub Pages currently always sets math_engine: mathjax in _config.yml, overriding any user-specified value (GitHub Help). This is a GitHub Pages limitation, and not a Jekyll limitation. However, there is a possibility that GitHub removes its math_engine: mathjax requirement.

GitHub Pages also sporadically updates the version of kramdown that it uses. As of August 2020, GitHub Pages v207 now uses kramdown version 2.3+.

Setup

I have described how kramdown, MathJax, and GitHub Pages work individually. Now I describe what it means when these moving pieces all come together.

November 2020 onwards

Because I am using GitHub Pages to automatically generate my static site, I am stuck with Jekyll’s math_engine: mathjax option in _config.yml. Since GitHub Pages now uses kramdown v2.3, math blocks $$ in Markdown are converted to either inline \(...\) or display \[...\] markers. This means that I can directly use Mathjax 3 without resulting to the findScript workaround.

I could have tried to avoid kramdown’s math blocks altogether by switching to single-dollar-sign $ delimiters for inline math and using \[...\] delimiters for display math. However, this strategy does not always work because kramdown does not know to treat the math expression as a math block and may instead interpret LaTeX underscores as italics. (This is also why setting markdown: GFM in _config.yml is a bad idea, because GFM does not know to parse math separately from normal Markdown.)

Thus, my current setup is as follows:

Setup

  • Set use_math: true in the preamble of the page. Write $ for an actual dollar-sign in the page; no need for any escaping.

Inline Math: [text] $$...$$ [text] (note the double dollar signs)

  • converted by kramdown into [text] \(...\) [text] in the HTML
  • MathJax 3 directly parses and renders the HTML as inline math

Display Math: [newline] $$...$$ [newline]

  • converted by kramdown into [newline] \[...\] [newline] in the HTML
  • MathJax 3 directly parses and renders the HTML as display math

Wishful / possible future directions

  • If kramdown supports custom math delimiters for either the math_engine: mathjax or math_engine: null options, then I could directly use \[...\] and \(...\) delimiters.
  • If GFM adds support for math blocks, then I could bypass kramdown altogether and specify markdown: GFM in _config.yml.
  • Now that kramdown v2.2+ directly outputs LaTeX into the HTML, I may consider using KaTeX instead of MathJax.

pre-November 2020

Because I am choosing to upgrade to MathJax 3 and I am using GitHub Pages to automatically generate my static site, I am stuck with Jekyll’s math_engine: mathjax option in _config.yml. Therefore, math blocks $$ in Markdown are converted to <script> tags, which I parse with MathJax by adding in the findScript action to its set of renderActions.

I could have tried to avoid kramdown’s math blocks altogether by switching to single-dollar-sign $ delimiters for inline math and using \[...\] delimiters for display math. However, this strategy does not always work because kramdown does not know to treat the math expression as a math block and may instead interpret LaTeX underscores as italics. Furthermore, I am also holding out for the possibility that GitHub removes its math_engine: mathjax requirement.

Thus, my current setup is as follows:

Setup

  • Set use_math: true in the preamble of the page. To write an actual dollar-sign in the page, escape it as \$. Such escaping is only necessary when use_math: true is set in the preamble.

Inline Math

  • legacy (but current) option: [text] $$...$$ [text] (note the double dollar signs)
    • converted by kramdown into [text] <script type="math/tex">...</script> [text]
    • MathJax 3 uses the findScript action in renderActions of the MathJax configuration to identify these script tags
  • future (hopefully): [text] $...$ [text] or [text] \\(...\\) [text] (double-backslash is necessary because kramdown treats \ as an escape character)
    • not converted by kramdown during jekyll Markdown-to-HTML compilation
    • rendered directly from LaTeX by MathJax 3 (after enabling $ inline delimiter)

Display Math: [newline] $$...$$ [newline]

  • legacy (but current) option
    • converted by kramdown into <script type="math/tex; mode=display">...</script>
    • MathJax 3 uses the findScript action in renderActions of the MathJax configuration to identify these script tags
  • future (hopefully)
    • not converted by kramdown during jekyll Markdown-to-HTML compilation
    • rendered directly from LaTeX by MathJax 3

Known issue: Because the findScript action is called separately from find, different math expressions are parsed at different times. Suppose the findScript action is called before find, as illustrated in the example above. As a result, if there are new commands or operators (e.g., \newcommand{\bx}{\mathbf{x}}) that are used by a math expression parsed by findScript but defined within an expression parsed by find, then the expression will fail to display properly.