Using KaTeX on github pages

9 minute read

Every now and then I write a blog post with some math in it. Math is hard enough as it is, without the added frustration of having to do a lot of effort (as blog writer) to make it look nice, or a lot of effort (as reader) to decode some math formula written in text, such as Newton’s law of gravity: F = G * (m_1*m_2)/ r^2. Luckily there are tools such as {\KaTeX} that make math look good: F = G \times \frac{m_1 \times m_2}{r^2}. This post explains how to get it working on your github pages (or any html page) through the use of javascript and client-side rendering.

TL;DR See this section for the code that you need to add to the top of your page, and the example section for examples.

There are other math rendering engines out there as well; during my search I also came by MathJax. Supposedly MathJax is supported out of the box in github pages, but I couldn’t find a clear description on how to do it. Since I prefer \KaTeX’s way of doing things (rending everything to HTML rather than to a png image), I quickly decided to go for that.

There is a \KaTeX jekyll plugin, however since github pages doesn’t allow custom plugins, this will not work. The solution is to render the \KaTeX expressions on the client, through the use of javascript. Advantage is that this will work not just on github pages, but on any html page.

Since I like to be explicit, I set the system up in such a way that it looks for “html-like” tags <katex-inline>...</katex-inline> (or <katex-block>...</katex-block>), and anything between these tags will be rendered by \KaTeX. I will introduce 2 methods to do this; one based on Web Components, and the other based on a one time render. We will then use the second as a fallback method for the first, since not all webbrowsers support web components yet.

Web Components

Using Web Components, one can make custom “html-like”-tags. It is (in its simplest form) a way to tell the browser “hey, if you encounter a tag with this name, please execute this code in order to show it”. Web Components have pretty decent browser support by now (95% of global users; Internet Explorer being the notable exception…); this may or may not be good enough for you (if you have a github pages page, you may not expect your content to be interesting to IE users anyways :)). The great advantage of Web Components over other methods is that the system tracks exactly what is going on: it will run your code once for every element it encounters. If elements are added/removed, the system will take care of it all.

Creating a web component is super easy:

class KatexInline extends HTMLElement {
    constructor() {
        super();
        katex.render(this.innerText, this, {throwOnError: false, displayMode: false});
    }
}
customElements.define("katex-inline", KatexInline)

We create a KatexInline class that extends from HTMLSpanElement. In the constructor, we call katex.render based on the current contents, and use the current element as target. Finally, we call customElements.define to register our new custom element.

Note: the first version of this blog actually inherited KatexInline from HTMLSpanElement. This works fine on Firefox, however gives an error on Safari: TypeError Illegal Constructor on the line where super() is called. The solution was to extend from HTMLElement rather than HTMLSpanElement. The current version of this acticle has the fixes in it.

We do the same for KatexBlock; only change being that we call katex.render with displayMode: true, which is the display mode for blocks of \KaTeX.

class KatexBlock extends HTMLElement {
    constructor() {
        super();
        katex.render(this.innerText, this, {throwOnError: false, displayMode: true});
    }
}
customElements.define("katex-block", KatexBlock)

One time render

The fallback method is quite easy; we just search for all <katex-inline> tags and render them. Obviously this will only pick up those tags that exist at the time of render, which is probably good enough.

document.querySelectorAll("katex-inline").forEach(
    (el) => {
        katex.render(el.innerText, el, {throwOnError: false, displayMode: false});
    }
)
document.querySelectorAll("katex-block").forEach(
    (el) => {
        katex.render(el.innerText, el, {throwOnError: false, displayMode: true});
    }
)

Bringing it all together

Before we can use either method, we need to make sure the \KaTeX library is loaded. We achieve this by putting the code in a callback for the onload of the library. The full code therefore to be added to the header of your page is (if you want, you can put the renderKatex() function in a separate file; probably good practice, but not essential for this tutorial):

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css" integrity="sha384-Um5gpz1odJg5Z4HAmzPtgZKdTBHZdw8S29IecapCSB31ligYPhHQZMIlWLYQGVoc" crossorigin="anonymous">

<script>
    function renderKatex() {
        let macros = {}
        if (customElements) {
            class KatexInline extends HTMLElement {
                constructor() {
                    super();
                    katex.render(this.innerText, this, {throwOnError: false, displayMode: false, macros: macros, output: "html"});
                }
            }
            customElements.define("katex-inline", KatexInline)

            class KatexBlock extends HTMLElement {
                constructor() {
                    super();
                    katex.render(this.innerText, this, {throwOnError: false, displayMode: true, macros: macros, output: "html"});
                }
            }
            customElements.define("katex-block", KatexBlock)
        } else {
            document.querySelectorAll("katex-inline").forEach(
                (el) => {
                    katex.render(el.innerText, el, {throwOnError: false, displayMode: false, macros: macros, output: "html"});
                }
            )
            document.querySelectorAll("katex-block").forEach(
                (el) => {
                    katex.render(el.innerText, el, {throwOnError: false, displayMode: true, macros: macros, output: "html"});
                }
            )
        }
    }
</script>
<!-- The loading of KaTeX is deferred to speed up page rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.js" integrity="sha384-YNHdsYkH6gMx9y3mRkmcJ2mFUjTd0qNQQvY9VYZgQd7DcN7env35GzlmFaZ23JGp" crossorigin="anonymous" onload="renderKatex()"></script>

Fonts

This section was written when I still had a sans-serif as my main blog font. Leaving it here, because it’s still interesting, but some of the information may not make sense anymore.

The \KaTeX library uses a serif-font by default (and makes it 20% larger). This may or may not be ideal for your use. Probably if you have a nice big formula on your page, it looks kind of fancy to have a different font.

\sum_{\substack{0<i<m\\0<j<n}}i^j
Some nice example sum

