The missing foundation for LaTeX formula processing.
TeXForm parses, edits, and transforms LaTeX math, built on a structured knowledge base of 530+ command and environment specifications across 7 LaTeX packages, validated against MathJax, KaTeX, and XeTeX. One Rust core, available in Rust, Python, and JavaScript.
Programs that process LaTeX formulas — cleaning OCR training data, comparing formulas for equivalence, preparing pretraining corpora, building math editors — keep reinventing the same fragile layer: regular expressions and one-off heuristics that guess what \frac, \over, or \quantity mean. The hard part was never the string manipulation. It is knowing the argument structure of every command well enough to work on formulas as syntax trees instead of text.
TeXForm is that missing layer. It parses formulas against a real command knowledge base, gives you an editable document tree, and normalizes formulas into canonical forms tuned for specific downstream uses.
| Knowledge-driven parser | Editable document tree | Profile-based transform engine |
|---|---|---|
|
Formulas parse into real syntax trees, driven by xparse-style argument specifications for every command in |
Parsing produces a |
Normalization has no single correct answer, so TeXForm never imposes one true form. You pick a |
Each Profile targets one downstream scenario:
| Profile | Target scenario |
|---|---|
Authoring |
Polished author-facing output; stylistic choices kept. |
Faithful |
Same rendered formula; convenience macros expanded into universal forms. |
Corpus |
Training-data normalization; layout hints dropped. |
Equiv |
Aggressive canonicalization for formula equivalence comparison. |
The same input, normalized under different profiles (real engine output):
| Input | Authoring |
Corpus |
Corpus, rendered |
|---|---|---|---|
a \over b |
\frac { a } { b } |
\frac { a } { b } |
|
\substack{a \\ b} |
\substack { a \\ b } |
\begin {subarray} {c} a \\ b \end {subarray} |
$\begin {subarray} {c} a \ b \end {subarray}$ |
\dv{f}{x} |
\dv { f } { x } |
\frac { \mathrm { d } f } { \mathrm { d } x } |
|
\ket{\psi} |
\ket { \psi } |
\left \vert \psi \right \rangle |
Every profile modernizes legacy syntax like \over. Authoring keeps the author's shorthand; Corpus expands it into universal forms that render on any vanilla MathJax or KaTeX deployment with no extra packages — including right here on GitHub, where inputs like \dv and \ket would not render at all.
Behind a profile, normalization runs as a multi-phase pipeline, not a find-and-replace pass:
- Rewrite applies a curated rule set in a fixed-point loop — modernizing legacy syntax, canonicalizing aliases, and expanding semantic macros, depending on the levels the profile selects.
- LowerAttributes canonicalizes font and style markup, so
{\bf x}and\mathbf{x}converge to a single form with declarative scope tracked correctly. - FlattenGroups strips redundant braces behind semantic and spacing guards, so flattening never changes script binding, environment cell boundaries, or atom spacing unless you opt in.
Two assets carry most of the weight, and neither existed as a reusable artifact before:
- The argspec knowledge base. Machine-readable, xparse-style argument signatures for every supported command and environment — validated by rendering against MathJax, KaTeX, and XeTeX rather than transcribed from documentation.
- The curated rewrite rule set. Every rule declares its normalization level, its worst-case render fidelity, and the forms it eliminates. The engine enforces that eliminated-form contract after every run, and corpus regression re-checks it against real-world datasets.
pip install texformimport texform
engine = texform.TransformEngine(profile="corpus")
assert engine.normalize(r"a \over b")["normalized"] == r"\frac { a } { b }"
parsed = engine.parse(r"a \over b")
if parsed["document"] is not None:
document = parsed["document"]
engine.transform(document)
assert document.to_latex() == r"\frac { a } { b }"npm install texformimport { TransformEngine } from "texform";
const engine = new TransformEngine({ profile: "corpus" });
console.assert(engine.normalize("a \\over b").normalized === "\\frac { a } { b }");
const parsed = engine.parse("a \\over b");
if (parsed.document) {
engine.transform(parsed.document);
console.assert(parsed.document.toLatex() === "\\frac { a } { b }");
}cargo add texformuse texform::{Profile, TransformEngine};
let engine = TransformEngine::builder().profile(Profile::Corpus).build()?;
let result = engine.normalize(r"a \over b")?;
assert_eq!(result.normalized, r"\frac { a } { b }");
let (mut document, _) = engine.parser().parse(r"a \over b").try_into_document()?;
engine.transform(&mut document)?;
assert_eq!(document.to_latex()?, r"\frac { a } { b }");Note: the
texformcrate is the only public, stability-guaranteed Rust entry point; thetexform-*crates it depends on are internal and may change in any release. SeeARCHITECTURE.mdfor the crate layout and the public API guarantees.
The Python and JavaScript bindings expose the same parser, document, and engine from the single Rust core. See the PyPI package notes and the npm package notes for language-specific details.
- Playground — try TeXForm in the browser
ARCHITECTURE.md— crate layout, pipeline, tree representations, API guaranteesTESTING.md— how TeXForm is tested, from contract tests to corpus regressionCHANGELOG.md— release history
Apache-2.0. See LICENSE.