Files
craft-libs/crates/math
Dylan M. Taylorandshuff57 22161d2d6a craft-math: operator names take their LaTeX scripts directly (\sin^2\theta round trip) (#1019)
* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath

The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.

mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.

* craft-math: read MathLive's and other spellings of LaTeX math commands

\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).

* craft-math: equaSHion's equation corpus as test data

corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.

* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README

The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.

* craft-math: an operator name with a script keeps applying to its argument in LaTeX

to_latex wrote \sin^2\theta as {\sin }^{2}{\theta }, which reads back as
(\sin)^2 followed by θ: the braces cut the name off from its argument. An
operator name written as a command now takes its scripts directly
(\sin ^{2}{\theta }). The corpus test no longer needs its exception.

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
2026-11-11 20:36:47 -05:00
..

craft-math

Equations as one tree, and the syntaxes it is read from and written to:

Module Syntax Read Write
math the tree: MNode, MRun, Math (an equation with its kept source OMML), modelled on the Office Math object model (ECMA-376 Part 1 §22.1)
math_linear UnicodeMath, the linear format Word shows (x=(-b±√(b^2-4ac))/2a) math::parse_linear math::to_linear
math_latex LaTeX math, including MathLive's spellings parse_latex to_latex
omml Office Math Markup (m:oMath, DOCX/PPTX/XLSX) read_omath, read_str write_equation, write_str
mathml presentation MathML (HTML, ODF formulas, the clipboard) parse, read, to_tree to_mathml, linear_to_mathml

Beside them: math_symbols (LaTeX names of symbols), math_tree (the arguments of a node, a walk over every run), symbol_fonts (Symbol, Wingdings and the other symbol fonts as Unicode) and Rgb (a run's colour).

use craft_math::{Math, math, math_latex, mathml, omml};

let tree = math_latex::parse_latex(r"\frac{a}{b}+\sqrt{x}");
assert_eq!(math::to_linear(&tree), "a/b+√x");

// OMML out and back.
let eq = Math { nodes: tree.clone(), ..Default::default() };
let xml = omml::write_str("", false, &eq, None);
let read = omml::read_str(&xml).unwrap();
assert_eq!(read.nodes, tree);

// MathML carries the linear format in an annotation, so it reads back exactly.
let ml = mathml::linear_to_mathml("a/b+√x", true);
assert_eq!(mathml::parse(&ml).and_then(|m| mathml::read(&m)), Some(("a/b+√x".to_string(), true)));

Reading OMML

omml::read_omath takes an m:oMath element of a craft_xml tree read with the canonical prefixes of omml::NAMESPACES (m, w and mc, transitional and strict URIs), so a document reader passes the element it already has. omml::read_str and omml::parse read a string. The source XML is kept in Math::omml; write_equation writes it back unchanged while the equation's colour is the same and the XML is well formed, so a saved document carries the equation as Word wrote it.

Limits

Nesting is bounded by math::MAX_DEPTH in every reader and writer; omml::parse reads at most omml::MAX_ELEMENTS elements and mathml::parse keeps at most mathml::MAX_NODES; the LaTeX reader looks at 20,000 characters. Nothing panics on any input.

Provenance

The tree, the linear format, LaTeX, the symbol names, the tree walk, the symbol fonts and Rgb are WordCraft's (crates/doc), as moved into craft-wordmodel; OMML is WordCraft's DOCX reader and writer (crates/docx/src/read/math.rs, write/math.rs) and MathML its formats crate's (crates/formats/src/mathml.rs), moved onto craft-xml. The MathLive command spellings and the round-trip corpus in tests/corpus/ come from equaSHion (storytold/craft-libs#29).