# Changelog

Notable user-visible changes in each TeXForm release, covering the Rust, Python, and JavaScript channels.

https://texform.dev/docs/changelog

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](https://texform.dev/docs/stability-and-versioning) for what each release type may contain.

The changelog is maintained in the repository and follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. It is rendered below from [`CHANGELOG.md`](https://github.com/texform-dev/texform/blob/main/CHANGELOG.md) at build time.

Treat the changelog as the release timeline. For the current shape of a configuration object or a report, [Transforms](https://texform.dev/docs/transforms) and the [API reference](https://texform.dev/api) 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-function` becomes `ams/operatorname-to-named-function`, because the rule only applies when `ams` provides `\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 `texform` command, also available as `python -m texform`, so the command-line tool no longer needs a Rust toolchain. Install it with `uv tool install texform`, or run it once with `uvx texform`. It runs the same code as `cargo install texform-cli`, with the same commands, output, and exit statuses; each invocation adds about 10 ms of Python startup, and `--lines` and `serve` throughput is unchanged.

### Fixed

- With `physics`, `\eval`, `\eval*`, and `\evaluated` without 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 x` becomes `\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. `\pdv` and `\fdv` are 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 `\break` or `\newline` attaches 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 `\goodbreak` used as a script base no longer panics in Corpus and lower profiles: `\goodbreak^2` becomes `{}^{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:** `SerializeOptions` now holds a single `style: Style`, either `Style::Spaced` (default) or `Style::Compact`. `CommandSpacing`, `MathGroupInnerSpacing`, `AdjacentCharSpacing`, `ScriptSpacing`, `ScriptOrder`, `InfixGrouping`, and `EnvironmentNameSpacing` are removed, and Python `to_latex` and JavaScript `toLatex` accept only the `style` key 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 with `SerializeOptions { style: Style::Compact }`, `to_latex(style="compact")`, or `toLatex({ style: "compact" })`; combinations that compacted only some axes, `sup_first` script order, and always-explicit infix operands have no replacement.
- With `physics`, the 26 `\trigbraces` names (`\sin` … `\coth`, `\asin` … `\acot`, `\log`, `\ln`) take `o d()`, and `\exp`, `\det`, `\Pr`, `\tr`, and `\Tr` take one paired `d<(,)><[,]><{,}>` 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(x` is a parse error with `physics`, as in LaTeX; across the regression datasets, 144 more formulas fail to parse with `physics` loaded.

### 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 in `x^{\sin}[2]` with `physics` or `x^{\rm}a`. Compact output preserves the parsed structure and is text-idempotent; its layout may improve across releases.
- `texform normalize --style spaced|compact` applies to text, `--json`, and `--report` output, and the `serve` `configure` request accepts an optional `style` and echoes it in `resolved`. `tokenize` always uses spaced output.
- Faithful rules expand physics named functions for renderers without physics. `physics/named-fn-paren-expand` rewrites `\sin(x)` and `\sin[n](x)`, and the same forms of the other `\trigbraces` names 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-expand` fences the argument of `\exp`, `\det`, and `\Pr`. `physics/named-fn-to-operatorname` rewrites physics-only names such as `\asin`, `\csch`, `\tr`, `\erf`, and `\Trace` to `\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 declare `Reading` fidelity.

### 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]x` no 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 as `guard_hits.argument_capture` (`argumentCapture` in JavaScript).
- Equiv rules that drop a fixed delimiter size no longer expose the delimiter to the preceding command: `a\\\big[x\big]` becomes `a\\{}[x]` instead of `a\\[x]`, which reparsed `x` as the line-break dimension, and with `physics` `\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`, and `trace-capital-to-Tr` rename 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 in `authoring` or stops later rules in `faithful`.
- With `abort_on_error`, a failure inside a command argument is reported at its source: `a+\frac{1}`, `a+\hspace{x}`, and `a+\text{$x}` no longer point at `a`, and `array` and `align` are 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_syntax` keep 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 `color` package 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 legacy `colorv2`. `\textcolor` takes `o:N m:N m` in math mode and `o:N m:N m:T` in 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}x` still fails to parse.
- Transform reports include `warnings` that 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, and `texform serve` `normalize` accepts an optional `report` request that returns the same report; plain responses and normalizer protocol v1 are unchanged.
- `texform::list_rules()`, Python `list_rules()`, and JavaScript `listRules()` return every built-in rewrite rule sorted by key, with its `level`, `fidelity`, `summary`, and enabling packages, so rule keys for `disableRules` and report statistics can be discovered. The facade re-exports `RuleLevel` and `RuleFidelity`; TypeScript adds `RuleFidelity` and `RuleInfo`, and the Python stubs add `RuleLevel`, `RuleFidelity`, and `RuleInfo`.
- The npm package adds a `texform/web` entry for browsers that need to defer WebAssembly loading or supply the module themselves. Call `init()` or `init({ 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, exclude `texform` from `optimizeDeps`.

### Fixed

- Brace groups directly after an unknown command are no longer flattened. In `\unknown{a}{}{bc}`, the empty group and the braces around `bc` were 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 as `unknown_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 `\of` or `\over`, and ArgSpec validation, including for custom entries, rejects characters, control symbols, and multi-token terminators. `ArgumentKind`, `ArgKindRef`, and `ArgSpecFormInfo` gain an `Until { 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\rrbracket` parses. `\raisebox` and `\scalebox` are recognized with their optional arguments; `\mbox`, `\fbox`, `\hbox`, `\makebox`, and `\parbox` work in text mode as well as math mode; `\label` and the `math` environment are allowed inside text containers; `\ensuremath` reads its argument as math in both modes; nested `\text` is allowed in text mode; `\emph` works in math mode; and `\llbracket` and `\rrbracket` are delimiters. These records describe parsing, not renderer support, and dimension arguments still require literal lengths.

### Changed

- The ArgSpec of the command `root` changes from `m` to `u{\of} m`. With `m`, the parser read only one token or group as the degree and left `\of` and the radicand as sibling nodes, so unbraced degrees such as `\root n_i\of{x}` were misparsed and `\root\of{x}` was rejected. A parsed `root` now has a degree slot and a radicand slot, and a `\root` without `\of` is a parse error. Normalized output is unchanged for previously accepted input.
- The ArgSpec of the command `buildrel` changes from `m` to `u{\over} m`. With `m`, `\buildrel` took only the first token of the annotation, and the parser used a source-text lookbehind to split the rest with an infix `\over`, so `a\over\buildrel b\over c` was rejected as an ambiguous infix and a braced `\over` inside the annotation was mistaken for the separator. A parsed `buildrel` now has an annotation slot that runs to the first `\over` outside braces and an operator slot, and a `\buildrel` without `\over` is a parse error. Scripts after the operator attach to the whole relation. The Faithful rule `base/buildrel-expand` is replaced by the Authoring rule `base/buildrel-to-stackrel`, which rewrites `\buildrel A\over B` to `\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.
- `Corpus` and 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. `Equiv` still 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-character` diagnostic instead of panicking. In Python the panic surfaced as `PanicException`, which escaped ordinary `except Exception` handlers; callers now receive a regular `ParseError`.
- Diagnostics name the specific problem. Internal parser branch failures such as `not a command` no longer replace the real diagnostic, an environment left open at end of input reports `unclosed-environment` with `\end{name}` as the expected token, and an environment used in a disallowed mode reports `environment-mode-error` in both strict and lenient parsing. `ParseDiagnosticKind` in 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`, and `a+\bad` at `a`; 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 `\newline` as well as `\cr`, and drop a final row terminator that has no `*` or spacing. `\eqalignno` no longer fails unless every row ends in `&(…)`: it becomes `align*`, 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`, and `eqnarray*`, `\\ [-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…\right` or `$…$` argument keeps its delimiters: `\frac\left(x\right)2` parses 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% comment` followed by `q]{x}` on the next line was read as `\sqrt[\leq]{x}`.
- Custom signatures with delimiters such as `d**` or `r~~` 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 `\end` no 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 from `Parser` and `TransformEngine` to `KnowledgeBase`. Build one `KnowledgeBase` and pass it to every parser, engine, and document that should interoperate, for example `Parser::builder().knowledge_base(kb.clone()).build()` and `TransformEngine::builder().knowledge_base(kb)`. `ParserBuilder::build()` now returns `Parser`, and `ParserBuildError` and `Error::ParserBuild` are removed. `TransformEngine::transform` accepts any document bound to the same instance and returns `Error::KnowledgeBaseMismatch` otherwise, replacing `Error::ForeignDocument`. In Python, pass `texform.KnowledgeBase(packages, items=...)` to `Parser(kb)` and `TransformEngine(profile, kb)`; in JavaScript, pass `{ knowledgeBase }`. The command-line tool and the `serve` protocol are unchanged.
- **Breaking:** Construction, edits, and syntax imports keep every complete document conformant to its knowledge base. A violation returns `ConformanceError` with a rooted path and a stable rule code (`TexformConformanceError` in JavaScript) and leaves the document unchanged. Replace `ArgValue` with `Arg`, and binding argument dictionaries with `Node | str | bool | None | Paired` values (`{ value, open, close }` pairs in JavaScript). `create_group` and `create_environment` take their children and body directly, binding delimiters are strings such as `"("` and `"\\langle"`, and environments are renamed with `set_env_name` instead of `set_command_name`. `Document::from_syntax` rejects non-conformant complete trees, with `FromSyntaxError::Conformance` replacing the prime-specific variants, and `KnowledgeBaseBuilder` rejects item names the lexer cannot produce with `KnowledgeBaseBuildError::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_mode` and `parse_fragment` construct in an explicit context mode.
- Deterministic edits: `set_subscript` and `set_superscript` add, change, or clear scripts, collapsing the wrapper when the last script is cleared; `set_delimiters`, `set_arg_delimiters`, and `set_prime_count` change delimiters and prime counts. `clone_node` copies a subtree within a document, and `import_node` copies one from another document, validating it against the destination knowledge base. Failed edits leave the document unchanged.
- `NodeRef::path` returns a node's rooted path and `Document::node_at` resolves it; `NodeRef::slot` and `NodeRef::is_known` report the parent slot and knowledge status. Python and JavaScript expose the same edits and reads, JavaScript adds `argKind(index)`, and argument reads include their slot form.
- `Document::to_columnar` exports 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 to `pyarrow.table`.
- `KnowledgeBase` instances are cheap to clone and compare by identity (`KnowledgeBase::ptr_eq`); `KnowledgeBase::default()` returns one process-wide instance, and `Document::with_knowledge_base` and `Document::from_syntax_with` bind an explicit one. Python and JavaScript expose `KnowledgeBase` with identity equality and hashing, document copies (`copy()` in Python, `clone()` in JavaScript), node identity, and `document()` 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 TeV` parses as `\mbox{T}eV` instead 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 & d` instead of the invalid `\mathrm{a & b \\ c & d}`, and a `\displaystyle` repeated 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 new `AlignmentTab` node in `SyntaxNode`, `Node`, and `NodeKind`, and `Char('&')` always means a literal ampersand. Code that matched or built `Char('&')` for an alignment tab should use `AlignmentTab`, and exhaustive matches need the new variant. `Document::create_alignment_tab` stages one (`create_alignment_tab` in Python, `createAlignmentTab` in 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_argument` guard and its report field are removed from Rust, Python, and JavaScript.
- The default packages are now `base`, `ams`, `braket`, `textmacros`, `bboldx`, and `boldsymbol`; `physics` must be loaded explicitly.

### Fixed

- Normalizing the output of `normalize` again 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`, and `Paired` arguments 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 as `a \& b` or `\mathrm{\&}`, is serialized as `\&` instead of a bare `&` that became a column separator inside `matrix` or `align`. 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-cli` crate installs a `texform` binary (`cargo install texform-cli`). `normalize`, `parse`, and `tokenize` read 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`, and `argspec validate` query the knowledge base and argument specifications with the same shapes as the bindings. `texform serve` speaks JSON-RPC 2.0 over stdio (normalizer protocol v1) so external tools can call a specific TeXForm build across a process boundary, and `texform --version` reports the source commit and whether it was dirty. The CLI loads the library's default packages; `braket` is opt-in.

### Changed

- **Breaking:** `Prime` is now a math symbol, and only `Scripted` expresses superscript binding. `x\prime` stays `x \prime` instead of becoming `x'`, `x^2\prime` no longer produces an unparsable double superscript, `f'^2` serializes as one superscript instead of a nested empty-base one, and `f^{'}` no longer collapses to `f'`. A pure prime superscript still serializes as `f'`. A leading quote, including inside `f^{'}` and `A^{'\alpha}`, parses as an empty-base `Scripted`, and whitespace-separated quotes such as `f' ' '` form one superscript as in MathJax. `x^'` and `x_'` are now parse errors. Snapshots that stored a bare `Prime` for a quote superscript should be reparsed from source or rebuilt with an empty-base `Scripted`.
- 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 `TransformError` instead 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} y` no longer becomes `\scriptstyle x y`, `{\oldstyle 1}2` no longer becomes `\oldstyle 12`, and `\mathbf{\scriptstyle x}y` no 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_report` on the Rust facade and the Python binding (`normalizeWithReport` / `transformWithReport` in JavaScript) for callers that need the diagnostic report. Report types now live in `texform::diagnostics`.
- Effective defaults are exposed on every binding: `default_parse_config()` / `default_transform_config()` in Python and `defaultParseConfig()` / `defaultTransformConfig()` in JavaScript return the engine's profile defaults. Rust adds `TransformEngine::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-drop` and `uproot-drop` drop the AMS `\leftroot` / `\uproot` index-position hints.

### Changed

- **Breaking:** transform reports are opt-in. `normalize` / `normalize_with` now return the canonical string and `transform` / `transform_with` return nothing, in Rust, Python, and JavaScript, so plain calls no longer pay for report collection or host-object conversion. `NormalizeResult` and the `FinalizeAstStep*` types are removed. The report itself is restructured (`rewrite.{iterations, rules}`, flattened FinalizeAst counters, `flatten_groups.guard_hits` with short guard names) and remains a diagnostic rather than a stability promise.
- **Breaking:** `TransformConfig` is organized by phase. The flat `rewrite_enabled`, `lower_attributes_enabled`, and `max_iterations` fields become `rewrite: RewriteConfig { enabled, max_iterations }` and `lower_attributes: LowerAttributesConfig`, alongside the existing `finalize_ast` and `flatten_groups`. Profile defaults are unchanged.
- **Breaking:** `FlattenGroupsConfig` exposes only `enabled` and `preserve_rendered_spacing` (`preserveRenderedSpacing` in JavaScript). The eleven `preserve_*` guard fields are removed and old keys are rejected.
- **Breaking:** serialization options are a flat set of seven fields; the nested `math` / `syntax` shape 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 as `config` is rejected. JavaScript accepts the same nested override tree in `normalize` and `transform`, with omitted fields keeping the profile default. Unknown keys and wrongly typed values raise `ConfigError` / `TexformConfigError` with the field path. The unused WASM config classes and `parseWith` are removed.
- **Breaking:** binding exception categories are corrected. Python cross-document edits raise `EditError` instead of `ParseError`. Transforming a document that has parse errors raises `TransformError` in Python and `TexformTransformError` (`kind: "transform"`) in JavaScript instead of the `internal` kind. The undocumented `Complete<T>` type is no longer importable from the npm typings.

### Fixed

- Parser recovery under `reject_unknown` without `abort_on_error` consumes only the unknown command, so `\mycmd{x} + y` yields a partial document instead of failing the whole parse. A stray `}` is no longer reported as an environment-name mismatch, and `^` / `_` with missing content report `Missing 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: `normalize` with a `flattenGroups` override now layers it on the engine profile's defaults instead of the strict baseline.
- JavaScript: internal DTO serialization failures are thrown as `TexformError` (kind `internal`) instead of being returned as successful values.
- Python: invalid per-call config objects raise `ConfigError` consistently across `parse`, `normalize`, and `transform`.

## 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:** `TransformEngine` now parses with `ParseConfig::LENIENT` by default (previously `ParseConfig::STRICT`), matching the standalone `Parser`; this applies to Rust, Python, and JavaScript. Unknown commands are preserved instead of rejected, so `normalize(r"\unknowncmd")` now succeeds, and `engine.parse` returns a recovery document with error nodes for malformed input. `normalize` still 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, pass `ParseConfig::STRICT` as the engine's default parse config, or set `reject_unknown` and `abort_on_error` to `true` (`rejectUnknown` / `abortOnError` in JavaScript).

### Fixed

- A lone prime inside an environment body, such as an `array` cell, no longer serializes as an explicit superscript group that renders as a double superscript. FlattenGroups now unwraps lone `Prime` nodes 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()` (and `to_tokenized_latex_with`) returns the canonical LaTeX string alongside ordered, typed output tokens — each classified as `ControlSequence`, `Character`, `Delimiter`, `Text`, `Raw`, or `Error`, 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 after `FlattenGroups` so newly adjacent text and prime nodes are canonicalized, and its work is reported through the `normalize_text_sequences` step counter on the transform report.
- A large batch of rewrite rules across several normalization groups: spacing drops (`\enspace`, `\quad`, `\qquad`) and merges (adjacent `\enspace` pairs, small-spacer runs); fraction styling (`\dfrac`/`\tfrac`/`\cfrac` → `\frac`, `\dbinom`/`\tbinom` → `\binom`); limit placement (`\limits`/`\nolimits` drops on audited operators); fixed delimiter sizing (`\big`–`\Bigg` size drops); negated-relation remaps (`\not=` → `\neq`, `\not\exists` → `\nexists`, `\not\in` → `\notin`, `\not\rightarrow` → `\nrightarrow`); accent sizing (`\widehat` → `\hat`, `\widetilde` → `\tilde`, the first `Equiv`-level builtins); math-mode control-space to active space; duplicate `mathrel`-wrapper drops; and named-function rewrites for plain operator names, with extended `\operatorname` canonicalization. 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 `RuleLevelSet` in place of `NormalizationLevelSet`, and the internal `NormalizationLevel` enum becomes `RuleLevel`; downstream code referring to the old names must update. Active rules are reclassified under `Authoring`, `Faithful`, `Corpus`, and `Equiv` while preserving current profile behavior.
- `\limits`/`\nolimits` drops move from `Equiv` to `Corpus`, 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-env` rule (its numbered `gather` output cannot preserve reading fidelity), the `repeat-spacer-collapse` rule, and the physics quick-`\quad` expansions (`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 `\quad` pairs merge into `\qquad` and 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: `\ast` canonicalizes to the pixel-identical literal `*`; `\dots` resolves to `\ldots` or `\cdots` from the surrounding atom class, leaving unclassifiable and boundary cases untouched; `\impliedby` expands to the source-equivalent spaced `\Longleftarrow`; the `Vmatrix` environment expands into a matrix wrapped in explicit double-bar fences; and the `\bigl`/`\bigr`/`\bigm` class variants — with their `\Big`, `\bigg`, and `\Bigg` counterparts — 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.transform` on the Python and JavaScript bindings, for normalizing a live `Document` in place. Every parsed document is stamped with a parse-context id, and a document produced by a different parser (or by `Document.from_syntax`) is rejected with a foreign-document error, so a document is only transformed by the engine that produced it.
- `:O` operator-name argspec content, so arguments to `\operatorname` and `\DeclareMathOperator` are modeled as math content and serialized compactly without special-casing command names.
- Typed serialization options for the Python bindings: `serialize()` and `Document.to_latex()` now accept a documented `SerializeOptions` TypedDict instead of an untyped `dict`, mirroring the TypeScript option interfaces.

### Changed

- **Breaking:** the public `Error` enum is now `#[non_exhaustive]`. Exhaustive `match` arms over `texform::Error` in 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^2` no longer serialize as `1 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`, and `textmacros` packages, with strict and lenient modes that preserve unknown commands and unparseable fragments as explicit nodes instead of failing the parse.
- Editable `Document` tree 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`, and `Equiv` — covering author-facing cleanup, render-faithful expansion, corpus preparation, and formula-equivalence comparison.
- `validate_argspec` for checking xparse-style argument specifications.
- Python (PyPI `texform`, Python ≥ 3.10) and JavaScript/TypeScript (npm `texform`, WebAssembly) bindings exposing the same parser, document, and transform engine from the shared Rust core.