Even a slightly complex formula (like \frac{1}{\infty}) actually looks quite good. However saying that avogadro’s number is 6.02214 \times 10^{23} mol^{-1} just looks weird.

One can argue whether one is not just better off using plain HTML for such simple things: avogadro’s number is 6.02214 × 1023 mol-1 (6.02214 &times; 10<sup>23</sup> <i>mol<sup>-1</sup></i>), but it feels not good to create html-math when you actually have a math library.

In order to allow such usage, I include some SCSS in my system to counter the \KaTeX fonts:

/* scss: */
katex-inline.keepfont {
    .katex, .mathnormal, .mathit, .mathrm {
        font-family: inherit;
    }
    .katex {
        font-size: 1em;
    }
}
/* or plain css: */
katex-inline.keepfont .katex, katex-inline.keepfont .mathnormal, katex-inline.keepfont .mathit, katex-inline.keepfont .mathrm { font-family: inherit; }
katex-inline.keepfont .katex { font-size: 1em; }

As a result I can write avogadro's number is <katex-inline class="keepfont">6.02214 \times 10^{23} mol^{-1}</katex-inline> and the result is avogadro’s number is 6.02214 \times 10^{23} mol^{-1}.

It should be noted that there is probably a good reason why \KaTeX has its own fonts; probably lots of glyphs don’t exist in normal fonts, or alignments don’t work out in normal fonts (you can actually already see that in the example, where the last letter of mol intersects the minus sign of the superscript). So using this is at your own risk, and I plan not to use it for anything but the simplest formulas inline.

Escaping

The code described in this blog gets the text to render from the innerText field of the element. This means that all non-html-safe characters should be escaped. For instance, in order to produce x^2 < x^3 | x > 1 should be written as

<katex-inline>x^2 &lt; x^3 | x &gt; 1</katex-inline>

The HTML parser is generally very forgiving through, and things will work just fine if you write

<katex-inline>x^2 < x^3 | x > 1</katex-inline>

However by being sloppy, you do run into the possibility that you create something that could be seen by the HTML parser as an HTML tag, and then things go wrong. For instance (the non-sensical):

<katex-inline>x^2 <x x> 1</katex-inline>

actually sees the <x x> as an opening tag <x> with an attribute x, so things break horribly while searching for the </x> tag. For good practice, always HTML escape your element.

Be aware that if your source-code goes through other systems (markdown, liquid, etc), you may need to escape / work around that too. However liquid can also help you:

<katex-inline>{{ 'x^2 <x x> 1' | escape }}</katex-inline>

results in x^2 <x x> 1 (because the | escape HTML-escapes the content. Be aware though that because you’re within liquid quotes, you have problems with curly brackets and backslashes and quotes….)

To really be safe (from almost everything) use liquid {% capture %} combined with {% raw %}..

{% capture formula %}
{% raw %}
  \text{Murphy's law} <x>  | \frac{1}{0}
{% endraw %}
{% endcapture %}
<katex-inline>{{ formula | escape }}</katex-inline>
\text{Murphy's law} <x> | \frac{1}{0}

Errors

The code in this blogpost has the setting throwOnError: false. This means that in case of an error in the thing to generate, the formula is shown in red. For instance:

<katex-inline>\frac{3}</katex-inline>

renders as \frac{3} (because \frac expects 2 parameters).

Available functions and codes

I find the supported functions page and the support table page extremely helpful as a cheatsheet on how to do things in \KaTeX. Probably it only describes \frac{1}{100}^{th} of what is possible; feel free to link to your favourite cheatsheet/documentation in the comments.

Security

The method described in this blog tells \KaTeX to render things securely, specifically trust is set to false (the default value) on render. This means that (in theory) you could render <katex-inline> tags on your webpage that were created by untrusted users (read more on the trust-setting), however if you intend to do that, I would advise to have an expert double-check if things actually work as you (and I) expect.

Note that by setting the trust option to true, some more \KaTeX tags are unlocked. If you need these, it should be easy to enable this option (put it in the code right next to throwOnError).

Examples

avogadro's number is <katex-inline class="keepfont">6.02214 \times 10^{23} mol^{-1}</katex-inline>.

avogadro’s number is 6.02214 \times 10^{23} mol^{-1}.

The progress is controlled by <katex-inline>x = \overbrace{a+b+c}^{\text{these are the major terms}} + \epsilon</katex-inline>.

The progress is controlled by x = \overbrace{a+b+c}^{\text{these are the major terms}} + \epsilon.

<katex-block>
\begin{CD}
   A @>a>> B \\
@VbVV @AAcA \\
   C @= D
\end{CD}
</katex-block>
\begin{CD} A @>a>> B \\ @VbVV @AAcA \\ C @= D \end{CD}

Probably there are much fancier examples; however it’s probably better to google them :).

OK one more…

{% capture formula %}
{% raw %}
\begin{align*}
  \mathcal{L} = &- \frac{1}{4} F_{\mu \nu} F^{\mu \nu} \\
      &+ i \bar{\psi} \cancel{D} \psi + h.c. \\
      &+ \bar{\psi}_i y_{ij} \psi_j \phi + h.c. \\
      &+ |D_\mu \phi|^2 - V(\phi)
\end{align*}
{% endraw %}
{% endcapture %}
<katex-block>{{ formula | escape }}</katex-block>
\begin{align*} \mathcal{L} = &- \frac{1}{4} F_{\mu \nu} F^{\mu \nu} \\ &+ i \bar{\psi} \cancel{D} \psi + h.c. \\ &+ \bar{\psi}_i y_{ij} \psi_j \phi + h.c. \\ &+ |D_\mu \phi|^2 - V(\phi) \end{align*}

Comments