The mathematical equations in my blog posts, and the ones you see on many other web sites, are formatted with MathJax, a JavaScript library that lets web developers write LaTeX formulas and turns them within your browser into nicely formatted math. The web pages of my blog are generated by Jekyll, a static web site generation system (meaning that it doesn’t go querying a database for its content, they are just web pages stored in files somewhere). I can write my posts in more than one format, but since the April 2017 LiveJournal apocalypse I’ve been writing them using kramdown, a system built into Jekyll for transforming marked-up text files into html ready for browsers to read and display. And so far mostly those different systems have been getting along really well together. Kramdown knows about MathJax and can handle equations in its input without trying to interpret their syntax as kramdown codes, Jekyll only needs me to modify a template somewhere so that my blog pages include an invocation of the MathJax library, and MathJax in your browser happily formats the equations in my posts. But recently, the MathJax people released MathJax version 3.0.0, and that doesn’t work so well with Jekyll and Kramdown. Despite some difficulty, I seem to have gotten them working again. So I thought it might be helpful to post here what went wrong and how I fixed it, in case others run into the same issues.

There are multiple ways of invoking MathJax, but the one I’ve been using is simply to put a line in my html headers saying to load the MathJax library from a content distribution network (asynchronously, so that it doesn’t delay the pages from being shown to readers). Once MathJax loads, it scans through the html that it has been applied to, looking for blocks of math to reformat. The default way of marking these blocks is to include them in \( ... \) or \[ ... \] delimiters (for inline formulas and display formulas that go on a line of their own, as you might use in LaTeX if you aren’t still using $ ... $ or $$ ... $$ instead). There are ways of changing the defaults, and those ways have also changed between MathJax 2 and MathJax 3, but I wasn’t using them.

In kramdown, you don’t use the same delimiters for math. Kramdown expects to see mathematical formulas delimited by $$ ... $$ in its marked-up text input, always. It will determine from context whether it’s an inline formula or a display formula. It also doesn’t use the default delimiters in the html that it generates. Instead it outputs html that puts inline formulas inside <script type="math/tex"> ... </script> html tags, and, similarly, puts display formulas inside <script type="math/tex; mode=display"> ... </script> tags. This all worked in MathJax 2, and these script delimiters are still recommended in the MathJax 3 documentation, but they don’t work any more.

The right way to fix this would be either to get MathJax 3 to understand the script delimiters, or to get kramdown to know how to generate something that works in MathJax 3, but I don’t have a lot of control over either. And the second-best fix might be to use some other software after kramdown runs, to change the delimiters in the static html files before they get served to anyone, but I don’t have that option on my blog host. Instead, I followed a suggestion in the kramdown documentation for working with KaTeX, a competing JavaScript library to MathJax for formatting mathematical equations in web pages. The suggestion is to add to your html files a little bit of glue JavaScript code that recognizes the formula delimiters produced by kramdown and does something with them. In my case, the something that I want to do is just to convert them to the delimiters that MathJax defaultly recognizes.

Timing is crucial here. If I try to run the JavaScript to convert the delimiters too early, they won’t yet be part of the html document that the JavaScript is running on and won’t be found and converted. In particular, running it at the time the html headers are parsed is too early. If I run it too late, the web page will already have been shown to the person viewing it, and each conversion step of each delimiter will also be shown as a slow and unsightly change of the text, on top of the later changes performed by MathJax. You can put JavaScript code at the end of the body of an html page, but that would be too late. Additionally, MathJax should be loaded asynchronously (to prevent slowdowns before the viewer sees something useful from the web page) but must not run until all of the delimiter conversions are complete, because otherwise it won’t see the converted delimiters. So I ended up with the following chunk of JavaScript code, in the Jekyll file _includes/head.html that gets copied into the headers of my html pages. It waits until the entire document is loaded, converts the delimiters, and then loads the MathJax library.

<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function(){
  document.querySelectorAll("script[type='math/tex']").forEach(function(el){
    el.outerHTML = "\\(" + el.textContent + "\\)";
  });
  document.querySelectorAll("script[type='math/tex; mode=display']").forEach(function(el){
    el.outerHTML = "\\[" + el.textContent + "\\]";
  });
  var script = document.createElement('script');
  script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
  document.head.appendChild(script);
}, false);
</script>

This could be simplified somewhat with JQuery, but I didn’t do that because this is the only JavaScript in my files and the overhead of loading JQuery seemed too much for that small use. It’s my first JavaScript code ever, so it could probably be done better by someone with more experience. And it’s a bit of a hack, but it seems to work. One other change that I made implies that you won’t see this code in the html for this post, though. The reason is that I don’t want MathJax incorrectly interpreting the example delimiters in my post and in the code block above as actual mathematics formula delimiters. So I also added some Jekyll conditionals that, with the right keyword in the header of a post, disable including the MathJax Javascript, and I’m using that keyword on this post.

…and I thought I was done, until I started looking at some mathematics-intensive older posts, and found some more problems. In a few cases, kramdown has been putting more than just the script delimiters around its math formulas. Within the script tags, the math has been surrounded by a second level of delimiters, % <![CDATA[ ... %]]>. This coding tells the html parser not to worry about weird special characters in the formula, and it was ignored by the old MathJax because the percent signs cause the rest of their lines to be treated as a comment. But the new MathJax parser doesn’t like the comments (or maybe treats the whole formula as a comment despite the newline characters within it) and displays a blank. This behavior is triggered in kramdown when a formula uses < instead of \lt (easy enough to avoid), or when it uses & (e.g. in an aligned set of equations, not easy to avoid). So the actual code I ended up with is a little more complicated:

<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function(){
  function stripcdata(x) {
    if (x.startsWith('% <![CDATA[') && x.endsWith('%]]>'))
      return x.substring(11,x.length-4);
    return x;
  }
  document.querySelectorAll("script[type='math/tex']").forEach(function(el){
    el.outerHTML = "\\(" + stripcdata(el.textContent) + "\\)";
  });
  document.querySelectorAll("script[type='math/tex; mode=display']").forEach(function(el){
    el.outerHTML = "\\[" + stripcdata(el.textContent) + "\\]";
  });
  var script = document.createElement('script');
  script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
  document.head.appendChild(script);
}, false);
</script>

If you see any mathematics glitches in any of my old or new posts, please tell me; they could be more interactions like this that I haven’t spotted yet.

(Discuss on Mathstodon, which also recently switched to MathJax 3)

Edited (2019-10-31) to add: Thanks to Alexander Kalinin for pointing me to the official solution (see final bullet point under “Changes in the MathJax API”; via). It does much the same replacement by text, but at MathJax processing time rather than at DOMContentLoaded time, and (I’m told) works for CDATA without special handling.