From 2798d11550a3c55bbdfcb5657f4667336b2d2c15 Mon Sep 17 00:00:00 2001 From: shuff57 <62350898+shuff57@users.noreply.github.com> Date: Thu, 12 Nov 2026 00:36:41 +0000 Subject: [PATCH] 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). --- crates/math/src/math_latex.rs | 56 +++++++++++++++++++++++++++++++-- crates/math/src/math_symbols.rs | 9 ++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/crates/math/src/math_latex.rs b/crates/math/src/math_latex.rs index 92979534..5f442f43 100644 --- a/crates/math/src/math_latex.rs +++ b/crates/math/src/math_latex.rs @@ -24,7 +24,7 @@ enum Tok { } /// Commands whose `{…}` argument is text, where spaces are part of the content. -const TEXT_COMMANDS: &[&str] = &["text", "textrm", "mbox", "textnormal", "textbf", "textit"]; +const TEXT_COMMANDS: &[&str] = &["text", "textrm", "mbox", "hbox", "textnormal", "textbf", "textit", "emph", "textsf"]; fn tokenize(s: &str) -> Vec { let cs: Vec = s.chars().take(20_000).collect(); @@ -382,7 +382,7 @@ impl Lp { let env = self.raw_arg(); self.environment(env.trim()) } - "text" | "textrm" | "mbox" | "textnormal" => { + "text" | "textrm" | "mbox" | "hbox" | "textnormal" => { let t = self.raw_arg(); one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Plain), ..Default::default() })) } @@ -390,10 +390,22 @@ impl Lp { let t = self.raw_arg(); one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Bold), ..Default::default() })) } - "textit" => { + "textit" | "emph" => { let t = self.raw_arg(); one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Italic), ..Default::default() })) } + "textsf" => { + let t = self.raw_arg(); + one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Plain), scr: MScr::SansSerif, ..Default::default() })) + } + // MathLive's absolute value, upright constants and differential. + "abs" => { + let e = self.arg(); + one(MNode::Delim { beg: Some('|'), end: Some('|'), sep: None, grow: true, shp_match: false, elems: vec![e] }) + } + "differentialD" => vec![run("d")], + "exponentialE" => vec![MNode::Run(MRun::plain("e"))], + "imaginaryI" => vec![MNode::Run(MRun::plain("i"))], "operatorname" => { let starred = self.peek() == Some(&Tok::Ch('*')); if starred { @@ -428,6 +440,7 @@ impl Lp { let a = self.arg(); styled(a, Some(MSty::Bold), MScr::Roman, false) } + "mathnormal" => self.arg(), "mathit" => { let a = self.arg(); styled(a, Some(MSty::Italic), MScr::Roman, false) @@ -481,6 +494,14 @@ impl Lp { one(g) } } + "underleftarrow" | "underrightarrow" | "underleftrightarrow" => { + let chr = match c { + "underleftarrow" => '←', + "underrightarrow" => '→', + _ => '↔', + }; + one(MNode::GroupChr { chr, top: false, e: self.arg() }) + } "stackrel" | "overset" => { let over = self.arg(); let base = self.arg(); @@ -1239,4 +1260,33 @@ mod tests { let tex = to_latex(&[delim]); assert!(tex.contains(r"\middle\|"), "Expected \\middle\\|, got: {tex}"); } + + /// Commands MathLive and typed input use, from equaSHion (storytold/craft-libs#29). + #[test] + fn mathlive_and_other_spellings() { + let lin = |src: &str| crate::math::to_linear(&parse_latex(src)); + assert_eq!(lin(r"\abs{x-1}"), lin(r"\left|x-1\right|")); + assert_eq!(lin(r"\lvert x\rvert+\lVert v\rVert"), "|x|+‖v‖"); + assert_eq!(lin(r"a\nparallel b \backprime"), "a∦b‵"); + assert_eq!(lin(r"\infinity\cross 2"), "∞×2"); + assert_eq!(lin(r"\mathnormal{x}"), "x"); + let runs = parse_latex(r"\exponentialE^{\imaginaryI\pi}\differentialD x"); + assert!( + matches!(&runs[0], MNode::Script { base, .. } if matches!(&base[0], MNode::Run(r) if r.text == "e" && r.sty == Some(MSty::Plain))), + "{runs:?}" + ); + for (src, sty, scr) in [ + (r"\emph{so it is}", MSty::Italic, MScr::Roman), + (r"\textsf{so it is}", MSty::Plain, MScr::SansSerif), + (r"\hbox{so it is}", MSty::Plain, MScr::Roman), + ] { + let a = parse_latex(src); + assert!(matches!(a.as_slice(), [MNode::Run(r)] if r.text == "so it is" && r.nor && r.sty == Some(sty) && r.scr == scr), "{src}: {a:?}"); + } + for (src, want) in [(r"\underrightarrow{AB}", '→'), (r"\underleftarrow{AB}", '←'), (r"\underleftrightarrow{AB}", '↔')] { + assert!(matches!(parse_latex(src).as_slice(), [MNode::GroupChr { chr, top: false, .. }] if *chr == want), "{src}"); + } + // The writer keeps its own names. + assert_eq!(to_latex(&parse_latex(r"\infinity")), to_latex(&parse_latex(r"\infty"))); + } } diff --git a/crates/math/src/math_symbols.rs b/crates/math/src/math_symbols.rs index 28b3a7f8..279ffd2c 100644 --- a/crates/math/src/math_symbols.rs +++ b/crates/math/src/math_symbols.rs @@ -340,6 +340,15 @@ pub const SYMBOLS: &[(&str, char)] = &[ ("tvec", '\u{20E1}'), ("lhvec", '\u{20D0}'), ("hvec", '\u{20D1}'), + // Other spellings (after the names the writer uses, which come first). + ("nparallel", '∦'), + ("backprime", '‵'), + ("lvert", '|'), + ("rvert", '|'), + ("lVert", '‖'), + ("rVert", '‖'), + ("infinity", '∞'), + ("cross", '×'), ]; /// The character for a name (`alpha` → α), including `doubleR`, `scriptL`, `frakturg`.