mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
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>
This commit is contained in:
co-authored by
shuff57
parent
24903d320d
commit
22161d2d6a
@@ -982,7 +982,12 @@ fn script_node(kind: ScriptKind, base: &[MNode], sub: &[MNode], sup: &[MNode], o
|
|||||||
braced(base, output, depth);
|
braced(base, output, depth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
braced(base, output, depth);
|
// An operator name takes its scripts directly (`\sin^{2}`): in braces it would no longer apply
|
||||||
|
// to what follows it when read back.
|
||||||
|
match base {
|
||||||
|
[MNode::Run(r)] if is_function_name(r) => run_node(r, output),
|
||||||
|
_ => braced(base, output, depth),
|
||||||
|
}
|
||||||
if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup) {
|
if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup) {
|
||||||
output.push('_');
|
output.push('_');
|
||||||
braced(sub, output, depth);
|
braced(sub, output, depth);
|
||||||
@@ -993,6 +998,11 @@ fn script_node(kind: ScriptKind, base: &[MNode], sub: &[MNode], sup: &[MNode], o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is `r` an operator name the writer writes as a command (`\sin`, `\log`)?
|
||||||
|
fn is_function_name(r: &MRun) -> bool {
|
||||||
|
!r.nor && r.sty == Some(MSty::Plain) && r.scr == MScr::Roman && FUNCTION_NAMES.contains(&r.text.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
fn radical_node(degree: &[MNode], degree_hidden: bool, expression: &[MNode], output: &mut String, depth: usize) {
|
fn radical_node(degree: &[MNode], degree_hidden: bool, expression: &[MNode], output: &mut String, depth: usize) {
|
||||||
output.push_str("\\sqrt");
|
output.push_str("\\sqrt");
|
||||||
if !degree_hidden && !degree.is_empty() {
|
if !degree_hidden && !degree.is_empty() {
|
||||||
@@ -1289,4 +1299,18 @@ mod tests {
|
|||||||
// The writer keeps its own names.
|
// The writer keeps its own names.
|
||||||
assert_eq!(to_latex(&parse_latex(r"\infinity")), to_latex(&parse_latex(r"\infty")));
|
assert_eq!(to_latex(&parse_latex(r"\infinity")), to_latex(&parse_latex(r"\infty")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An operator name with a script still applies to its argument when the LaTeX is read back.
|
||||||
|
#[test]
|
||||||
|
fn operator_names_take_their_scripts_directly() {
|
||||||
|
for src in [r"\sin^2\theta+\cos^2\theta=1", r"\log_2 x", r"\ln^{3}(x+1)"] {
|
||||||
|
let tree = parse_latex(src);
|
||||||
|
let tex = to_latex(&tree);
|
||||||
|
assert_eq!(crate::math::to_linear(&parse_latex(&tex)), crate::math::to_linear(&tree), "{src}: {tex}");
|
||||||
|
assert!(!tex.contains(r"{\sin }") && !tex.contains(r"{\log }"), "{tex}");
|
||||||
|
}
|
||||||
|
assert!(to_latex(&parse_latex(r"\sin^2\theta")).starts_with(r"\sin ^{2}"));
|
||||||
|
// Other bases keep their braces.
|
||||||
|
assert_eq!(to_latex(&parse_latex("x^2")), "{x}^{2}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ fn equations() -> Vec<(String, math::Arg, bool)> {
|
|||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Equations whose LaTeX this crate writes in a form it reads back differently (a function
|
|
||||||
/// raised to a power: `\sin^2\theta` comes back as `(\sin)^2 θ`). Known; kept visible here.
|
|
||||||
const LATEX_ROUND_TRIP_GAPS: &[&str] = &["func"];
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_corpus_equation_reads_without_unknown_commands() {
|
fn every_corpus_equation_reads_without_unknown_commands() {
|
||||||
for (id, tree, _) in equations() {
|
for (id, tree, _) in equations() {
|
||||||
@@ -54,10 +50,8 @@ fn every_corpus_equation_survives_mathml_and_latex() {
|
|||||||
let lin = math::to_linear(&tree);
|
let lin = math::to_linear(&tree);
|
||||||
let ml = mathml::to_mathml(&tree, display, "");
|
let ml = mathml::to_mathml(&tree, display, "");
|
||||||
assert_eq!(mathml::parse(&ml).and_then(|m| mathml::read(&m)), Some((lin.clone(), display)), "{id}: {ml}");
|
assert_eq!(mathml::parse(&ml).and_then(|m| mathml::read(&m)), Some((lin.clone(), display)), "{id}: {ml}");
|
||||||
if !LATEX_ROUND_TRIP_GAPS.contains(&id.as_str()) {
|
let tex = math_latex::to_latex(&tree);
|
||||||
let tex = math_latex::to_latex(&tree);
|
assert_eq!(math::to_linear(&math_latex::parse_latex(&tex)), lin, "{id}: {tex}");
|
||||||
assert_eq!(math::to_linear(&math_latex::parse_latex(&tex)), lin, "{id}: {tex}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user