use super::symbols::{
char_class, char_remap, command, operator_name, styled_letter, Class, Variant,
};
#[derive(Debug, Clone)]
pub enum Node {
Symbol { ch: char, class: Class },
BigOp { ch: char, limits: bool },
OpName { text: String, limits: bool },
Group(Vec<Node>),
Frac { num: Vec<Node>, den: Vec<Node>, bar: bool },
Sqrt { index: Option<Vec<Node>>, body: Vec<Node> },
Scripts {
base: Box<Node>,
sup: Option<Vec<Node>>,
sub: Option<Vec<Node>>,
},
Delimited {
left: Option<char>,
right: Option<char>,
body: Vec<Node>,
},
SizedDelim { ch: char, class: Class, level: u8 },
Accent {
mark: char,
stretchy: bool,
body: Vec<Node>,
},
OverUnder {
body: Vec<Node>,
over: Option<char>,
under: Option<char>,
rule: bool,
},
Text(String),
Space(f32),
Array {
rows: Vec<Vec<Vec<Node>>>,
left: Option<char>,
right: Option<char>,
align_left: bool,
},
}
struct Lexer {
src: Vec<char>,
i: usize,
}
#[derive(Debug, Clone, PartialEq)]
enum Tok {
Char(char),
Cmd(String),
Open,
Close,
Sup,
Sub,
Amp,
Newline, Prime(usize),
}
impl Lexer {
fn new(s: &str) -> Self {
Lexer {
src: s.chars().collect(),
i: 0,
}
}
fn next(&mut self) -> Option<Tok> {
loop {
let c = *self.src.get(self.i)?;
self.i += 1;
match c {
' ' | '\t' | '\n' | '\r' => continue,
'{' => return Some(Tok::Open),
'}' => return Some(Tok::Close),
'^' => return Some(Tok::Sup),
'_' => return Some(Tok::Sub),
'&' => return Some(Tok::Amp),
'\'' => {
let mut n = 1;
while self.src.get(self.i) == Some(&'\'') {
n += 1;
self.i += 1;
}
return Some(Tok::Prime(n));
}
'\\' => {
let Some(&d) = self.src.get(self.i) else {
return Some(Tok::Char('\\'));
};
if d == '\\' {
self.i += 1;
return Some(Tok::Newline);
}
if d.is_ascii_alphabetic() {
let mut name = String::new();
while let Some(&e) = self.src.get(self.i) {
if e.is_ascii_alphabetic() {
name.push(e);
self.i += 1;
} else {
break;
}
}
return Some(Tok::Cmd(name));
}
self.i += 1;
return Some(Tok::Cmd(d.to_string()));
}
_ => return Some(Tok::Char(c)),
}
}
}
fn peek(&mut self) -> Option<Tok> {
let save = self.i;
let t = self.next();
self.i = save;
t
}
}
pub fn parse(src: &str) -> Vec<Node> {
let src: String = src.chars().take(20_000).collect();
let mut p = Parser {
lx: Lexer::new(&src),
depth: 0,
};
p.list(StopAt::Eof, Variant::Normal)
}
#[derive(Clone, Copy, PartialEq)]
enum StopAt {
Eof,
Brace,
Right,
}
struct Parser {
lx: Lexer,
depth: usize,
}
impl Parser {
fn list(&mut self, stop: StopAt, var: Variant) -> Vec<Node> {
let mut out: Vec<Node> = Vec::new();
if self.depth > 200 {
return out;
}
loop {
let Some(tok) = self.lx.peek() else { break };
match (&tok, stop) {
(Tok::Close, StopAt::Brace) => {
self.lx.next();
break;
}
(Tok::Cmd(c), StopAt::Right) if c == "right" => break,
(Tok::Cmd(c), _) if c == "end" => break,
(Tok::Amp, _) | (Tok::Newline, _) => break,
_ => {}
}
let Some(mut node) = self.atom(var) else { break };
while let Some(Tok::Cmd(c)) = self.lx.peek() {
let over = match c.as_str() {
"limits" => true,
"nolimits" => false,
_ => break,
};
self.lx.next();
if let Node::BigOp { limits, .. } | Node::OpName { limits, .. } =
&mut node
{
*limits = over;
}
}
let node = self.scripts(node, var);
out.push(node);
}
out
}
fn scripts(&mut self, mut base: Node, var: Variant) -> Node {
let mut sup: Option<Vec<Node>> = None;
let mut sub: Option<Vec<Node>> = None;
loop {
match self.lx.peek() {
Some(Tok::Prime(n)) => {
self.lx.next();
let primes: Vec<Node> = (0..n)
.map(|_| Node::Symbol {
ch: '\u{2032}',
class: Class::Ord,
})
.collect();
match &mut sup {
Some(s) => {
s.splice(0..0, primes);
}
None => sup = Some(primes),
}
}
Some(Tok::Sup) => {
self.lx.next();
let g = self.arg(var);
sup = Some(match sup {
Some(mut s) => {
s.extend(g);
s
}
None => g,
});
}
Some(Tok::Sub) => {
self.lx.next();
sub = Some(self.arg(var));
}
_ => break,
}
}
if sup.is_some() || sub.is_some() {
base = Node::Scripts {
base: Box::new(base),
sup,
sub,
};
}
base
}
fn arg(&mut self, var: Variant) -> Vec<Node> {
match self.lx.peek() {
Some(Tok::Open) => {
self.lx.next();
self.depth += 1;
let l = self.list(StopAt::Brace, var);
self.depth -= 1;
l
}
Some(_) => {
if let Some(n) = self.atom(var) {
vec![n]
} else {
vec![]
}
}
None => vec![],
}
}
fn atom(&mut self, var: Variant) -> Option<Node> {
let tok = self.lx.next()?;
Some(match tok {
Tok::Open => {
self.depth += 1;
let l = self.list(StopAt::Brace, var);
self.depth -= 1;
Node::Group(l)
}
Tok::Close | Tok::Sup | Tok::Sub | Tok::Prime(_) | Tok::Amp | Tok::Newline => {
Node::Symbol {
ch: match tok {
Tok::Sup => '^',
Tok::Sub => '_',
_ => '\u{FFFD}',
},
class: Class::Ord,
}
}
Tok::Char(c) => {
if c.is_ascii_alphabetic() {
Node::Symbol {
ch: styled_letter(c, if var == Variant::Normal { Variant::Italic } else { var }),
class: Class::Ord,
}
} else if c.is_ascii_digit() {
Node::Symbol {
ch: styled_letter(c, var),
class: Class::Ord,
}
} else {
Node::Symbol {
ch: char_remap(c),
class: char_class(c),
}
}
}
Tok::Cmd(name) => self.command(&name, var)?,
})
}
fn command(&mut self, name: &str, var: Variant) -> Option<Node> {
let em = match name {
"," => Some(3.0 / 18.0),
":" | ">" => Some(4.0 / 18.0),
";" => Some(5.0 / 18.0),
"!" => Some(-3.0 / 18.0),
" " => Some(6.0 / 18.0),
"quad" => Some(1.0),
"qquad" => Some(2.0),
"thinspace" => Some(3.0 / 18.0),
"enspace" => Some(0.5),
_ => None,
};
if let Some(e) = em {
return Some(Node::Space(e));
}
match name {
"frac" | "dfrac" | "tfrac" | "cfrac" => {
let num = self.arg(var);
let den = self.arg(var);
Some(Node::Frac { num, den, bar: true })
}
"binom" | "dbinom" | "tbinom" => {
let num = self.arg(var);
let den = self.arg(var);
Some(Node::Delimited {
left: Some('('),
right: Some(')'),
body: vec![Node::Frac { num, den, bar: false }],
})
}
"sqrt" => {
let index = if self.lx.peek() == Some(Tok::Char('[')) {
self.lx.next();
Some(self.until_char(']', var))
} else {
None
};
let body = self.arg(var);
Some(Node::Sqrt { index, body })
}
"left" => {
let d = self.delim_char();
self.depth += 1;
let body = self.list(StopAt::Right, var);
self.depth -= 1;
let mut right = None;
if self.lx.peek() == Some(Tok::Cmd("right".into())) {
self.lx.next();
right = self.delim_char();
}
Some(Node::Delimited { left: d, right, body })
}
"right" => {
let _ = self.delim_char();
None
}
"bigl" | "bigr" | "big" | "bigm" => self.sized(1),
"Bigl" | "Bigr" | "Big" | "Bigm" => self.sized(2),
"biggl" | "biggr" | "bigg" | "biggm" => self.sized(3),
"Biggl" | "Biggr" | "Bigg" | "Biggm" => self.sized(4),
"mathbf" | "boldsymbol" | "bm" | "pmb" => Some(self.styled(Variant::Bold)),
"mathit" => Some(self.styled(Variant::Italic)),
"mathrm" | "mathnormal" => Some(self.styled(Variant::Roman)),
"mathsf" | "textsf" => Some(self.styled(Variant::SansSerif)),
"mathtt" | "texttt" => Some(self.styled(Variant::Mono)),
"mathcal" => Some(self.styled(Variant::Script)),
"mathscr" => Some(self.styled(Variant::Script)),
"mathbb" => Some(self.styled(Variant::DoubleStruck)),
"mathfrak" => Some(self.styled(Variant::Fraktur)),
"text" | "textnormal" | "mbox" | "textbf" | "textit" => {
Some(Node::Text(self.raw_group()))
}
"operatorname" => {
let t = self.raw_group();
Some(Node::OpName {
text: t,
limits: false,
})
}
"hat" => self.accent('\u{0302}', false, var),
"widehat" => self.accent('\u{0302}', true, var),
"tilde" => self.accent('\u{0303}', false, var),
"widetilde" => self.accent('\u{0303}', true, var),
"bar" => self.accent('\u{0304}', false, var),
"vec" => self.accent('\u{20D7}', false, var),
"dot" => self.accent('\u{0307}', false, var),
"ddot" => self.accent('\u{0308}', false, var),
"acute" => self.accent('\u{0301}', false, var),
"grave" => self.accent('\u{0300}', false, var),
"check" => self.accent('\u{030C}', false, var),
"breve" => self.accent('\u{0306}', false, var),
"mathring" => self.accent('\u{030A}', false, var),
"overline" => {
let body = self.arg(var);
Some(Node::OverUnder {
body,
over: Some('\u{2015}'),
under: None,
rule: true,
})
}
"underline" => {
let body = self.arg(var);
Some(Node::OverUnder {
body,
over: None,
under: Some('\u{2015}'),
rule: true,
})
}
"overbrace" => {
let body = self.arg(var);
Some(Node::OverUnder {
body,
over: Some('\u{23DE}'),
under: None,
rule: false,
})
}
"underbrace" => {
let body = self.arg(var);
Some(Node::OverUnder {
body,
over: None,
under: Some('\u{23DF}'),
rule: false,
})
}
"begin" => self.environment(var),
"end" => {
let _ = self.raw_group();
None
}
"limits" | "nolimits" | "displaystyle" | "textstyle"
| "scriptstyle" | "scriptscriptstyle" => Some(Node::Space(0.0)),
"not" => {
let nxt = self.atom(var)?;
Some(Node::Accent {
mark: '\u{0338}',
stretchy: false,
body: vec![nxt],
})
}
_ => {
if let Some((s, c)) = command(name) {
if c == Class::Op {
Some(Node::BigOp {
ch: s,
limits: !matches!(name, "int" | "iint" | "iiint" | "oint"),
})
} else {
Some(Node::Symbol { ch: s, class: c })
}
} else if let Some((text, limits)) = operator_name(name) {
Some(Node::OpName {
text: text.to_string(),
limits,
})
} else {
Some(Node::Text(format!("\\{name}")))
}
}
}
}
fn sized(&mut self, level: u8) -> Option<Node> {
let d = self.delim_char()?;
Some(Node::SizedDelim {
ch: d,
class: match d {
'(' | '[' | '{' | '\u{27E8}' | '\u{2308}' | '\u{230A}' => Class::Open,
_ => Class::Close,
},
level,
})
}
fn styled(&mut self, v: Variant) -> Node {
Node::Group(self.arg(v))
}
fn accent(&mut self, mark: char, stretchy: bool, var: Variant) -> Option<Node> {
let body = self.arg(var);
Some(Node::Accent {
mark,
stretchy,
body,
})
}
fn delim_char(&mut self) -> Option<char> {
match self.lx.next()? {
Tok::Char('.') => None,
Tok::Char('(') => Some('('),
Tok::Char(')') => Some(')'),
Tok::Char('[') => Some('['),
Tok::Char(']') => Some(']'),
Tok::Char('|') => Some('\u{007C}'),
Tok::Char('/') => Some('/'),
Tok::Char(c) => Some(c),
Tok::Open => Some('{'),
Tok::Close => Some('}'),
Tok::Cmd(c) => command(&c).map(|(s, _)| s).or(match c.as_str() {
"{" => Some('{'),
"}" => Some('}'),
"|" => Some('\u{2016}'),
_ => None,
}),
_ => None,
}
}
fn raw_group(&mut self) -> String {
let lx = &mut self.lx;
while matches!(lx.src.get(lx.i), Some(' ' | '\t' | '\n' | '\r')) {
lx.i += 1;
}
let mut s = String::new();
match lx.src.get(lx.i).copied() {
Some('{') => {
lx.i += 1;
let mut depth = 1usize;
while let Some(&c) = lx.src.get(lx.i) {
lx.i += 1;
match c {
'\\' => {
if let Some(&n) = lx.src.get(lx.i) {
s.push(n);
lx.i += 1;
} else {
s.push('\\');
}
}
'{' => {
depth += 1;
s.push('{');
}
'}' => {
depth -= 1;
if depth == 0 {
break;
}
s.push('}');
}
_ => s.push(c),
}
}
}
Some(c) => {
lx.i += 1;
s.push(c);
}
None => {}
}
s
}
fn until_char(&mut self, end: char, var: Variant) -> Vec<Node> {
let mut out = Vec::new();
while let Some(t) = self.lx.peek() {
if t == Tok::Char(end) {
self.lx.next();
break;
}
let Some(n) = self.atom(var) else { break };
out.push(self.scripts(n, var));
}
out
}
fn environment(&mut self, var: Variant) -> Option<Node> {
let name = self.raw_group();
let name = name.trim();
let (left, right, align_left) = match name {
"pmatrix" => (Some('('), Some(')'), false),
"bmatrix" => (Some('['), Some(']'), false),
"Bmatrix" => (Some('{'), Some('}'), false),
"vmatrix" => (Some('\u{007C}'), Some('\u{007C}'), false),
"Vmatrix" => (Some('\u{2016}'), Some('\u{2016}'), false),
"cases" => (Some('{'), None, true),
"matrix" | "array" | "aligned" | "align" | "align*" | "alignedat"
| "gathered" | "smallmatrix" => (None, None, name.starts_with("align")),
_ => (None, None, false),
};
let mut rows: Vec<Vec<Vec<Node>>> = vec![vec![]];
loop {
match self.lx.peek() {
None => break,
Some(Tok::Cmd(c)) if c == "end" => {
self.lx.next();
let _ = self.raw_group();
break;
}
Some(Tok::Newline) => {
self.lx.next();
rows.push(vec![]);
}
Some(Tok::Amp) => {
self.lx.next();
rows.last_mut().unwrap().push(vec![]);
}
_ => {
let cell = self.list(StopAt::Eof, var);
if cell.is_empty() {
if matches!(self.lx.peek(), Some(Tok::Amp) | Some(Tok::Newline)) {
continue;
}
match self.lx.peek() {
Some(Tok::Cmd(c)) if c == "end" => continue,
None => break,
_ => {
self.lx.next();
}
}
} else {
let row = rows.last_mut().unwrap();
if row.is_empty() {
row.push(cell);
} else {
row.last_mut().unwrap().extend(cell);
}
}
}
}
}
if rows.last().map(|r| r.is_empty()).unwrap_or(false) {
rows.pop();
}
Some(Node::Array {
rows,
left,
right,
align_left,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frac_and_scripts() {
let n = parse("x^2 + \\frac{1}{2}");
assert!(matches!(n[0], Node::Scripts { .. }));
assert!(n.iter().any(|t| matches!(t, Node::Frac { .. })));
}
#[test]
fn sqrt_with_index() {
let n = parse("\\sqrt[3]{x}");
match &n[0] {
Node::Sqrt { index, body } => {
assert!(index.is_some());
assert_eq!(body.len(), 1);
}
_ => panic!("expected sqrt"),
}
}
#[test]
fn left_right_delimiters() {
let n = parse("\\left( a \\right)");
assert!(matches!(
n[0],
Node::Delimited {
left: Some('('),
right: Some(')'),
..
}
));
}
#[test]
fn unknown_command_degrades_to_text() {
let n = parse("\\foobar x");
assert!(matches!(&n[0], Node::Text(t) if t == "\\foobar"));
}
#[test]
fn adversarial_inputs_do_not_panic() {
for s in [
"", "{", "}", "^", "_", "\\", "\\\\", "\\frac", "\\sqrt[",
"\\left(", "{{{{{{{{{{", "\\begin{matrix}", "a & b \\\\ c",
"x^^_2", "\\frac{}{}", "\\\\\\\\",
] {
let _ = parse(s);
}
}
#[test]
fn matrix_environment() {
let n = parse("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}");
match &n[0] {
Node::Array { rows, left, .. } => {
assert_eq!(*left, Some('('));
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].len(), 2);
}
_ => panic!("expected array"),
}
}
fn only(src: &str) -> Node {
let mut n = parse(src);
assert_eq!(n.len(), 1, "expected a single node from {src:?}: {n:?}");
n.pop().unwrap()
}
#[test]
fn scripts_super_sub_and_order() {
for src in ["x^2_n", "x_n^2"] {
match only(src) {
Node::Scripts { sup, sub, .. } => {
assert!(sup.is_some() && sub.is_some(), "{src}");
}
other => panic!("{src}: expected Scripts, got {other:?}"),
}
}
}
#[test]
fn primes_become_superscript_runs() {
match only("x''") {
Node::Scripts { sup: Some(s), sub, .. } => {
assert!(sub.is_none());
assert_eq!(s.len(), 2);
assert!(s.iter().all(|t| matches!(
t,
Node::Symbol { ch: '\u{2032}', .. }
)));
}
other => panic!("expected primed Scripts, got {other:?}"),
}
match only("x'^2") {
Node::Scripts { sup: Some(s), .. } => {
assert!(s.len() >= 2);
assert!(matches!(s[0], Node::Symbol { ch: '\u{2032}', .. }));
}
other => panic!("expected merged prime+sup, got {other:?}"),
}
}
#[test]
fn frac_family_and_binom() {
for cmd in ["frac", "dfrac", "tfrac", "cfrac"] {
assert!(matches!(
only(&format!("\\{cmd}{{1}}{{2}}")),
Node::Frac { bar: true, .. }
));
}
match only("\\binom{n}{k}") {
Node::Delimited { left: Some('('), right: Some(')'), body } => {
assert!(matches!(body[0], Node::Frac { bar: false, .. }));
}
other => panic!("expected delimited binom, got {other:?}"),
}
}
#[test]
fn sqrt_without_index() {
match only("\\sqrt{x+1}") {
Node::Sqrt { index, body } => {
assert!(index.is_none());
assert!(!body.is_empty());
}
other => panic!("expected sqrt, got {other:?}"),
}
}
#[test]
fn null_and_sized_delimiters() {
match only("\\left. x \\right|") {
Node::Delimited { left: None, right: Some('\u{007C}'), .. } => {}
other => panic!("expected null-left delimited, got {other:?}"),
}
assert!(matches!(
parse("\\bigl(")[0],
Node::SizedDelim { ch: '(', level: 1, .. }
));
assert!(matches!(
parse("\\Biggl[")[0],
Node::SizedDelim { ch: '[', level: 4, .. }
));
}
#[test]
fn accents_and_over_under() {
assert!(matches!(
only("\\hat{x}"),
Node::Accent { mark: '\u{0302}', stretchy: false, .. }
));
assert!(matches!(
only("\\widehat{x}"),
Node::Accent { mark: '\u{0302}', stretchy: true, .. }
));
assert!(matches!(
only("\\vec{v}"),
Node::Accent { mark: '\u{20D7}', .. }
));
assert!(matches!(
only("\\overline{x}"),
Node::OverUnder { over: Some(_), rule: true, .. }
));
assert!(matches!(
only("\\underbrace{x}"),
Node::OverUnder { under: Some('\u{23DF}'), rule: false, .. }
));
}
#[test]
fn text_operatorname_and_spacing() {
assert!(matches!(only("\\text{hi there}"), Node::Text(t) if t == "hi there"));
match only("\\operatorname{lcm}") {
Node::OpName { text, limits: false } => assert_eq!(text, "lcm"),
other => panic!("expected OpName, got {other:?}"),
}
let n = parse("a\\,b\\quad c\\!d");
let spaces: Vec<f32> = n
.iter()
.filter_map(|x| match x {
Node::Space(e) => Some(*e),
_ => None,
})
.collect();
assert!(spaces.contains(&(3.0 / 18.0)));
assert!(spaces.contains(&1.0));
assert!(spaces.iter().any(|&e| e < 0.0), "\\! is negative space");
}
#[test]
fn font_switches_bake_styled_codepoints() {
assert!(matches!(
only("x"),
Node::Symbol { ch: '\u{1D465}', .. }
));
match only("\\mathbb{R}") {
Node::Group(inner) => assert!(matches!(
inner[0],
Node::Symbol { ch: '\u{211D}', .. }
)),
other => panic!("expected group, got {other:?}"),
}
match only("\\mathbf{A}") {
Node::Group(inner) => assert!(matches!(
inner[0],
Node::Symbol { ch: '\u{1D400}', .. }
)),
other => panic!("expected group, got {other:?}"),
}
}
#[test]
fn big_operator_limits_flag() {
assert!(matches!(
only("\\sum"),
Node::BigOp { ch: '\u{2211}', limits: true }
));
assert!(matches!(
only("\\int"),
Node::BigOp { ch: '\u{222B}', limits: false }
));
}
#[test]
fn limits_and_nolimits_override_the_default() {
match &parse("\\int\\limits_{0}^{1}")[0] {
Node::Scripts { base, .. } => assert!(matches!(
**base,
Node::BigOp { ch: '\u{222B}', limits: true }
)),
other => panic!("expected scripted ∫, got {other:?}"),
}
match &parse("\\sum\\nolimits_{i}")[0] {
Node::Scripts { base, .. } => assert!(matches!(
**base,
Node::BigOp { ch: '\u{2211}', limits: false }
)),
other => panic!("expected scripted ∑, got {other:?}"),
}
let _ = parse("x\\limits^2");
}
#[test]
fn environments_shapes() {
match only("\\begin{bmatrix} 1 \\\\ 2 \\end{bmatrix}") {
Node::Array { left: Some('['), right: Some(']'), rows, .. } => {
assert_eq!(rows.len(), 2);
}
other => panic!("expected bmatrix, got {other:?}"),
}
match only("\\begin{cases} a & x>0 \\\\ b & x\\le 0 \\end{cases}") {
Node::Array { left: Some('{'), right: None, align_left: true, rows } => {
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].len(), 2);
}
other => panic!("expected cases, got {other:?}"),
}
match only("\\begin{aligned} a &= b \\\\ &= c \\end{aligned}") {
Node::Array { left: None, right: None, align_left: true, rows } => {
assert_eq!(rows.len(), 2);
}
other => panic!("expected aligned, got {other:?}"),
}
}
#[test]
fn larger_adversarial_corpus_never_panics() {
for s in [
"\\frac{\\frac{\\frac{a}{b}}{c}}{d}",
"\\sqrt[\\sqrt{2}]{\\sqrt{x}}",
"x^_^_^",
"\\left\\left\\left",
"\\right)\\right)",
"\\begin{matrix}",
"\\end{matrix}",
"&&&\\\\\\\\&&",
"\\mathbb{\\frac{1}{2}}",
"^{^{^{^{^{x}}}}}",
"\\hat{\\hat{\\hat{\\hat{x}}}}",
"\\text{",
"\\operatorname",
"\\\\begin{cases}",
"$\\%\\#\\&\\_\\{\\}$",
&"{".repeat(5000),
&"x^".repeat(5000),
] {
let _ = parse(s);
}
}
#[test]
fn unbraced_single_token_arguments() {
match only("\\frac ab") {
Node::Frac { num, den, .. } => {
assert_eq!(num.len(), 1);
assert_eq!(den.len(), 1);
}
other => panic!("expected \\frac a b, got {other:?}"),
}
match only("\\sqrt x") {
Node::Sqrt { index: None, body } => assert_eq!(body.len(), 1),
other => panic!("expected \\sqrt x, got {other:?}"),
}
let n = parse("x^2y");
assert!(matches!(n[0], Node::Scripts { .. }));
assert_eq!(n.len(), 2, "y must be its own atom: {n:?}");
}
#[test]
fn scriptless_and_degenerate_scripts_dont_panic() {
let _ = parse("^2");
let _ = parse("_n");
let _ = parse("{}^{2}");
let n = parse("x_a_b c");
assert!(matches!(n[0], Node::Scripts { .. }));
assert!(n.iter().any(|t| matches!(
t,
Node::Symbol { ch, .. } if *ch == super::super::symbols::styled_letter('c', super::super::symbols::Variant::Italic)
)));
let _ = parse("\\sqrt[3{x}");
let _ = parse("\\sqrt[}");
}
#[test]
fn common_commands_all_resolve() {
let structural = [
"frac", "dfrac", "tfrac", "binom", "sqrt", "left",
"text", "operatorname", "hat", "vec", "bar", "tilde", "dot",
"overline", "underline", "overbrace", "underbrace", "mathbb",
"mathbf", "mathcal", "mathrm", "mathfrak", "mathsf",
"quad", "qquad", "bigl", "Big",
];
for cmd in [
"alpha", "beta", "gamma", "pi", "theta", "lambda", "mu", "phi",
"omega", "Gamma", "Delta", "Omega", "sum", "prod", "int", "oint",
"lim", "sin", "cos", "tan", "log", "ln", "exp", "min", "max",
"leq", "geq", "neq", "approx", "equiv", "in", "subset", "cup",
"cap", "to", "rightarrow", "Rightarrow", "mapsto", "infty",
"partial", "nabla", "forall", "exists", "times", "div", "pm",
"cdot", "circ", "oplus", "otimes", "langle", "rangle", "lfloor",
"rfloor", "lceil", "rceil", "hbar", "ell", "Re", "Im", "aleph",
"emptyset", "ldots", "cdots", "vdots", "wedge", "vee", "neg",
"uparrow", "downarrow", "cong", "sim", "propto", "perp",
"parallel", "mid", "setminus", "star", "ast", "bullet", "dagger",
] {
let n = parse(&format!("\\{cmd}"));
assert_eq!(n.len(), 1, "\\{cmd} produced {} nodes", n.len());
let resolved = matches!(
&n[0],
Node::Symbol { .. }
| Node::BigOp { .. }
| Node::OpName { .. }
| Node::Space(_)
);
assert!(
resolved,
"\\{cmd} did not resolve to a symbol/op (got {:?}) — \
likely a typo'd or trailing-space table key",
n[0]
);
}
for cmd in structural {
let n = parse(&format!("\\{cmd}{{x}}{{y}}"));
let head = n.first().unwrap_or_else(|| panic!("\\{cmd} produced nothing"));
assert!(
!matches!(head, Node::Text(t) if t.starts_with('\\')),
"\\{cmd} fell through to literal text"
);
}
}
}