mathra 1.1.1

Equation converter: math LaTeX to a tree of meaning and accessible SVG, with locale rules (English and pt-BR).
Documentation

mathra

Accessible SVG equations from LaTeX, without a TeX install, for JavaScript, Python, Rust and the terminal. Try it in the browser.

mathra reads math LaTeX, builds a tree of meaning (what each piece is) and draws it with explicit rules, numbered and tested. Every SVG carries the phrase a screen reader says, in 16 languages, and the locale decides the form: in pt-BR 3,14 is a single number and \sin comes out as sen. All four packages run the same core, so an equation gives the same SVG, byte for byte, wherever it is converted.

$ mathra '3,14' --locale pt-BR --explain
input    3,14
locale   pt-BR
text     3,14
speech   3,14
tree     {"type":"row","children":[{"type":"number","value":"3.14"}]}
rules
  L-NUM-1   '3,14': comma between digits read as decimal separator (3.14)
  L-NUM-2   3.14 written with ',' as decimal separator (pt-BR)

$ mathra 'f(1, 5) = \sin x - 2.5' -o function.svg

Examples

Every image below is an SVG written by mathra; the LaTeX for each one is in crates/core/tests/examples.rs, which also checks that the files match the code. They are shown on a white card so they read well on dark pages too.

Famous equations

Beyond the basics

Continued fractions, matrices with dots, cases, binomials, labeled braces, chemistry (\ce), units (\SI), commutative diagrams, the pt-BR locale and computed long division:

What a screen reader hears

Each SVG carries the equation as speech in its <title>:

Equation Speech (--speech)
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} x equals fraction, negative b plus or minus square root of b squared minus 4 a c, end root, over 2 a, end fraction
\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} sum from n equals 1 to infinity of 1 over n squared equals pi squared over 6
\lvert a \rvert + \lvert b \rvert \ge \lvert a + b \rvert absolute value of a plus absolute value of b is greater than or equal to absolute value of, a plus b, end absolute value
\divchave{247}{5} with --locale pt-BR divisão armada de 247 por 5: quociente 49, resto 2

Broken LaTeX

By default mathra draws what it can and returns warnings for what it had to fix (E-REC-1 to E-REC-9, in spec/tolerance.md): unknown commands, unbalanced braces, double scripts, leftover JSON escapes such as \n or \u00b2. Warnings show up in --explain, on stderr, in Output::warnings and in the SVG's <metadata>. With --strict, mathra refuses invalid input instead, like TeX.

$ mathra '\vertA\vert = C_{16,2}' --text
mathra: warning E-REC-1: column 1: unsupported command: \vertA; read as \vert A
|A| = C_(16, 2)

LaTeX, MathML and AsciiMath

