Math on the web without JavaScript: LaTeX to MathML in Markdown, with prices left alone
The Markdown package in the 561 Group's engineering tree, version 0.5.6, renders LaTeX to MathML on the server using the temml library, with no client script, and attaches the original TeX to every formula as an application/x-tex annotation. Inline $…$ math is opt-in per document and follows Pandoc's rules, so a sentence containing "$9.00 and $12.00" or "$5 and $6" is never mistaken for a formula. This article shows the real output of the renderer for each case.
Why render math as MathML instead of with JavaScript?
Most sites that show equations load a client-side library that finds TeX in the page and typesets it after load. That costs a script download, shifts the layout when the equations appear, shows raw TeX to anything that does not run the script (including some crawlers and every plain-text projection), and hides the formula from assistive technology until the library finishes. MathML is the web platform's own format for mathematics, and current Chrome, Firefox and Safari render it natively. Producing it at build time means the equation is in the HTML the server sends. Readers get it with no script, a crawler sees the structure, and a screen reader can use the markup.
The temml library, a pure-JavaScript TeX-to-MathML converter, runs in the build, not in the reader's browser. The package pins temml 0.14.0.
What does the renderer emit?
The delimiters $$…$$, \[…\] (display) and \(…\) (inline) are always on. Rendering \(a_i\) produces:
<span class="math math-inline"><math><semantics><msub><mi>a</mi><mi>i</mi></msub>
<annotation encoding="application/x-tex">a_i</annotation></semantics></math></span>and \[ E = mc^2 \] produces a display block:
<div class="math math-display"><math display="block" class="tml-display" style="display:block math;">
<semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn class="tml-sml-pad">2</mn></msup></mrow>
<annotation encoding="application/x-tex">E = mc^2</annotation></semantics></math></div>(Line breaks added for display; these are outputs from running the 0.5.6 source.) Inline math is a span.math.math-inline, display math is a div.math.math-display (or a span with the same classes when it sits inside running text), and every rendering has the <semantics> wrapper with the TeX in an <annotation encoding="application/x-tex">.
Why keep the TeX? Because MathML is verbose and lossy as a quotation. A reader who wants to copy the formula into their own document wants E = mc^2, not twelve elements. A search or answer engine that extracts text from the page can take the annotation, which is the author's own notation. And an editor that reopens the page can round-trip it. In the parsed representation used for tables of contents and plain-text exports, a math node carries tex and text fields, both the TeX source, so headings and other text projections read the TeX instead of a tree of MathML elements.
How do prices avoid becoming math?
A dollar sign is both the currency symbol and the most common inline math delimiter. A renderer that treats every pair of $ as math turns "costs $9.00 and $12.00" into an italicized, spaceless run, which is a real defect in any commerce or finance text, and the market this team operates quotes $9.00 and $12.00 for publications and $0.0012 for API calls.
The package solves it in two layers.
- Inline dollar math is off unless a document asks. A document turns it on with front matter
math: true, and the caller passes that flag to the parser. Without it$is never interpreted, so prices stay prices on every page that did not opt in. The always-on delimiters,$$,\[and\(, do not collide with ordinary currency. - When it is on, Pandoc's `tex_math_dollars` rules apply. The opening
$must be followed by a non-space character, the closing$must be preceded by a non-space character and must not be followed by a digit, and\$is a literal dollar.
We ran the 0.5.6 parser with math: true over four strings. Each result is the rendered HTML:
| Input | Output |
|---|---|
It costs $5 and $6 in total. | <p>It costs $5 and $6 in total.</p> (no math) |
Totals: $9.00 and $12.00. | <p>Totals: $9.00 and $12.00.</p> (no math) |
a sentence with the inline code span $x$ | the <code>$x$</code> element, untouched |
Pythagoras: $x^2+y^2=z^2$ holds. | a span.math.math-inline with the MathML of x² + y² = z² |
With math not set the last input stays literal: <p>Pythagoras: $x^2+y^2=z^2$ holds.</p>. The first row is the one that matters. In "$5 and $6", the first $ is followed by 5, but the candidate closing $ is preceded by a space, so it cannot close, and the text is left alone.
The trade-off is real. Under these rules a closing dollar sign immediately followed by a digit does not close a formula, so $x$2 stays literal, and a formula may not have a space just inside either delimiter. They are the cost of safety, and they are the same costs Pandoc users already know.
What gets skipped?
The scanner does not look inside inline code spans, fenced code blocks, link URLs or autolinks, so a code sample showing $x$ is displayed as written. Display blocks may span lines but not a blank line. Math inside **strong** or *emphasis* is not parsed; those spans remain literal text. This is a limit, not a feature: if you need bold math, put the bold inside TeX with \mathbf{}.
What happens when the TeX is wrong?
TeX that temml refuses renders as a <code class="math-error"> element holding the source, and renderBody never throws on it. Rendering $$\frac{1}{$$ gives:
<div class="math math-display"><code class="math-error">\frac{1}{</code></div>A reader sees the offending source and the rest of the page survives. The renderer also runs temml with trust: false and strict: 'ignore', so a formula cannot invoke URL-loading or HTML-injecting commands. That matters because math is the only HTML the renderer does not escape: it is the one place where the output contains markup derived from user text. The package's comment states it directly, "TeX renders to MathML through temml; it is the only HTML not escaped."
Is there a breaking change to know about?
Yes. Before 0.5.6, the $$…$$, \[…\] and \(…\) delimiters rendered as literal text. After it they render as math. If an existing document used \( in prose without meaning math, it will now be parsed. The package's release note flags it as a behavior change, and so do we.
Is this running on this site?
Not yet. This site's publication pipeline uses the engine described in How this site is built, and when we built a test page containing $$…$$ and math: true through that pipeline, the delimiters came through as text and no MathML was produced. We say so because it would be easy to write about equations on a page that cannot show one. The capability exists in the Markdown package and we verified its output directly; wiring it into this particular publication path is separate work, and we will not claim it until a page here renders an equation.
Why does this matter to answer engines?
A formula stored as an image is invisible to a language model. A formula stored as client-rendered TeX is visible only if the crawler runs the script. A formula stored as MathML with its TeX annotation is visible as structure and as the author's notation, in the HTML the server sent. The same logic motivates the rest of the publishing stack: put the meaning in the markup, keep a plain-text twin, and let the page stand on its own (how this site is built). For the four-valued logic operations that would be natural to typeset, see Belnap's logic as an API.