Render maths with KaTeX
Draw the formulas a model writes — $…$, $$…$$, \(…\), \[…\] — while the reply streams, with @aparte/plugin-katex.
<aparte-chat-bubble message-id="a1" data-role="assistant" streaming></aparte-chat-bubble>
<script>
// A formula is drawn as it arrives; one that does not parse yet keeps its last drawing.
const bubble = document.querySelector('aparte-chat-bubble');
bubble.setSegments([{ id: 's1', type: 'text', content: '' }]);
const text = 'The area under $x^2$ from 0 to 1:\n\n$$\n\\int_0^1 x^2\\,dx = \\frac{1}{3}\n$$\n\nAnd a price stays a price: it costs $5 and $10.';
let i = 0;
const tick = setInterval(() => {
bubble.appendToSegment('s1', text.slice(i, i + 3));
i += 3;
if (i >= text.length) {
clearInterval(tick);
bubble.updateSegment('s1', { isStreaming: false });
bubble.removeAttribute('streaming');
}
}, 60);
</script>Install and register
Section titled “Install and register”npm install @aparte/plugin-katex kateximport 'katex/dist/katex.min.css';
import { setupKatex } from '@aparte/plugin-katex';
setupKatex();A formula is drawn by KaTeX, loaded on the first one. Its stylesheet is yours to import, because its fonts are relative URLs your bundler resolves; without it each formula shows twice, and the plugin says so once in the console.
Which Markdown provider
Section titled “Which Markdown provider”Maths is recognised by the Markdown provider, before Markdown can read it: to Markdown, \, is an
escape and a*b*c is emphasis. @aparte/plugin-marked, streaming or not, and
@aparte/plugin-streaming-markdown do it on their own once setupKatex() has run — nothing else
to configure. To keep dollars as text whatever is registered, pass math: false to the marked
provider.
Both read maths by the rules in the table below; they differ in when a formula appears while
the reply streams. @aparte/plugin-marked/streaming draws a $$, \[ or \( formula as it
arrives, and a single $ once it closes. @aparte/plugin-streaming-markdown draws a display
formula opened by $$ or \[ on its own line as it arrives, and any other once its closer has
come. The finished reply is drawn by the one-shot provider — marked, in the
recommended setup.
A provider of your own hands maths over by writing the markup remark-math already writes:
<code class="language-math math-inline">TeX</code> inline, and
<pre><code class="language-math math-display">TeX</code></pre> for a block.
What counts as maths
Section titled “What counts as maths”| written | drawn as |
|---|---|
$x^2$, \(x^2\) | inline |
$$x^2$$, \[x^2\], on their own lines or inside a sentence | display |
$5 and $10 | text |
A single $ follows Pandoc’s rule: a character right after the opening $, one right before the
closing $, and no digit right after it. A price stays a price. Code always wins: a $ in
backquotes or a fence is code.
While the reply streams
Section titled “While the reply streams”A formula is drawn as it arrives. Half a formula — \frac{1}{ — does not parse, so the last
drawing that did stays on screen until the next one does, and nothing flashes red mid-stream. A
single $ is drawn only once it closes, since until then it could be a price. Once the reply is
finished, a formula that still does not parse shows its TeX, quietly, with KaTeX’s message as a
tooltip.
What a model cannot do with it
Section titled “What a model cannot do with it”trust is always off, so a formula cannot carry a link (\href) or set a class or a style
(\htmlClass); maxSize and maxExpand are capped, so a size cannot pass 10em and a macro
cannot expand forever; KaTeX does not cap a negative shift, so a formula taller than 100em —
\raisebox{-10000em}{x}; a 30-row matrix is under 40 — shows as its TeX; every formula is painted
inside its own box, so \smash, \raisebox or a negative \kern cannot lay one over the chat
around it; and every formula gets a fresh set of macros, so a \gdef in one reply cannot reach
the next. KaTeX’s own options pass through setupKatex({ katex: { … } }); these are kept
whatever they say.
Bring your own KaTeX
Section titled “Bring your own KaTeX”@aparte/plugin-katex/core imports nothing from katex: hand it the instance, or a function that
loads it.
import katex from 'katex';
import { setupKatexWith } from '@aparte/plugin-katex/core';
setupKatexWith(katex, { live: false });KatexOptions takes live (false draws a formula once it has closed), katex and maxLength.
Any object shaped like KatexLike will do. Both setups return a function that removes the renderer.