Metalsmith Plugins for Server-side KaTeX Processing

In a prior post, I briefly described the Metalsmith tool-chain I use to render my blog as a collection of static pages (no repeated server fetches, and minimal client-side Javascript). The tool-chain continues to work just fine, though my postings are still very sporadic. Recently, I spent some time trying to improve the Markdown support for formatting of LaTeX math expressions. The implementation I had running relied on two separate Markdown schemes, one supported in IPython files and the other in regular Markdown files. Since this was becoming a burden for me to remember which to use, I decided to clean up the KaTeX processing to only use one Markdown style involving ‘$’ tokens.

IPython Processing

The Jupyter framework supports the presence of LaTeX commands surrounded by $ or $$ tokens. The text wrapped by a single-$ will appear inline with the rest of the text in the current paragraph, while text wrapped with a double-$ will appear on its own in a bigger format and centered on the page. This makes it very easy to enter math equations such as E=mc2E = mc^2 and symbols such as λ\lambda.

Unfortunately, the notebookjs code I rely on to render the IPython files into HTML does not support such extension to Markdown, and so I worked out a scheme where I changed the delimiters and then relied on in-browser Javascript to ultimately do the rendering using the KaTeX package. In order to bring the processing back into the tool-chain, I needed to write something that would perform the KaTeX expansion without harming the normal Markdown processing done by notebookjs.

The solution I came up with was a pre-processor called notebookjs-katex. It only works on IPython Markdown cells, transforming any embedded $...$ or $$...$$ spans into HTML generated by the KaTeX library. Since notebookjs will accept and ignore embedded HTML, this seems to be doing just fine. Here is how I use it in my Metalsmith build script:

var KatexFilter = require("notebookjs-katex");
var kf = new KatexFilter();
var ipynb = JSON.parse(fs.readFileSync('/path/to/notebook.ipynb'));
kf.expandKatexInNotebook(ipynb); // (1)
var notebook = notebookjs.parse(ipynb); // (2)
var html = notebook.render().outerHTML;

You can see an example of the new KaTeX plugin at work here.

Markdown Processing

The Markdown processor I use in my tool-chain is the Remarkable package. It is fast and supports a variety of useful Markdown extensions. Although it does not natively support math expression tagging, it does support expansion via plugins. Since I found no existing plugins for KaTeX rendering, I decided to roll my own and call it — surprise! — remarkable-katex.

To use the plugin is incredibly easy since there are no customization options yet:

var Remarkable = require('remarkable');
var plugin = require('remarkable-katex');
var md = new Remarkable();

End Result

Removing the in-browser rendering proved to be less work than I had originally thought. Now all KaTeX processing takes place in the site build script, page load time is reduced and there is no more flashing due to the in-browser KaTeX processing. Segregating the code into separate NPM modules makes the build script code a bit cleaner — and perhaps someone else will be able to use it as well.

Finally, some math formatting examples (taken from TiddlyWiki KaTeX Demo page.

Example 1

f(x)=f^(ξ)e2πiξxdξf(x) = \int_{-\infty}^\infty \hat f(\xi)\,e^{2 \pi i \xi x} \,d\xi

Example 2

1(ϕ5ϕ)e25π=1+e2π1+e4π1+e6π1+e8π1+\frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }

Example 3

1+q2(1q)+q6(1q)(1q2)+=j=01(1q5j+2)(1q5j+3),for q<1.1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots = \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})}, \quad\quad \text{for }\lvert q\rvert<1.

Example 4

In=lnUλI_n = \frac{-ln U}{\lambda}

Inline Examples

The equation c=±a2+b2\color{#9c0}c = \pm\sqrt{a^2 + \color{#F44}{b^2}} looks familiar. Since abmod2a\equiv b \mod{2}, we are done. Unfortunately, in the interval T0T_0 to T1T_1 we have no data.