// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package mathml import ( "bytes" "encoding/xml" "testing" ) // corpus is the project's own hand-written corpus: every case states a TeX // input and the exact MathML Core the engine produces for it. var corpus = []struct { name string input string want string }{ {"single letter", "x", `x`}, {"digits", "42", `42`}, {"decimal", "1.5", `1.5`}, {"sum", "a+b", `a+b`}, {"greek", `\alpha + \beta`, `α+β`}, {"uppercase greek", `\Gamma`, `Γ`}, {"relation", `a \leq b`, `a≤b`}, {"operator with limits", `\sum_{i=1}^{n} i`, `∑i=1ni`}, {"integral keeps side scripts", `\int_0^1 x`, `∫01x`}, {"fraction", `\frac{a}{b}`, `ab`}, {"fraction shorthand", `\frac12`, `12`}, {"binom", `\binom{n}{k}`, `(nk)`}, {"over infix", `a \over b`, `ab`}, {"choose infix", `a \choose b`, `(ab)`}, {"sqrt", `\sqrt{x}`, `x`}, {"root with index", `\sqrt[3]{x}`, `x3`}, {"sub and sup", `x_1^2`, `x12`}, {"single digit script", `x^10`, `x10`}, {"group", `{xy}`, `xy`}, {"left right", `\left( x \right)`, `(x)`}, {"left right invisible", `\left. x \right)`, `x)`}, {"function", `\sin x`, `sinx`}, {"limit under", `\lim_{x \to 0}`, `limx→0`}, {"accent", `\hat{x}`, `x^`}, {"vec accent", `\vec{v}`, `v→`}, {"overline", `\overline{x}`, `x‾`}, {"text", `\text{if}`, `if`}, {"text keeps spaces", `\text{hello world}`, `hello world`}, {"mathrm", `\mathrm{d}x`, `dx`}, {"mathbb", `\mathbb{R}`, `R`}, {"operatorname", `\operatorname{sgn}`, `sgn`}, {"spacing", `a \, b`, `ab`}, {"quad", `a \quad b`, `ab`}, {"prime", `x'`, `x′`}, {"escaped brace", `\{x\}`, `{x}`}, {"overset", `\overset{a}{b}`, `ba`}, {"matrix", `\begin{matrix} a & b \\ c & d \end{matrix}`, `abcd`}, {"pmatrix", `\begin{pmatrix} a \\ b \end{pmatrix}`, `(ab)`}, {"cases", `\begin{cases} a & b \\ c & d \end{cases}`, `{abcd`}, {"aligned", `\begin{aligned} a &= b \\ c &= d \end{aligned}`, `a=bc=d`}, {"array spec", `\begin{array}{c|l} a & b \end{array}`, `ab`}, {"macro", `\newcommand{\R}{\mathbb{R}} \R`, `R`}, {"macro with argument", `\newcommand{\ip}[2]{\langle #1, #2 \rangle} \ip{a}{b}`, `⟨a,b⟩`}, {"def", `\def\dx{\mathrm{d}x} \dx`, `dx`}, {"escaping in text", `\text{a < b & c}`, `a < b & c`}, {"unicode letter", `λ`, `λ`}, {"pmod", `x \pmod n`, `x(modn)`}, {"colon relation precomposed", `f \coloneqq g`, `f≔g`}, {"colon relation linear", `f \coloneq g`, `f:−g`}, {"colon relation double", `f \Coloneqq g`, `f∷=g`}, {"eqqcolon", `a \eqqcolon b`, `a≕b`}, } func TestCorpus(t *testing.T) { for _, tc := range corpus { t.Run(tc.name, func(t *testing.T) { got := string(Render([]byte(tc.input), false)) want := `` + tc.want + `` if got != want { t.Errorf("input %q\ngot: %s\nwant: %s", tc.input, got, want) } }) } } // degrade holds the inputs whose constructs lie outside the mappable // surface: nothing may disappear, everything stays as verbatim source in // a marked element. var degrade = []struct { name string input string }{ {"unknown command", `\tikz{x}`}, {"unsupported environment", `\begin{tikzpicture} \draw (0,0); \end{tikzpicture}`}, {"unclosed group", `{x`}, {"unclosed environment", `\begin{matrix} a \end{pmatrix}`}, {"reserved character", `a # b`}, {"stray ampersand", `a & b`}, {"stray row end", `a \\ b`}, {"recursive macro", `\newcommand{\x}{\x}\x`}, {"boxed", `\boxed{x}`}, {"sideset", `\sideset{_a^b}{_c^d}\sum`}, {"tag", `\tag{1} x`}, } func TestDegradation(t *testing.T) { for _, tc := range degrade { t.Run(tc.name, func(t *testing.T) { out := Render([]byte(tc.input), false) if !bytes.Contains(out, []byte("")) { t.Errorf("input %q produced no merror:\n%s", tc.input, out) } if !wellFormed(out) { t.Errorf("input %q produced malformed XML:\n%s", tc.input, out) } }) } } func TestVerbatimSourceKept(t *testing.T) { out := string(Render([]byte(`a + \unknowncmd b`), false)) want := `\unknowncmd` if !bytes.Contains([]byte(out), []byte(want)) { t.Errorf("degraded construct lost its source:\n%s", out) } } func TestDeterministicOutput(t *testing.T) { source := []byte(`\frac{1}{2}\sqrt[3]{x}\begin{pmatrix} a & b \\ c & d \end{pmatrix}\sum_{i=1}^{n} i`) first := Render(source, true) for range 5 { if next := Render(source, true); !bytes.Equal(first, next) { t.Fatal("Render of the same source differs between calls") } } } // wellFormed checks that the output parses as XML. func wellFormed(out []byte) bool { dec := xml.NewDecoder(bytes.NewReader(out)) for { _, err := dec.Token() if err != nil { return err.Error() == "EOF" } } } func TestAllCorpusWellFormed(t *testing.T) { for _, tc := range corpus { if out := Render([]byte(tc.input), false); !wellFormed(out) { t.Errorf("input %q produced malformed XML:\n%s", tc.input, out) } } } func TestDisplayBlock(t *testing.T) { if got := string(Render([]byte("x"), true)); got != `x` { t.Errorf("display form = %s", got) } } func TestMacroDepthBounded(t *testing.T) { // A macro that doubles itself grows past any depth limit; the parser // must degrade rather than hang or exhaust memory. out := Render([]byte(`\newcommand{\a}{\a\a}`+"\n"+`\a`), false) if !bytes.Contains(out, []byte("")) { t.Errorf("unbounded macro produced no merror:\n%s", out) } }