LaTeX is the default. MathML (what Word, Google Docs, MathType and MathJax export) and AsciiMath (sum_(i=1)^n i^2) are read into the same tree of meaning, so they come out with the same drawing and the same speech. MathML passed as LaTeX, starting with <math, is recognized on its own. What each notation covers is in spec/input.md, and the reference page shows every construction, in AsciiMath (the tables of asciimath.org), LaTeX (by topic, as Overleaf's Mathematics articles), the science packages and MathML, each drawn and in braille, live in the browser.

$ mathra 'sum_(i=1)^n i^2 = (n(n+1)(2n+1))/6' --from asciimath -o sum.svg
$ mathra '<math><msqrt><mi>x</mi></msqrt></math>' --speech      # square root of x

Braille

Every equation also comes out in braille, as Unicode braille cells: in the CMU (Código Matemático Unificado) for Portuguese and Spanish, and in Nemeth for the other languages, or in either one by choice. Both follow their official documents, and the tests check them against the documents' own examples; what is covered and what is still open is in spec/braille.md. The braille also goes into the SVG's <metadata>. It was compared with MathCAT row by row, with the official documents deciding every disagreement (docs/braille-review.md, and the Braille tab of the reference page); it still needs a braille transcriber's review before it reaches students.

$ mathra '\frac{3}{4} + \sqrt{x}' --locale pt-BR --braille     # ⠼⠒⠙⠖⠫⠱⠭
$ mathra 'x^2 + 1' --braille                                     # ⠭⠘⠆⠐⠬⠂ (Nemeth)
$ mathra 'x^2 + 1' --braille --braille-code cmu                  # ⠭⠡⠼⠃⠖⠼⠁

Locales

16 languages: en (default), pt-BR, es, fr, de, it, ru, zh, ja, ko, hi, ar, id, tr, nl and pl, with --locale (locale="fr" in Python, { locale: "fr" } in JavaScript). The locale changes how the equation is read, written and spoken: in pt-BR 3,14 is one number with a decimal comma, 1.000 is one thousand, \sin is written sen and ]a, b[ is an open interval; in fr \frac{3}{4} is spoken "trois quarts"; in zh \frac{1}{2} is "2 分之 1". Each language is a JSON file in crates/core/src/locales/, described in spec/locales.md with how to add one. English and pt-BR are reviewed; the other languages still need a native speaker's review of the speech. Every rule that depends on the locale is numbered in spec/rules.md.

What the SVG carries

Besides the outlines, every SVG follows the contract in spec/svg.md: speech for screen readers, the source equation, the tree and the braille in <metadata>, color inherited from the page, reused glyphs, a text layer for selection and unique ids per equation. The same input always produces the same bytes.

Install and use

Where Install Registry
JavaScript and TypeScript: Node, bundlers, browsers npm install @mathlabs/mathra npm
Python 3.9+: wheels for Linux, macOS and Windows pip install mathra PyPI
Terminal cargo install mathra-cli crates.io
Rust cargo add mathra crates.io · docs.rs

JavaScript and TypeScript

Types included. In Node it works right away; in browsers and bundlers, await init() once. More in npm/README.md.

import { init, texToSvg, render } from "@mathlabs/mathra";

await init();                                                    // browsers and bundlers; Node needs nothing
const svg = texToSvg("3,14", { locale: "pt-BR" });
const { speech, warnings } = render(String.raw`\frac{3}{4}`, { locale: "fr" });   // "trois quarts"
render("sqrt(x)/2", { input: "asciimath" });                     // also "mathml"
render("x^2", { locale: "pt-BR" }).braille;                      // "⠭⠡⠼⠃" (CMU); { braille: "nemeth" } forces the code

Python

import mathra

svg = mathra.tex_to_svg(r"\sin x = 0.5")                 # <title>…sine of x equals 0.5</title>
r = mathra.render(r"\frac{3}{4}", locale="fr")            # dict: svg, speech, text, tree, applied, warnings
r["speech"]                                               # 'trois quarts'
mathra.tex_to_svg(r"\vertA\vert", strict=True)            # ValueError, like TeX
mathra.render("sqrt(x)/2", input="asciimath")             # also input="mathml"
mathra.render("x^2", locale="pt-BR")["braille"]           # '⠭⠡⠼⠃' (CMU); braille="nemeth" forces the code

Types included; more on PyPI.

Terminal

$ mathra 'x \le 2.75; y \ge 1' -o inequality.svg
$ mathra '3,14 + \sin x' --locale pt-BR
$ mathra 'x = -1' --speech        # x equals negative 1
$ echo 'a + b' | mathra --tree
$ mathra 'a/b + sqrt(c)' --from asciimath
$ mathra '\frac{3}{4}' --locale pt-BR --braille   # ⠼⠒⠙
$ mathra --batch -o out/ < equations.txt

Rust

let r = mathra::convert("3.14", &mathra::Options::default())?;
println!("{}", r.svg);
println!("{}", r.speech);

What it converts

Layout follows the parameters of the embedded font's MATH table.

  • Fractions (\frac, \dfrac, \tfrac, \cfrac, \over), binomials, roots with an index
  • Scripts, primes (f'), limits (\lim, \sum, \prod, \int and family, \limits)
  • \left … \middle … \right, \big and family, delimiters assembled from parts
  • Matrices (matrix, pmatrix, bmatrix, Bmatrix, vmatrix, Vmatrix, smallmatrix), cases, rcases, aligned, align, gathered, split, array with |, @{}, \hline, \cline
  • Accents (\hat, \vec, \bar, \dot, \widehat, \overrightarrow, \arc…), \underrightarrow and family, \overline, \underline, \overbrace/\underbrace and \overbracket/\underbracket with labels, \xrightarrow and family, \overset, \underset, \stackrel
  • The amssymb symbols, negations included (\nleq, \nsim, \nRightarrow, \lneqq…), and integrals up to \iiiint, \idotsint and \varoiint
  • \text with $…$ and old-style accents (\'e, \c{c}), \textsc, alphabets (\mathbb, \mathcal, \mathfrak, \mathbf, \boldsymbol, \mathsf, \mathtt), old \bf/\rm/\tt
  • \cancel, \boxed, \textcircled, \color, \colorbox, \fcolorbox, \phantom, \pmod, \operatorname, spaces (\, \quad \hspace)
  • siunitx (\SI, \qty, \si, \num, \ang with minutes and seconds, ranges, lists and products), HTML entities coming from editors
  • mhchem (\ce, \pu): formulas, charges, radicals, isotopes, hydrates, bonds, decimal and fractional coefficients, arrows with labels (->[\Delta], <=>[K_w]), \frac with formulas
  • Substitution macros: \newcommand, \renewcommand, \def, \let, \DeclareMathOperator, \DeclarePairedDelimiter, \newenvironment
  • Packages: physics (derivatives, \dd, \eval, \expval, \mel, \comm, \grad, \curl, \laplacian, \vb, \vu, \order, \Tr, \qty), braket (\ket, \braket, \Set), tensor (\tensor, \indices), mathtools (\prescript, rcases, \overbracket), xfrac, units, \genfrac, \substack, \sideset, \smash, \mathclap, \multicolumn, \intertext, \hdotsfor, \raisebox, \scalebox, \rule, \verb; old syntax \cases, \matrix, \eqalign
  • amscd commutative diagrams (\begin{CD})
  • mathra extensions: \divchave{247}{5} (long division, Brazilian layout) and \contaarmada{+}{347}{285} (column arithmetic), both computed

Out of scope, by the rule in spec/boundary.md (mathra is for equations): TikZ, tikz-cd, xymatrix, \chemfig and TeX programming (\ifx, \catcode). Not yet: automatic line breaking, MathML and PNG output (MathML input is supported), content MathML, mhchem's text mode (\ce{Hydrogen + Oxygen -> Water} is read as formulas).

Benchmark

corpus/ holds 116 hand-written constructions, one or a few per command family and package. One run of cargo run --release -p mathra --example bench (arm64, Apple M5, rustc 1.99.1):

Measure Value
Constructions 116
Strict: converted 109 (94.0%)
Tolerant: no warning 109 (94.0%)
Tolerant: with warnings 7 (6.0%)
Tolerant: error 0 (0.0%)
Time per equation, mean 17.9 µs
Time per equation, p95 46.5 µs

Times vary by a few microseconds between runs. The 7 that strict mode refuses are not equations: TeX programming (\ifx, \expandafter, \catcode) and diagrams (tikz-cd, xymatrix, \tikz, \chemfig). Tolerant mode draws them as text with a warning.

Compared with KaTeX and MathJax

bench/compare.mjs runs the three libraries on the same equations in Node (cd bench && npm install && npm run compare; Node 24, Apple M5, katex 0.16.22, mathjax-full 3.2.2). "Clean" means no error and no command drawn in red. The playground runs the same comparison in your browser.

mathra (wasm) MathJax (SVG) KaTeX (HTML)
Gallery, 18 equations: clean 18 15 15
Gallery: time per equation, mean 121 µs 643 µs 69 µs
Corpus, 116 constructions: clean 109 70 62
Output per equation, gzip 2.32 KB 2.43 KB 0.95 KB + CSS and fonts
Download to render in a browser, gzip 926 KB 660 KB 77 KB + up to 254 KB of fonts

KaTeX is faster and lighter to ship, but its output is HTML that needs its stylesheet and fonts, not a standalone image. Against MathJax's SVG, the closest equivalent, mathra is about 5× faster with output of similar size. Most of mathra's download is the embedded font (628 KB gzip). The corpus was written while building mathra, so it favors it; the three gallery equations the others miss use \SI, \sen and \divchave.

Layout

crates/core          tree, rules, layout, embedded font, outputs (SVG, speech, text)
crates/core/src/locales   the 16 languages, one JSON file each (spec/locales.md)
crates/core/examples bench (the benchmark above) and banner (the image above)
crates/cli           `mathra` binary
crates/python        Python module (PyO3 + maturin)
crates/wasm          WebAssembly bindings (wasm-bindgen), used by npm/ and site/
npm/                 the npm package: JavaScript API, types, build and tests
site/                the playground and docs on GitHub Pages
bench/               mathra next to KaTeX and MathJax
spec/                numbered rules (rules.md), the SVG contract (svg.md), languages (locales.md)
corpus/              hand-written constructions used by the benchmark
docs/examples/       the README gallery, generated by tests/examples.rs

Development

$ cargo test                 # one test per rule, with the rule id in its name
$ cargo clippy --all-targets
$ cargo run -p mathra-cli -- '3,14' --locale pt-BR --explain
$ npm/build.sh && npm/test/run.sh     # the npm package, packed and installed as users get it
$ site/build.sh                       # then open site/index.html
$ scripts/release.sh --dry-run        # what a release would publish

Releasing. Each package has its own version: Cargo.toml (crates mathra and mathra-cli), npm/package.json and crates/python/Cargo.toml. Bump the ones to release in a pull request, merge it, then run scripts/release.sh on main: it tags every version not yet on its registry (v*, npm-v*, py-v*) and pushes the tags at once; each tag runs its workflow, which publishes with trusted publishing. A rerun of a half-done release skips what is already out.

Every pull request runs the same checks (.github/workflows/ci.yml): fmt, clippy, the Rust, wasm, npm and Python tests.

To change behavior, write the rule in spec/, cite its id in the code and add a test with the same id.

The tests cover 100% of the lines of the core, the CLI and the Python module. docs/tests.md explains how to run each suite (including wasm on Node and pytest), what each file checks, the coverage per file and the problem inputs already tested.

The banner above is drawn by mathra itself (cargo run -p mathra --example banner); tests/banner.rs fails if it goes stale.

Licenses

Code: your choice of MIT or Apache-2.0, as is common in the Rust ecosystem. Embedded font: STIX Two Math, SIL Open Font License 1.1 (crates/core/fonts/OFL-STIXTwoMath.txt).