craft-math: operator names take their LaTeX scripts directly again (restores #1019)

#1019 (22161d2d) wrote \sin^2\theta as \sin ^{2}{\theta } so it reads back as
the same tree. The squash merge of #1038, whose branch predated it, put
math_latex.rs and the corpus test back as they were before. This applies
#1019's change again, unchanged, with its test.
This commit is contained in:
Dylan M. Taylor
2026-11-12 02:05:56 +00:00
parent 5538d8a0d7
commit a3e8e6dc5c
2 changed files with 27 additions and 9 deletions
+25 -1
View File
@@ -982,7 +982,12 @@ fn script_node(kind: ScriptKind, base: &[MNode], sub: &[MNode], sup: &[MNode], o
braced(base, output, depth);
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) {
output.push('_');
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) {
output.push_str("\\sqrt");
if !degree_hidden && !degree.is_empty() {
@@ -1289,4 +1299,18 @@ mod tests {
// The writer keeps its own names.
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}");
}
}
+2 -8
View File
@@ -18,10 +18,6 @@ fn equations() -> Vec<(String, math::Arg, bool)> {
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]
fn every_corpus_equation_reads_without_unknown_commands() {
for (id, tree, _) in equations() {
@@ -54,10 +50,8 @@ fn every_corpus_equation_survives_mathml_and_latex() {
let lin = math::to_linear(&tree);
let ml = mathml::to_mathml(&tree, display, "");
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);
assert_eq!(math::to_linear(&math_latex::parse_latex(&tex)), lin, "{id}: {tex}");
}
let tex = math_latex::to_latex(&tree);
assert_eq!(math::to_linear(&math_latex::parse_latex(&tex)), lin, "{id}: {tex}");
}
}