TeXForm keeps a single changelog for all three release channels — one version number covers the Rust crate, the Python package, and the JavaScript package. See Stability and versioning for what each release type may contain.
The changelog is maintained in the repository and follows the Keep a Changelog format. It is rendered below from CHANGELOG.md at build time.
Treat the changelog as the release timeline. For the current shape of a configuration object or a report, Transforms and the API reference are the authority.
0.10.1 · 28 Oct 2026#
This release lets the PyPI package install the texform command-line tool, and fixes transform failures and output that did not reparse, mostly with physics. One rule key changes; see Changed.
Changed#
- The rule key
base/operatorname-to-named-functionbecomesams/operatorname-to-named-function, because the rule only applies whenamsprovides\operatorname. Configurations that enable or disable the old key must use the new one; the old key is not kept as an alias. Output is unchanged.
Added#
- The PyPI package installs the
texformcommand, also available aspython -m texform, so the command-line tool no longer needs a Rust toolchain. Install it withuv tool install texform, or run it once withuvx texform. It runs the same code ascargo install texform-cli, with the same commands, output, and exit statuses; each invocation adds about 10 ms of Python startup, and--linesandservethroughput is unchanged.
Fixed#
- With
physics,\eval,\eval*, and\evaluatedwithout a following(,[, or brace group no longer fail with a rewrite contract violation. As in MathJax, they expand to\left.\vphantom{\int}\right|, so\eval xbecomes\left.\vphantom{\int}\right| x. - With
physics, an ordered derivative whose variable already has a superscript no longer produces a double superscript that fails to reparse:\dv[2]{f}{x^2}now gives\mathrm{d}{x^{2}}^{2}in the denominator.\pdvand\fdvare fixed the same way. - A delimited argument whose content ends with an unpaired copy of its opening delimiter is kept in a protective brace group, and the parser strips that group again. Under Equiv with
physics,\qty(a{\big(}b)now becomes\qty({a(b})instead of\qty(a(b), which failed to parse. - Rewriting a scripted
\breakor\newlineattaches its scripts to an empty base, so\break'becomes\\{}', and FlattenGroups keeps the group in{\cr}_1. Both outputs previously changed when parsed again. - Dropping
\goodbreakused as a script base no longer panics in Corpus and lower profiles:\goodbreak^2becomes{}^{2}.
0.10.0 · 14 Oct 2026#
This release replaces the serializer's seven option axes with two output styles: spaced, the previous default, and a new compact style for human-readable LaTeX. With physics, named functions such as \sin and \exp now parse the argument that physics.sty prints as an auto-sized fence, and three Faithful rules make that fence explicit. Normalization also no longer removes a group or a delimiter size that kept a preceding command from reading the next token as an argument on reparse. The serializer change is breaking; see Changed for migration notes.
Changed#
- Breaking:
SerializeOptionsnow holds a singlestyle: Style, eitherStyle::Spaced(default) orStyle::Compact.CommandSpacing,MathGroupInnerSpacing,AdjacentCharSpacing,ScriptSpacing,ScriptOrder,InfixGrouping, andEnvironmentNameSpacingare removed, and Pythonto_latexand JavaScripttoLatexaccept only thestylekey and reject the removed ones. Spaced output is byte-for-byte identical to the previous default and stays byte-stable across releases. Replace axis settings withSerializeOptions { style: Style::Compact },to_latex(style="compact"), ortoLatex({ style: "compact" }); combinations that compacted only some axes,sup_firstscript order, and always-explicit infix operands have no replacement. - With
physics, the 26\trigbracesnames (\sin…\coth,\asin…\acot,\log,\ln) takeo d(), and\exp,\det,\Pr,\tr, and\Trtake one pairedd<(,)><[,]><{,}>slot, because physics.sty prints that argument as an auto-sized fence. A delimiter separated from the command by a group is now literal, and FlattenGroups keeps that group, as in{\cos}(x)or\cos{}(x). An unclosed parenthesis such as\sin(xis a parse error withphysics, as in LaTeX; across the regression datasets, 144 more formulas fail to parse withphysicsloaded.
Added#
- Compact output keeps only lexically required spaces and omits braces around a script that is a single character, a single prime,
~, or a known command without argument slots, adding a readability space before a following ASCII letter or digit (\frac{a}{b}+x_i^2,x^2 3). Scripts holding a command with argument slots, an unknown command, or a declarative switch keep their braces, as inx^{\sin}[2]withphysicsorx^{\rm}a. Compact output preserves the parsed structure and is text-idempotent; its layout may improve across releases. texform normalize --style spaced|compactapplies to text,--json, and--reportoutput, and theserveconfigurerequest accepts an optionalstyleand echoes it inresolved.tokenizealways uses spaced output.- Faithful rules expand physics named functions for renderers without physics.
physics/named-fn-paren-expandrewrites\sin(x)and\sin[n](x), and the same forms of the other\trigbracesnames that have base commands, to\sin\left(x\right)and\sin^{n}\left(x\right);\sin[n]without parentheses is unchanged.physics/physics-operator-fence-expandfences the argument of\exp,\det, and\Pr.physics/named-fn-to-operatornamerewrites physics-only names such as\asin,\csch,\tr,\erf, and\Traceto\operatorname, with the same expansion where the name takes an argument. The explicit fence is an inner atom, so it can add up to 1/6 em on each side; the rules declareReadingfidelity.
Fixed#
- FlattenGroups keeps a group when splicing it would let a known command read the following token as an omitted trailing optional, star, group, or delimited argument. With
physics,\sin{[2]}x,{\sin}[2]x, and\sin{}[2]xno longer become\sin[2]x, and{\qty}(a)no longer becomes\qty(a);\text{a{\\}*b}no longer becomes\text{a\\*b}. The protection applies in every profile, and reports count it asguard_hits.argument_capture(argumentCapturein JavaScript). - Equiv rules that drop a fixed delimiter size no longer expose the delimiter to the preceding command:
a\\\big[x\big]becomesa\\{}[x]instead ofa\\[x], which reparsedxas the line-break dimension, and withphysics\ln\Bigr[x\Bigr]no longer becomes\ln[x]. - The physics authoring aliases
sine-to-sin,cosine-to-cos,naturallogarithm-to-ln,probability-to-pr,trace-to-tr, andtrace-capital-to-Trrename only a command not followed by(,[, or a brace group.\trace(\rho)and\Probability(\frac{x}{y})no longer gain an auto-sized fence, and\sine(x)no longer fails the conformance check inauthoringor stops later rules infaithful. - With
abort_on_error, a failure inside a command argument is reported at its source:a+\frac{1},a+\hspace{x}, anda+\text{$x}no longer point ata, andarrayandalignare no longer reported as unknown or as not allowed in text mode. An unclosed inline$is reported at its opener in strict and non-strict parsing. Which inputs fail is unchanged. - Trees built with
from_syntaxkeep a space after a control-word delimiter such as\langle, or an error snippet ending in a control word, before an ASCII letter, and after a math-mode control sequence before a sibling[or*. Serialized output of parsed input is unchanged.
0.9.1 · 9 Oct 2026#
This release adds the color package to the default knowledge base, gives transform reports warnings for unknown input, and adds a rule catalog API and an explicit texform/web entry for browsers. Unknown commands now keep the brace groups that follow them, so their argument boundaries survive normalization. Formulas that use color commands now parse with known signatures, which changes their parsed trees and normalized output.
Added#
- The
colorpackage covers\color,\textcolor,\definecolor,\colorbox, and\fcolorbox, and is loaded by default in the library and the command-line tool, which now default to seven packages.\color[model]{color}is a declaration with LaTeX and MathJax 3/4 semantics, scoped to its enclosing group, not the two-argument form of MathJax's legacycolorv2.\textcolortakeso:N m:N min math mode ando:N m:N m:Tin text mode. Color names, models, and specifications are kept as literal strings: they are not validated and control sequences in them are not expanded, so\color{\mycolor}xstill fails to parse. - Transform reports include
warningsthat list the unknown command and environment names in the input, deduplicated and sorted by kind and name. They are collected whenever a report is requested, even with every phase disabled, and do not make a transform fail. Rust, Python, and JavaScript report types expose them, andtexform servenormalizeaccepts an optionalreportrequest that returns the same report; plain responses and normalizer protocol v1 are unchanged. texform::list_rules(), Pythonlist_rules(), and JavaScriptlistRules()return every built-in rewrite rule sorted by key, with itslevel,fidelity,summary, and enabling packages, so rule keys fordisableRulesand report statistics can be discovered. The facade re-exportsRuleLevelandRuleFidelity; TypeScript addsRuleFidelityandRuleInfo, and the Python stubs addRuleLevel,RuleFidelity, andRuleInfo.- The npm package adds a
texform/webentry for browsers that need to defer WebAssembly loading or supply the module themselves. Callinit()orinit({ wasm })before using the API; it is idempotent, can be retried after a failure, and reports typed initialization errors. The default bundler entry is unchanged. With Vite, excludetexformfromoptimizeDeps.
Fixed#
- Brace groups directly after an unknown command are no longer flattened. In
\unknown{a}{}{bc}, the empty group and the braces aroundbcwere treated as redundant and removed, which lost the argument boundaries. Every profile now keeps them, including empty groups and a group used as a script base such as\unknown{a}^2, and the guard report counts them asunknown_command_arguments. This keeps the boundaries, not the meaning of unknown input; other phases still normalize the group contents.
0.9.0 · 4 Oct 2026#
This release is a round of fixes that make TeXForm safer to run on real-world input. Parsing no longer panics on any source text, reports each problem with its specific diagnostic kind at its actual source position, and reads several plain TeX constructs as TeX does instead of silently misparsing them. Input that TeXForm cannot read correctly is now rejected with a diagnostic rather than turned into a wrong tree. The Corpus profile no longer drops modifiers whose visible effect depends on the math style, so its output keeps the rendering of its input. To parse the plain TeX macros \root…\of and \buildrel…\over correctly, the release adds the u{\name} form to the ArgSpec syntax; its new argument-form variant is the only breaking change, for Rust code that matches argument forms exhaustively.
Added#
- Breaking: The ArgSpec syntax gains the until form
u{\name}. It describes plain TeX delimited parameters such as\def\root#1\of#2, whose argument runs up to a terminating control word instead of being one token or one brace group, which no existing form could express. The argument collects tokens up to the first terminator outside brace groups and strips one brace layer when it is exactly one group; empty content is valid, and a missing terminator is a parse error. The terminator must be a single control word such as\ofor\over, and ArgSpec validation, including for custom entries, rejects characters, control symbols, and multi-token terminators.ArgumentKind,ArgKindRef, andArgSpecFormInfogain anUntil { close }variant, and bindings, columnar export, and CLI output name the form"until"; exhaustive matches over argument forms must handle it. - The knowledge base covers LaTeX text boxes and related commands, so input such as
\text{\mbox{$x$}},\raisebox{1ex}{$x$}, or\left\llbracket x\right\rrbracketparses.\raiseboxand\scaleboxare recognized with their optional arguments;\mbox,\fbox,\hbox,\makebox, and\parboxwork in text mode as well as math mode;\labeland themathenvironment are allowed inside text containers;\ensuremathreads its argument as math in both modes; nested\textis allowed in text mode;\emphworks in math mode; and\llbracketand\rrbracketare delimiters. These records describe parsing, not renderer support, and dimension arguments still require literal lengths.
Changed#
- The ArgSpec of the command
rootchanges frommtou{\of} m. Withm, the parser read only one token or group as the degree and left\ofand the radicand as sibling nodes, so unbraced degrees such as\root n_i\of{x}were misparsed and\root\of{x}was rejected. A parsedrootnow has a degree slot and a radicand slot, and a\rootwithout\ofis a parse error. Normalized output is unchanged for previously accepted input. - The ArgSpec of the command
buildrelchanges frommtou{\over} m. Withm,\buildreltook only the first token of the annotation, and the parser used a source-text lookbehind to split the rest with an infix\over, soa\over\buildrel b\over cwas rejected as an ambiguous infix and a braced\overinside the annotation was mistaken for the separator. A parsedbuildrelnow has an annotation slot that runs to the first\overoutside braces and an operator slot, and a\buildrelwithout\overis a parse error. Scripts after the operator attach to the whole relation. The Faithful rulebase/buildrel-expandis replaced by the Authoring rulebase/buildrel-to-stackrel, which rewrites\buildrel A\over Bto\stackrel{A}{B}; rule selections that name the old key should use the new one. Faithful, Corpus, and Equiv output is unchanged for previously accepted input, and Authoring now succeeds instead of failing its rewrite contract. Corpusand lower profiles keep\dfrac,\tfrac,\limits, and\nolimits. Their visible effect depends on the math style, which a single formula does not know, so dropping them could change the fraction size or move the scripts.Equivstill rewrites them.
Fixed#
- Source text containing a character the lexer cannot tokenize, such as an ASCII control character other than tab, newline, carriage return, or form feed, returns an
invalid-characterdiagnostic instead of panicking. In Python the panic surfaced asPanicException, which escaped ordinaryexcept Exceptionhandlers; callers now receive a regularParseError. - Diagnostics name the specific problem. Internal parser branch failures such as
not a commandno longer replace the real diagnostic, an environment left open at end of input reportsunclosed-environmentwith\end{name}as the expected token, and an environment used in a disallowed mode reportsenvironment-mode-errorin both strict and lenient parsing.ParseDiagnosticKindin Python and TypeScript includes the new kinds. - Diagnostics point at the actual source position. With
reject_unknown,\frac{\bad}{x}reported the unknown command at\frac, anda+\badata; spans inside argument content were also shifted after a comment or a run of spaces. Errors now keep the failing token's span, and diagnostics and node spans inside arguments map token by token back to the source. - Plain TeX alignment macros (
\matrix,\pmatrix,\cases,\eqalign,\eqalignno) split rows at\\and\newlineas well as\cr, and drop a final row terminator that has no*or spacing.\eqalignnono longer fails unless every row ends in&(…): it becomesalign*, and only rows with a numbering cell get a\tag(or\tag*when the cell is not wrapped in parentheses). - A script at the start of an alignment row attaches to an empty base in that row instead of to the preceding row separator:
\begin{matrix}a\\_i b\end{matrix}no longer serializes as\\ _{i} b. - In
array,darray,eqnarray, andeqnarray*,\\ [-5pt]reads the bracket as row spacing, as LaTeX does. amsmath environments still read it as row content, so a row can start with an interval such as[a,b]. - An unbraced
\left…\rightor$…$argument keeps its delimiters:\frac\left(x\right)2parses like\frac{\left(x\right)}{2}instead of\frac{x}{2}, and\text$x$like\text{$x$}. - A comment between a control word and following letters inside an argument no longer merges them:
\sqrt[\le% commentfollowed byq]{x}on the next line was read as\sqrt[\leq]{x}. - Custom signatures with delimiters such as
d**orr~~match*,$,&,#,^,_, and~in the input instead of always treating the argument as absent. ArgSpec validation rejects delimiters that can never match:%,',’, and ASCII control characters. \hspace*{…}parses in math and text modes.- A text environment whose body ends with a group, inline math, or whitespace before
\endno longer reports a spurious diagnostic.
0.8.0 · 30 Sep 2026#
This release makes Document a complete API for building and editing formulas. Every node kind now has a constructor, so a formula can be built from scratch as a tree and serialized, and every construction and edit is checked against the document's knowledge base, so the result always conforms to it, as parser output does, and transforms accept it. Deterministic edits, rooted node paths, cross-document imports, and a columnar export cover targeted edits and bulk analysis. To support this, KnowledgeBase becomes an immutable object shared by parsers, engines, and documents. The release also fixes script, text-argument, and declaration-scope handling in alignments and text mode. The knowledge base and construction changes are breaking; see Changed for migration notes.
Changed#
- Breaking: Package selection, custom items, and removals move from parser and engine builders to
KnowledgeBaseBuilder, and knowledge queries move fromParserandTransformEnginetoKnowledgeBase. Build oneKnowledgeBaseand pass it to every parser, engine, and document that should interoperate, for exampleParser::builder().knowledge_base(kb.clone()).build()andTransformEngine::builder().knowledge_base(kb).ParserBuilder::build()now returnsParser, andParserBuildErrorandError::ParserBuildare removed.TransformEngine::transformaccepts any document bound to the same instance and returnsError::KnowledgeBaseMismatchotherwise, replacingError::ForeignDocument. In Python, passtexform.KnowledgeBase(packages, items=...)toParser(kb)andTransformEngine(profile, kb); in JavaScript, pass{ knowledgeBase }. The command-line tool and theserveprotocol are unchanged. - Breaking: Construction, edits, and syntax imports keep every complete document conformant to its knowledge base. A violation returns
ConformanceErrorwith a rooted path and a stable rule code (TexformConformanceErrorin JavaScript) and leaves the document unchanged. ReplaceArgValuewithArg, and binding argument dictionaries withNode | str | bool | None | Pairedvalues ({ value, open, close }pairs in JavaScript).create_groupandcreate_environmenttake their children and body directly, binding delimiters are strings such as"("and"\\langle", and environments are renamed withset_env_nameinstead ofset_command_name.Document::from_syntaxrejects non-conformant complete trees, withFromSyntaxError::Conformancereplacing the prime-specific variants, andKnowledgeBaseBuilderrejects item names the lexer cannot produce withKnowledgeBaseBuildError::InvalidName.
Added#
- Every node kind has a constructor, including scripted nodes, infix commands, primes, inline math, and delimited groups. Commands and environments are built from their knowledge-base records, so optional, star, and paired slots can be filled, and a constructor called with a name of another command kind names the one to use. Arguments are given as nodes, source text parsed in the slot's mode, star Booleans, or explicit absent and paired values;
in_modeandparse_fragmentconstruct in an explicit context mode. - Deterministic edits:
set_subscriptandset_superscriptadd, change, or clear scripts, collapsing the wrapper when the last script is cleared;set_delimiters,set_arg_delimiters, andset_prime_countchange delimiters and prime counts.clone_nodecopies a subtree within a document, andimport_nodecopies one from another document, validating it against the destination knowledge base. Failed edits leave the document unchanged. NodeRef::pathreturns a node's rooted path andDocument::node_atresolves it;NodeRef::slotandNodeRef::is_knownreport the parent slot and knowledge status. Python and JavaScript expose the same edits and reads, JavaScript addsargKind(index), and argument reads include their slot form.Document::to_columnarexports a document as a preorder node table and an argument table for Arrow and DataFrame tools (to_columnar()in Python,toColumnar()in JavaScript); Python tables pass directly topyarrow.table.KnowledgeBaseinstances are cheap to clone and compare by identity (KnowledgeBase::ptr_eq);KnowledgeBase::default()returns one process-wide instance, andDocument::with_knowledge_baseandDocument::from_syntax_withbind an explicit one. Python and JavaScript exposeKnowledgeBasewith identity equality and hashing, document copies (copy()in Python,clone()in JavaScript), node identity, anddocument()on nodes.
Fixed#
- In alignments, a script directly after a column separator (
&^2,&_1,&') now starts the next cell with an empty base instead of using&as its base. Escaped\&still takes scripts. - An unbraced argument of a text-mode command takes a single token, as in TeX and MathJax:
\mbox TeVparses as\mbox{T}eVinstead of\mbox{TeV}.\text{$x$ }and\textbf{ \emph k }now parse. - Alignment separators (
&,\\,\newline,\cr) end declarative scopes in environment bodies and math arguments, so\begin{array}{cc} \rm a & b \\ c & d \end{array}lowers to\mathrm{a} & b \\ c & dinstead of the invalid\mathrm{a & b \\ c & d}, and a\displaystylerepeated in a later cell is kept.
0.7.0 · 26 Sep 2026#
This release makes normalized output reparse to the same tree, so normalizing it again no longer changes it, and stops math-mode \& from turning into an alignment tab. Both fixes change the tree shape; see Changed for migration notes.
Changed#
- Breaking: A bare
&now parses as a newAlignmentTabnode inSyntaxNode,Node, andNodeKind, andChar('&')always means a literal ampersand. Code that matched or builtChar('&')for an alignment tab should useAlignmentTab, and exhaustive matches need the new variant.Document::create_alignment_tabstages one (create_alignment_tabin Python,createAlignmentTabin JavaScript); node kinds report"AlignmentTab"in Python and"alignmentTab"in JavaScript. - Breaking: A command argument whose content is a single brace group now parses to an implicit container around that group, so a brace group directly in any slot owns that slot's braces. FlattenGroups always unwraps redundant user braces in argument slots, and the
command_argumentguard and its report field are removed from Rust, Python, and JavaScript. - The default packages are now
base,ams,braket,textmacros,bboldx, andboldsymbol;physicsmust be loaded explicitly.
Fixed#
- Normalizing the output of
normalizeagain no longer changes it. Argument braces around a single brace group are kept (\overline{{{\Psi}}},\mathrm{{}}), braces that protect a closing delimiter inside an optional argument are restored on output (\xrightarrow[{]}]{x}no longer becomes\xrightarrow [ ] ] { x }), and a lone\at a line end or input end is a control space, as in TeX. - Optional,
Delimited, andPairedarguments track brace depth, so protected content such as\qty[{]}]parses. - In text mode, whitespace after an argument-free control word is no longer content:
\text{\bf\large x}no longer normalizes to\textbf{ x}. - Math-mode
\&, such asa \& bor\mathrm{\&}, is serialized as\&instead of a bare&that became a column separator insidematrixoralign. All profiles were affected, including plain parse and serialize.
0.6.0 · 25 Sep 2026#
This release adds the texform command-line tool, gives prime marks a consistent meaning as symbols with explicit superscript binding, and makes normalization stable across transform phases. It also fixes spacing drift after text-mode control symbols and declarations leaking out of local groups, and roughly halves transform time. The prime change is breaking; see Changed for migration notes.
Added#
- A new
texform-clicrate installs atexformbinary (cargo install texform-cli).normalize,parse, andtokenizeread one formula from an argument or stdin, or one per line with--lines, and print human-readable text or one JSON object per formula;info,packages, andargspec validatequery the knowledge base and argument specifications with the same shapes as the bindings.texform servespeaks JSON-RPC 2.0 over stdio (normalizer protocol v1) so external tools can call a specific TeXForm build across a process boundary, andtexform --versionreports the source commit and whether it was dirty. The CLI loads the library's default packages;braketis opt-in.
Changed#
- Breaking:
Primeis now a math symbol, and onlyScriptedexpresses superscript binding.x\primestaysx \primeinstead of becomingx',x^2\primeno longer produces an unparsable double superscript,f'^2serializes as one superscript instead of a nested empty-base one, andf^{'}no longer collapses tof'. A pure prime superscript still serializes asf'. A leading quote, including insidef^{'}andA^{'\alpha}, parses as an empty-baseScripted, and whitespace-separated quotes such asf' ' 'form one superscript as in MathJax.x^'andx_'are now parse errors. Snapshots that stored a barePrimefor a quote superscript should be reparsed from source or rebuilt with an empty-baseScripted. - Transforms are faster: rewrite trigger indexes are built once per
Plan, and phases skip work they have already done. On a 10,000-formula sample, transform time falls from 33–44 µs to 14–18 µs per formula across the four profiles.
Fixed#
- Normalization no longer depends on how many times it is applied. Later phases could expose attribute merges or rewrite matches after earlier phases had finished; the phases now run to a shared fixed point, and a transform that fails to converge within eight rounds returns a
TransformErrorinstead of partially normalized output. As a result, a prefix command's single-node argument is no longer wrapped in an implicit group in the transformed tree; serialized output is unchanged. - Text-mode control symbols such as
\and\,no longer gain a separator space on every serialization (\mbox{mod\ 1}grew by one space per round). A separator is emitted only where it keeps a control word from absorbing the following letter. - Declaratives without a prefix form keep their local scope:
{\scriptstyle x} yno longer becomes\scriptstyle x y,{\oldstyle 1}2no longer becomes\oldstyle 12, and\mathbf{\scriptstyle x}yno longer emits the style twice.
0.5.0 · 22 Sep 2026#
This release reshapes the configuration and reporting surface across Rust, Python, and JavaScript so the three APIs share one configuration tree and one set of override semantics. Transform reports become opt-in, TransformConfig is organized by phase, serialization options are flattened, and binding exception categories are corrected. It also hardens parser recovery and diagnostics, fixes serialization of bare delimiter arguments, and adds two corpus rules. Most changes are breaking; see Changed for migration notes.
Added#
normalize_with_report/transform_with_reporton the Rust facade and the Python binding (normalizeWithReport/transformWithReportin JavaScript) for callers that need the diagnostic report. Report types now live intexform::diagnostics.- Effective defaults are exposed on every binding:
default_parse_config()/default_transform_config()in Python anddefaultParseConfig()/defaultTransformConfig()in JavaScript return the engine's profile defaults. Rust addsTransformEngine::default_normalize_config(). RewriteConfig, plus the serialization option enums (CommandSpacing,MathGroupInnerSpacing,AdjacentCharSpacing,ScriptSpacing,ScriptOrder,InfixGrouping,EnvironmentNameSpacing), are re-exported from the Rust facade.- Corpus rules
leftroot-dropanduproot-dropdrop the AMS\leftroot/\uprootindex-position hints.
Changed#
- Breaking: transform reports are opt-in.
normalize/normalize_withnow return the canonical string andtransform/transform_withreturn nothing, in Rust, Python, and JavaScript, so plain calls no longer pay for report collection or host-object conversion.NormalizeResultand theFinalizeAstStep*types are removed. The report itself is restructured (rewrite.{iterations, rules}, flattened FinalizeAst counters,flatten_groups.guard_hitswith short guard names) and remains a diagnostic rather than a stability promise. - Breaking:
TransformConfigis organized by phase. The flatrewrite_enabled,lower_attributes_enabled, andmax_iterationsfields becomerewrite: RewriteConfig { enabled, max_iterations }andlower_attributes: LowerAttributesConfig, alongside the existingfinalize_astandflatten_groups. Profile defaults are unchanged. - Breaking:
FlattenGroupsConfigexposes onlyenabledandpreserve_rendered_spacing(preserveRenderedSpacingin JavaScript). The elevenpreserve_*guard fields are removed and old keys are rejected. - Breaking: serialization options are a flat set of seven fields; the nested
math/syntaxshape is removed. Python takes snake_case keyword arguments (doc.to_latex(script_order="sup_first"),serialize(node, **options)), and JavaScript takes a flat camelCase object. - Breaking: configuration inputs are unified across bindings. Python reserves
config=for a complete config object (TransformConfig,ParseConfig); partial settings are passed as keyword overrides that keep unspecified defaults, and a dict passed asconfigis rejected. JavaScript accepts the same nested override tree innormalizeandtransform, with omitted fields keeping the profile default. Unknown keys and wrongly typed values raiseConfigError/TexformConfigErrorwith the field path. The unused WASM config classes andparseWithare removed. - Breaking: binding exception categories are corrected. Python cross-document edits raise
EditErrorinstead ofParseError. Transforming a document that has parse errors raisesTransformErrorin Python andTexformTransformError(kind: "transform") in JavaScript instead of theinternalkind. The undocumentedComplete<T>type is no longer importable from the npm typings.
Fixed#
- Parser recovery under
reject_unknownwithoutabort_on_errorconsumes only the unknown command, so\mycmd{x} + yyields a partial document instead of failing the whole parse. A stray}is no longer reported as an environment-name mismatch, and^/_with missing content reportMissing superscript content/Missing subscript content. - Diagnostics keep their structured kind and precise source span through nested content parsing instead of highlighting the outer command or closing brace, and internal kind labels no longer leak into messages.
- Literal carriage returns are treated as whitespace, so Windows (CRLF) and CR-only inputs parse instead of panicking.
- Mandatory delimiter arguments are serialized without braces (
\middle \vert,\big \langle,\genfrac . . {} {}), so the output renders in MathJax again. - JavaScript:
normalizewith aflattenGroupsoverride now layers it on the engine profile's defaults instead of the strict baseline. - JavaScript: internal DTO serialization failures are thrown as
TexformError(kindinternal) instead of being returned as successful values. - Python: invalid per-call config objects raise
ConfigErrorconsistently acrossparse,normalize, andtransform.
0.4.0 · 26 Jul 2026#
This release aligns TransformEngine parsing with the standalone Parser by defaulting to lenient parsing, and fixes prime superscripts inside environment bodies.
Changed#
- Breaking:
TransformEnginenow parses withParseConfig::LENIENTby default (previouslyParseConfig::STRICT), matching the standaloneParser; this applies to Rust, Python, and JavaScript. Unknown commands are preserved instead of rejected, sonormalize(r"\unknowncmd")now succeeds, andengine.parsereturns a recovery document with error nodes for malformed input.normalizestill requires a complete tree and raises a parse error, carrying the diagnostics and the partial document, when the input cannot produce one. To restore the previous behavior, passParseConfig::STRICTas the engine's default parse config, or setreject_unknownandabort_on_errortotrue(rejectUnknown/abortOnErrorin JavaScript).
Fixed#
- A lone prime inside an environment body, such as an
arraycell, no longer serializes as an explicit superscript group that renders as a double superscript. FlattenGroups now unwraps lonePrimenodes in superscript slots while still preserving other environment-body groups.
0.3.0 · 21 Jul 2026#
This release adds a tokenized serialization channel, extends AST canonicalization with text-sequence normalization, and substantially expands the rewrite rule set — spacing, fraction and delimiter styling, negated-relation remaps, named-function operators, and more. It also renames the rule-level taxonomy on the public API and prunes several rules whose render fidelity did not hold up under corpus review.
Added#
- Tokenized serialization.
Document::to_tokenized_latex()(andto_tokenized_latex_with) returns the canonical LaTeX string alongside ordered, typed output tokens — each classified asControlSequence,Character,Delimiter,Text,Raw, orError, and carrying its math/text mode and a non-overlapping UTF-8 byte span into that string. Tokens are recorded during the existing serializer traversal rather than by re-lexing the output, and the feature is exposed across the Rust, Python, and JavaScript APIs while leaving text-only serialization's allocation behavior unchanged. - Text-sequence normalization in
FinalizeAst. The profile-neutral canonicalization phase now merges adjacent text-mode siblings, collapses ordinary lexer whitespace runs to a single space without trimming edges, and cleans empty text. The pass re-runs afterFlattenGroupsso newly adjacent text and prime nodes are canonicalized, and its work is reported through thenormalize_text_sequencesstep counter on the transform report. - A large batch of rewrite rules across several normalization groups: spacing drops (
\enspace,\quad,\qquad) and merges (adjacent\enspacepairs, small-spacer runs); fraction styling (\dfrac/\tfrac/\cfrac→\frac,\dbinom/\tbinom→\binom); limit placement (\limits/\nolimitsdrops on audited operators); fixed delimiter sizing (\big–\Biggsize drops); negated-relation remaps (\not=→\neq,\not\exists→\nexists,\not\in→\notin,\not\rightarrow→\nrightarrow); accent sizing (\widehat→\hat,\widetilde→\tilde, the firstEquiv-level builtins); math-mode control-space to active space; duplicatemathrel-wrapper drops; and named-function rewrites for plain operator names, with extended\operatornamecanonicalization. Shared delimiter-rewrite helpers back the delimiter-oriented rules.
Changed#
- Breaking: the rule-level taxonomy is renamed from normalization level to rule level across the public API, the transform engine, the generated registry, rule metadata, and documentation. The facade now re-exports
RuleLevelSetin place ofNormalizationLevelSet, and the internalNormalizationLevelenum becomesRuleLevel; downstream code referring to the old names must update. Active rules are reclassified underAuthoring,Faithful,Corpus, andEquivwhile preserving current profile behavior. \limits/\nolimitsdrops move fromEquivtoCorpus, so they now participate in corpus normalization.
Fixed#
- Removed rewrite rules whose worst-case render fidelity did not hold across supported renderers: the
displaylines-to-gather-envrule (its numberedgatheroutput cannot preserve reading fidelity), therepeat-spacer-collapserule, and the physics quick-\quadexpansions (qcomma-expand,qqtext-expand). These forms are now preserved instead of rewritten.
0.2.1 · 16 Jul 2026#
This release expands the normalization rule set with spacing canonicalization and a batch of alias and macro-expansion rules, so more spacing markup, legacy shorthand, and convenience commands collapse to canonical, universally renderable forms.
Added#
- Spacing normalization rules. Adjacent
\quadpairs merge into\qquadand repeated small spacers collapse; MathJax spacing aliases —\thinspace,\negthinspace,\hskip,\mkern,\mskip,\hfil,\hfilll,\space,\nobreakspace, and\gt— map to their canonical spellings while preserving scalar dimensions and documented fidelity boundaries; and pure layout hints (\mathstrut,\strut, and line-break hints) are dropped. - Alias and macro-expansion rules that fold convenience and legacy forms into universal output:
\astcanonicalizes to the pixel-identical literal*;\dotsresolves to\ldotsor\cdotsfrom the surrounding atom class, leaving unclassifiable and boundary cases untouched;\impliedbyexpands to the source-equivalent spaced\Longleftarrow; theVmatrixenvironment expands into a matrix wrapped in explicit double-bar fences; and the\bigl/\bigr/\bigmclass variants — with their\Big,\bigg, and\Biggcounterparts — collapse to the plain\big-family delimiters.
Fixed#
- Rule metadata is aligned with the published rule proposals, keeping each rule's declared normalization level and fidelity in sync with its documented contract.
- Rewrite rules that synthesize new commands — the implies, derivative, eval, and multi-integral expansions — now declare every character they emit, drawing on builtin character records so the engine's eliminated-form contract sees their full output.
0.2.0 · 3 Jul 2026#
This release adds in-place document normalization to the Python and JavaScript bindings, teaches the parser and serializer to preserve whitespace and spacing faithfully, and makes parsing and transforming large formulas dramatically faster.
Added#
TransformEngine.transformon the Python and JavaScript bindings, for normalizing a liveDocumentin place. Every parsed document is stamped with a parse-context id, and a document produced by a different parser (or byDocument.from_syntax) is rejected with a foreign-document error, so a document is only transformed by the engine that produced it.:Ooperator-name argspec content, so arguments to\operatornameand\DeclareMathOperatorare modeled as math content and serialized compactly without special-casing command names.- Typed serialization options for the Python bindings:
serialize()andDocument.to_latex()now accept a documentedSerializeOptionsTypedDict instead of an untypeddict, mirroring the TypeScript option interfaces.
Changed#
- Breaking: the public
Errorenum is now#[non_exhaustive]. Exhaustivematcharms overtexform::Errorin downstream Rust must add a wildcard arm; in return, future error variants can be introduced without another breaking change. - Parsing and transforming large formulas is dramatically faster. Release builds no longer run debug-only structural invariant sweeps, which were quadratic on wide formulas (a 60k-character formula now parses ~14× faster and transforms ~45× faster); source spans are carried as a positional tree (10–25% faster parsing); and rewrite rules are indexed by trigger name (31–48% faster transforms). Normalized output is byte-identical.
Fixed#
- Edge whitespace in text arguments is preserved:
\text{ or }and\textbf{ a }no longer drop their leading and trailing spaces on parse, serialize, or transform. - Adjacent math digits stay compact: multi-digit numbers such as
1093^2no longer serialize as1 0 9 3 ^ { 2 }under the default spacing option, while letters and symbols still honor it. - Tight argspec slot spacing is preserved, so no-leading-space slots — including linebreak dimensions and custom tight optional slots — stay tight through parse/serialize round trips.
- Whitespace is kept outside attribute wrappers during transforms.
- Inline math inside text-mode control sequences, and whitespace-only text arguments, are now accepted.
- The recovery parser is hardened against an unnecessary unwrap.
- The license file is included in the Python sdist.
0.1.0 · 12 Jun 2026#
Initial public release of TeXForm — a LaTeX formula parser, editable document model, and normalization engine, available in Rust, Python, and JavaScript from a single Rust core.
Added#
- Knowledge-driven parser backed by 530+ command and environment specifications across the
base,ams,physics,braket,bboldx,boldsymbol, andtextmacrospackages, with strict and lenient modes that preserve unknown commands and unparseable fragments as explicit nodes instead of failing the parse. - Editable
Documenttree with validated, fallible edits and canonical LaTeX serialization that guarantees text idempotency over parse/serialize cycles. - Profile-based transform engine with four normalization profiles —
Authoring,Faithful,Corpus, andEquiv— covering author-facing cleanup, render-faithful expansion, corpus preparation, and formula-equivalence comparison. validate_argspecfor checking xparse-style argument specifications.- Python (PyPI
texform, Python ≥ 3.10) and JavaScript/TypeScript (npmtexform, WebAssembly) bindings exposing the same parser, document, and transform engine from the shared Rust core.