If you select one of these options, it will be stored in your web browser and automatically applied to other pages on this website.

Simple MathML authoring on Eleventy

How to add beautiful, performant math with a custom Eleventy shortcode.

Reading time: 2–3 minutes

Picture by Dan Cristian Pădureț on Unsplash

I wrote a post about the WCAG color contrast formula and how it works. The post includes some math, which I wanted to embed as accessibly as possible.

Choosing MathML

Mathematical Markup Language (MathML) sounds like the right solution. It's an HTML-style language for describing mathematical notation. Until recently, this hasn't been a feasible option. Browser support was scattershot and there were significant accessibility gaps.

I had a similar problem at work a couple of years ago. Then I did some research and settled on MathJax. MathJax acts as a polyfill, and lets you write LaTeX, while displaying it as MathML, SVG or HTML as output. Compatibility was the deciding factor - MathML wasn't available in enough browsers yet.

But MathJax is heavy, and if you only want to show a simple expression, it's usually overkill. Another alternative was images, but they are bad for accessibility, even if you give them a good description.

Since then, things have changed. MathML is now supported by all major browsers. Assistive technology support is generally good, at least for the simple expressions tested.

Compatibility

MDN recommends providing a fallback. You can do this with a simple:

<script src="https://fred-wang.github.io/mathml.css/mspace.js"></script>

This loads the script from this GitHub repository. Once the page loads, the script checks for any <math> elements in the page. If they're present, it tests whether MathML works in the browser.

If MathML isn't available, it adds a link to the CSS file.

This CSS file instructs the browser on how to render simple MathML rules.

It would be easy to include this CSS everywhere, but don't do this. The rules in the CSS file will conflict if the browser has MathML support.

The First Attempt

Since HTML inside Markdown works well, I first copied some MathML into the post, to see it working.

<math display="block">
    <msub>
        <mi>R</mi>
        <mrow>
            <mi>s</mi>
            <mi>R</mi>
            <mi>G</mi>
            <mi>B</mi>
        </mrow>
    </msub>
    <mo>=</mo>
    <mfrac>
        <msub>
            <mi>R</mi>
            <mrow>
                <mn>8</mn>
                <mi>b</mi>
                <mi>i</mi>
                <mi>t</mi>
            </mrow>
        </msub>
        <mn>255</mn>
    </mfrac>
</math>

It renders like this:

RsRGB=R8bit255

It worked well, scaled with the fonts and matched the color of the surrounding text.

The syntax of MathML means you probably don't want to start editing it yourself. You'll need an authoring tool to do all but the simplest tasks.

If using math in your posts is a one-off occurrence, you could stop here. You can use OpenOffice Math to create expressions. Export them as MathML and paste MathML into your Markdown files.

I wanted the option to do it a bit more often - so it's time to write a shortcode.

The Shortcode

I needed an authoring tool running inside Eleventy, so one Javascript-based. After some research, I settled on Mathup. It's a nice, MIT licensed project.

If you know LaTeX or AsciiMath, the syntax is quite comfortable.

Let's install it:

# with npm
npm install mathup
# or with yarn
yarn add mathup

Here's the code. Depending how you organize your config, this can go inside the .eleventy.js config file, or wherever you keep your shortcodes.

const mathup = require("mathup");

function math(expression, display = false){
    return mathup(expression, {display: (display ? "block" : "inline")}).toString();
}

We are creating a function which wraps a call to Mathup. We take an optional argument to trigger display: block, for use outside of sentences.

Add the function to the .eleventy.js file as a shortcode.

module.exports = function(eleventyConfig) {
    // You'll need to combine this with whatever you already have inside
    // your module.exports block
    eleventyConfig.addShortcode("math", math);
}

Now, back in the Markdown, we can replace the embedded MathML with a simple call to our new shortcode.

{% math "R_(sRGB) = (R_(8bit))/255", true %}

The result looks like this:

RsRGB=R8bit255

It's compact, at least compared to the generated MathML. The commit where I swapped to using shortcodes had 340 lines deleted and 17 added.

As long as you re-write the content correctly, the generated files are identical. There's no client-side Javascript (except the tiny fallback part), so no measurable performance hit.