kern

Typst math, in the browser.

kern's job is to render Typst math syntax to MathML that matches what Typst itself would produce. This page tracks which constructs are supported, where kern diverges, and how we measure correctness.

Last reviewed against Typst commit de6f40097 (0.14.2). Run pnpm -C scripts check-parity to regenerate the structural diff report.

ok a passing test exercises the construct. partial renders but diverges from Typst in a documented way. no not yet supported. skip requires the full Typst layout engine.

Atoms & symbols

ConstructStatusNotes
x, n (italic var)ok<mi>
sin, cos, logokUpright, with trailing thin space
42, 3.14ok<mn>
alpha, Sigma, omegaokFrom the symbol table
dif / DifokThin space + upright d, used as integral f dif x
plus.minus, arrow.r.longokDotted names traversed
Hex literals (0xff)noTypst's code-mode form, not in math

Style

ConstructStatusNotes
cal(A)okmathvariant="script"
bb(R)okmathvariant="double-struck"
frak(g)okmathvariant="fraktur"
bold(x)okBold-italic for single letters
italic(x), upright(d), sans, monook
scr(A)partialAliased to cal; MathML only exposes one script variant

Structure

ConstructStatusNotes
frac(a, b), a / bok<mfrac>
binom(n, k)oklinethickness="0" inside stretched parens
x^2, x_i, x_i^2okmsup/msub/msubsup
sqrt(x), root(n, x)okmsqrt/mroot
Primes (f', f'')okPromoted to a sup atom
Explicit attach(b, tl: ..., br: ...)noPre-scripts via <mmultiscripts> still pending
stretch(glyph, size: ...)noNeeds glyph-variant selection from the OpenType MATH table

Limits & scripts

ConstructStatusNotes
sum_(i=0)^n (display)okmunderover
sum_(i=0)^n (inline)okmsubsup
integral_0^1 (display)okStays msubsup per Typst's rule for integrals
lim_(x -> oo)okmunder in display
limits(x), scripts(x)ok
op("text", limits: true)ok

Delimiters & fences

ConstructStatusNotes
(x), [x], {x} (plain)okBody height, stretchy="false"
Auto-sized parens around tall contentokBare (a/b) upgrades to stretchy fences; mirrors Typst's lr()
lr((...)), lr([...])okstretchy="true" symmetric="true" for Safari parity
abs(x), norm(v)okStretchy symmetric bars
floor(x), ceil(x)ok
Conditional bar inside lr() (P(A | B))ok| becomes a stretchy symmetric fence so Safari sizes it with the parens
round(x)noRenders as a generic call
mid(x)partialWrapped as a relation class; doesn't yet stretch to enclosing lr()
Mismatched fences (lr([x/y)))partialUse explicit lr(); bare mismatched delimiters error at parse

Matrices & multi-line

ConstructStatusNotes
vec(a, b, c)ok
mat(a, b; c, d)okComma = column, semicolon = row
cases(...)okLeft brace, left-aligned cells
mat(..., delim: "[")okHonored for (), [], {}, |, ||
mat(..., augment: 1)okInteger or paren-list of integers draws vertical rules after the given column(s). Hline support via the augment dict shorthand is queued
mat(..., gap: ...)okgap, row-gap, column-gap in em units are honored via CSS variables
& alignment across linesokSingle-row & stays an inline align marker; with \, & splits into <mtd> cells with alternating right/left alignment
Multi-line equations with \ok\ at end-of-token (or \\) becomes a row break; the whole construct renders as <mtable class="kern-eqarray kern-aligned">
Equation numbering / supplementsskipBelongs to Typst's figure/layout system

Accents & under/over

ConstructStatusNotes
hat, tilde, bar, dot, ddotokmover accent="true"
arrow(x), arrow.l(x), arrow.l.r(x)okVector accent and harpoons
acute, grave, breve, caron, circle, macronok
overline, underlineok
overbrace, underbrace (+ annotation)ok
overparen, underparen, overbracket, underbracketok
Bottom-attached accents via combining classnoTypst flips combining-Below characters; kern keeps every accent on top

Spacing, classes, cancellation

ConstructStatusNotes
thin, med, thick, quad, qquad, wideokwide = 2 em
class("rel", x) & friendspartialSingle-atom bodies only
display, inline, script, sscriptok<mstyle> with displaystyle/scriptlevel
cancel, bcancel, xcancelokmenclose notation
Text in math: "word"ok<mtext> with thin-space padding

How we measure correctness

Typst's compiler is the spec. Pixel-level parity is not achievable (different fonts, hinting, sub-pixel positioning), so the oracle is structural MathML: tags, nesting, and the attributes that affect layout.

The script at scripts/check-parity.ts reads tests/corpus.txt, renders every entry through kern, and (when typst compile --features html exposes equation MathML, currently pending in Typst 0.14.2) renders the same entries through Typst, then runs a structural diff that ignores class, style, and presentation-only attributes.

The latest run is at parity-report.html. Visual regression tests live under packages/kern/test/visual/ and act as a regression gate, never as a correctness oracle: thresholds are constants committed to the repo, and failures dump the Typst baseline, the kern candidate, and a pixelmatch diff to disk for human review.

Why kern doesn't bundle Typst's WASM compiler

The MathML emitter we measure against is Rust code in typst/crates/typst-html/src/mathml.rs. It depends on Typst's Engine, layout, font, and IR crates - shipping it to the browser would multiply kern's bundle by an order of magnitude and couple every release to Typst's. Modern browsers already do glyph-level layout from <mfrac> / <msqrt> / etc.; we only need to emit the right MathML tree.

Instead, we treat Typst's emitter as the structural oracle and keep kern a small, JS-native renderer. The parity gap between the two trees is the north star metric; the parity report tracks it expression by expression.