2
\$\begingroup\$

I've created a JS snippet to enhance markdown at display-time with KaTeX math rendering. (Specifically, this is mostly for documentation of math-heavy libraries where the host language documentation tool doesn't have specific math support (e.g. rustdoc).) The snippet itself is the HTML in <head> to load the KaTeX scripts and manipulate the DOM (which would be injected into whatever generated documentation), and the rest of the snippet is to demonstrate the functionality.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">

        <!--KaTeX (preload the two most common fonts)-->
        <link rel="preload" as="font" type="font/woff2" crossorigin
            href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/fonts/KaTeX_Main-Regular.woff2">
        <link rel="preload"  as="font" type="font/woff2" crossorigin
            href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/fonts/KaTeX_Math-Italic.woff2">
        <link rel="stylesheet" crossorigin
            href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.css"
            integrity="sha384-yFRtMMDnQtDRO8rLpMIKrtPCD5jdktao2TV19YiZYWMDkUR5GQZR/NOVTdquEx1j">
        <script defer crossorigin
            src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.js"
            integrity="sha384-9Nhn55MVVN0/4OFx7EE5kpFBPsEMZxKTCnA+4fqDmg12eCTqGi6+BB2LjY8brQxJ"
            onload="(() => {
                const todo = []; // don't mutate document while iterating

                function processMath(prev, code, displayMode) {
                    prev.splitText(prev.textContent.length - 1).remove();               // remove [
                    code.childNodes[0].splitText(code.textContent.length - 2).remove(); // remove \]
                    const span = document.createElement('span');
                    katex.render(code.textContent, span, {displayMode: displayMode, throwOnError: false});
                    code.parentNode.replaceChild(span, code);
                }

                for (const code of document.getElementsByTagName('code')) {
                    const prev = code.previousSibling;

                    if (prev && prev.nodeType === Node.TEXT_NODE) {
                        if (/\[$/.test(prev.textContent) && /\\\]$/.test(code.textContent)) {
                            todo.push(() => processMath(prev, code, true));
                        } else if (/\($/.test(prev.textContent) && /\\\)$/.test(code.textContent)) {
                            todo.push(() => processMath(prev, code, false));
                        }
                    }
                }

                for (const f of todo) f();
            })()"></script>
    </head>
    <body>
        <!--
            The following HTML is converted from
            ```md
            The quadratic formula is \[` x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\text{.} \]`

            The quadratic formula is \(` x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)`.
            ```    
        -->
        <p>The quadratic formula is [<code> x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\text{.} \]</code></p>
        <p>The quadratic formula is (<code> x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \)</code>.</p>
    </body>
</html>

The purpose is to take source Markdown

The quadratic formula is \[` x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\text{.} \]`

which is processed into the HTML

<p>The quadratic formula is [<code> x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\text{.} \]</code></p>

And process the block defined by \[` ... \]` into display-style math and \(` ... \)` into inline-style math. (Specific syntax domain issue: the ` is uniformly after the brackets because without at least one being inside the markdown code block, we can't tell the difference between the markdown \(`code`\) and (`code`), the latter of which we do not want to process.) An alternative syntax would be $` ... `$ for inline style and $$` ... `$$ for display style, but I went with the newer LaTeX-style brackets over the older TeX-style dollars for personal preference. (Feel free to argue why the dollars would be better, if you want!)

General review is of course welcome, but here's a few specific points to look at:

  • Node.previousSibling supposedly behaves subtly differently on Gecko and Blink. Did I handle this correctly on both engines? (EdgeHTML, disappearing as it is?)
  • All of my custom code is stuffed into katex.onload; this feels icky to me, but it avoids adding globals (other than katex).
  • KaTeX offers ECMAScript module loading support, but my attempt to use it was noticeably slower (without throttling my connection!), probably due to accidentally serializing something that didn't need to be. Is it better to use the module loading if it's available?
  • Is this a proper use of <link rel="preload">? (My daily driver is FireFox which has preloading disabled because of an issue, but Chrome shows a render time improvement when throttled to 3G speeds at least.)
  • I don't use Javascript often, so general style issues and browser support. (The markdown syntax was chosen specifically so that noscript fallback isn't too horrid.)

End note: whatever syntax is used should support display-style math within a paragraph, as convention is to treat even display-style math as part of the sentence of the paragraph containing it. Otherwise, ```math is translated into <pre><code class="language-math"> per the CommonMark spec, which is a great target for translating. Unfortunately, this precludes putting a display-style into a paragraph, and makes syntax distinctly different from inline. The bracket style supports almost this style of fencing due to variable-width fences, though: \[```/\]``` works just fine, and embeds the resulting processed <span> in a <p> as is semantically correct. (Bare $/$$ or \[/\( should also be avoided as these require support from the markdown engine to avoid mangling the contents, whereas literal code blocks give us this for free.)

\$\endgroup\$
  • \$\begingroup\$ See also pulldown-cmark's issue tracker where I wrote next-to too much about the potential for math extensions to CommonMark markdown. (It includes a slightly modified version of this snippet that handles both bracket-style and dollar-style.) \$\endgroup\$ – CAD97 Jun 4 at 4:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.