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 from "@mathlabs/mathra";
await ; // browsers and bundlers; Node needs nothing
const svg = ;
const = ; // "trois quarts"
; // also "mathml"
.; // "⠭⠡⠼⠃" (CMU); { braille: "nemeth" } forces the code
Python
= # <title>…sine of x equals 0.5</title>
= # dict: svg, speech, text, tree, applied, warnings
# 'trois quarts'
# ValueError, like TeX
# also input="mathml"
# '⠭⠡⠼⠃' (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 = convert?;
println!;
println!;
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,\intand family,\limits) \left … \middle … \right,\bigand family, delimiters assembled from parts- Matrices (
matrix,pmatrix,bmatrix,Bmatrix,vmatrix,Vmatrix,smallmatrix),cases,rcases,aligned,align,gathered,split,arraywith|,@{},\hline,\cline - Accents (
\hat,\vec,\bar,\dot,\widehat,\overrightarrow,\arc…),\underrightarrowand family,\overline,\underline,\overbrace/\underbraceand\overbracket/\underbracketwith labels,\xrightarrowand family,\overset,\underset,\stackrel - The amssymb symbols, negations included (
\nleq,\nsim,\nRightarrow,\lneqq…), and integrals up to\iiiint,\idotsintand\varoiint \textwith$…$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,\angwith 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]),\fracwith 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).