mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
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).
This commit is contained in:
@@ -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<Tok> {
|
||||
let cs: Vec<char> = 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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user