Files
craft-libs/crates/math/src/math_tree.rs
T
Dylan M. Taylorandshuff57 85b4dabf0f Add craft-math: equations as one tree with OMML, MathML, LaTeX and UnicodeMath (#1013)
* 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.

---------

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

105 lines
4.0 KiB
Rust

//! Structural access to an equation tree: the arguments of a node, and every run in a tree.
use crate::math::{Arg, MAX_DEPTH, MNode, MRun};
/// Number of child arguments of a node.
pub fn child_count(n: &MNode) -> usize {
match n {
MNode::Run(_) => 0,
MNode::Frac { .. } | MNode::Func { .. } | MNode::Lim { .. } => 2,
MNode::Rad { .. } => 2,
MNode::Script { .. } | MNode::Nary { .. } => 3,
MNode::Delim { elems, .. } => elems.len(),
MNode::EqArr { rows } => rows.len(),
MNode::Matrix { rows, .. } => rows.iter().map(|r| r.len()).sum(),
MNode::Acc { .. } | MNode::Bar { .. } | MNode::BorderBox { .. } | MNode::Boxed { .. } | MNode::GroupChr { .. } | MNode::Phant { .. } => 1,
}
}
/// Child argument `i` of a node (see [`child_count`] for the numbering: fraction num 0 / den 1;
/// script base 0 / sub 1 / sup 2; radical degree 0 / base 1; n-ary sub 0 / sup 1 / operand 2;
/// function name 0 / argument 1; limit base 0 / limit 1; matrices row by row).
pub fn child(n: &MNode, i: usize) -> Option<&Arg> {
match (n, i) {
(MNode::Frac { num, .. }, 0) => Some(num),
(MNode::Frac { den, .. }, 1) => Some(den),
(MNode::Script { base, .. }, 0) => Some(base),
(MNode::Script { sub, .. }, 1) => Some(sub),
(MNode::Script { sup, .. }, 2) => Some(sup),
(MNode::Rad { deg, .. }, 0) => Some(deg),
(MNode::Rad { e, .. }, 1) => Some(e),
(MNode::Nary { sub, .. }, 0) => Some(sub),
(MNode::Nary { sup, .. }, 1) => Some(sup),
(MNode::Nary { e, .. }, 2) => Some(e),
(MNode::Delim { elems, .. }, i) => elems.get(i),
(MNode::Func { name, .. }, 0) => Some(name),
(MNode::Func { e, .. }, 1) => Some(e),
(MNode::Lim { e, .. }, 0) => Some(e),
(MNode::Lim { lim, .. }, 1) => Some(lim),
(
MNode::Acc { e, .. }
| MNode::Bar { e, .. }
| MNode::BorderBox { e, .. }
| MNode::Boxed { e }
| MNode::GroupChr { e, .. }
| MNode::Phant { e, .. },
0,
) => Some(e),
(MNode::EqArr { rows }, i) => rows.get(i),
(MNode::Matrix { rows, .. }, i) => rows.iter().flatten().nth(i),
_ => None,
}
}
pub fn child_mut(n: &mut MNode, i: usize) -> Option<&mut Arg> {
match (n, i) {
(MNode::Frac { num, .. }, 0) => Some(num),
(MNode::Frac { den, .. }, 1) => Some(den),
(MNode::Script { base, .. }, 0) => Some(base),
(MNode::Script { sub, .. }, 1) => Some(sub),
(MNode::Script { sup, .. }, 2) => Some(sup),
(MNode::Rad { deg, .. }, 0) => Some(deg),
(MNode::Rad { e, .. }, 1) => Some(e),
(MNode::Nary { sub, .. }, 0) => Some(sub),
(MNode::Nary { sup, .. }, 1) => Some(sup),
(MNode::Nary { e, .. }, 2) => Some(e),
(MNode::Delim { elems, .. }, i) => elems.get_mut(i),
(MNode::Func { name, .. }, 0) => Some(name),
(MNode::Func { e, .. }, 1) => Some(e),
(MNode::Lim { e, .. }, 0) => Some(e),
(MNode::Lim { lim, .. }, 1) => Some(lim),
(
MNode::Acc { e, .. }
| MNode::Bar { e, .. }
| MNode::BorderBox { e, .. }
| MNode::Boxed { e }
| MNode::GroupChr { e, .. }
| MNode::Phant { e, .. },
0,
) => Some(e),
(MNode::EqArr { rows }, i) => rows.get_mut(i),
(MNode::Matrix { rows, .. }, i) => rows.iter_mut().flatten().nth(i),
_ => None,
}
}
/// Call `f` on every run in the tree.
pub fn each_run_mut(root: &mut Arg, f: &mut dyn FnMut(&mut MRun)) {
fn go(a: &mut Arg, f: &mut dyn FnMut(&mut MRun), depth: usize) {
if depth > MAX_DEPTH {
return;
}
for n in a.iter_mut() {
if let MNode::Run(r) = n {
f(r);
}
for c in 0..child_count(n) {
if let Some(ch) = child_mut(n, c) {
go(ch, f, depth + 1);
}
}
}
}
go(root, f, 0);
}