use crate::utils::display::display_width;
use std::{cell::RefCell, rc::Rc};
const LAYOUT_START: char = '\u{f0000}';
const LAYOUT_END: char = '\u{f0001}';
const PROTECTED_SPACE: char = '\u{f0002}';
const NAMED_OP_START: char = '\u{f0004}';
const NAMED_OP_END: char = '\u{f0005}';
const NEGATIVE_SPACE: char = '\u{0}';
const SYMBOLS: &[(&str, &str)] = &[
("alpha", "α"),
("beta", "β"),
("gamma", "γ"),
("delta", "δ"),
("epsilon", "ϵ"),
("varepsilon", "ε"),
("zeta", "ζ"),
("eta", "η"),
("theta", "θ"),
("vartheta", "ϑ"),
("iota", "ι"),
("kappa", "κ"),
("varkappa", "ϰ"),
("lambda", "λ"),
("mu", "μ"),
("nu", "ν"),
("xi", "ξ"),
("pi", "π"),
("varpi", "ϖ"),
("rho", "ρ"),
("varrho", "ϱ"),
("sigma", "σ"),
("varsigma", "ς"),
("tau", "τ"),
("upsilon", "υ"),
("phi", "ϕ"),
("varphi", "φ"),
("chi", "χ"),
("psi", "ψ"),
("omega", "ω"),
("Gamma", "Γ"),
("Delta", "Δ"),
("Theta", "Θ"),
("Lambda", "Λ"),
("Xi", "Ξ"),
("Pi", "Π"),
("Sigma", "Σ"),
("Upsilon", "Υ"),
("Phi", "Φ"),
("Psi", "Ψ"),
("Omega", "Ω"),
("pm", "±"),
("mp", "∓"),
("times", "×"),
("div", "÷"),
("cdot", "·"),
("ast", "∗"),
("star", "⋆"),
("circ", "∘"),
("bullet", "•"),
("oplus", "⊕"),
("ominus", "⊖"),
("otimes", "⊗"),
("oslash", "⊘"),
("odot", "⊙"),
("bigcirc", "○"),
("dagger", "†"),
("ddagger", "‡"),
("amalg", "⨿"),
("uplus", "⊎"),
("sqcap", "⊓"),
("sqcup", "⊔"),
("triangleleft", "◁"),
("triangleright", "▷"),
("wr", "≀"),
("cap", "∩"),
("cup", "∪"),
("bigcap", "⋂"),
("bigcup", "⋃"),
("bigwedge", "⋀"),
("bigvee", "⋁"),
("bigsqcup", "⨆"),
("biguplus", "⨄"),
("bigoplus", "⨁"),
("bigotimes", "⨂"),
("bigodot", "⨀"),
("setminus", "∖"),
("in", "∈"),
("notin", "∉"),
("ni", "∋"),
("subset", "⊂"),
("supset", "⊃"),
("subseteq", "⊆"),
("supseteq", "⊇"),
("sqsubset", "⊏"),
("sqsupset", "⊐"),
("sqsubseteq", "⊑"),
("sqsupseteq", "⊒"),
("prec", "≺"),
("preceq", "≼"),
("succ", "≻"),
("succeq", "≽"),
("ll", "≪"),
("gg", "≫"),
("le", "≤"),
("leq", "≤"),
("leqslant", "≤"),
("ge", "≥"),
("geq", "≥"),
("geqslant", "≥"),
("ne", "≠"),
("neq", "≠"),
("equiv", "≡"),
("approx", "≈"),
("sim", "∼"),
("simeq", "≃"),
("cong", "≅"),
("asymp", "≍"),
("doteq", "≐"),
("propto", "∝"),
("parallel", "∥"),
("perp", "⊥"),
("mid", "∣"),
("vdash", "⊢"),
("dashv", "⊣"),
("models", "⊨"),
("Vdash", "⊩"),
("Vvdash", "⊪"),
("nvdash", "⊬"),
("nvDash", "⊭"),
("forall", "∀"),
("exists", "∃"),
("nexists", "∄"),
("neg", "¬"),
("land", "∧"),
("wedge", "∧"),
("lor", "∨"),
("vee", "∨"),
("to", "→"),
("rightarrow", "→"),
("longrightarrow", "→"),
("leftarrow", "←"),
("longleftarrow", "←"),
("gets", "←"),
("leftrightarrow", "↔"),
("longleftrightarrow", "↔"),
("hookleftarrow", "↩"),
("hookrightarrow", "↪"),
("twoheadleftarrow", "↞"),
("twoheadrightarrow", "↠"),
("leftharpoonup", "↼"),
("leftharpoondown", "↽"),
("rightharpoonup", "⇀"),
("rightharpoondown", "⇁"),
("rightleftharpoons", "⇌"),
("leftrightharpoons", "⇋"),
("nearrow", "↗"),
("searrow", "↘"),
("swarrow", "↙"),
("nwarrow", "↖"),
("rightsquigarrow", "⇝"),
("leadsto", "⇝"),
("Rightarrow", "⇒"),
("Longrightarrow", "⇒"),
("Leftarrow", "⇐"),
("Longleftarrow", "⇐"),
("Leftrightarrow", "⇔"),
("Longleftrightarrow", "⇔"),
("implies", "⇒"),
("iff", "⇔"),
("mapsto", "↦"),
("longmapsto", "↦"),
("uparrow", "↑"),
("downarrow", "↓"),
("partial", "∂"),
("nabla", "∇"),
("int", "∫"),
("iint", "∬"),
("iiint", "∭"),
("oint", "∮"),
("sum", "∑"),
("prod", "∏"),
("coprod", "∐"),
("infty", "∞"),
("emptyset", "∅"),
("varnothing", "∅"),
("angle", "∠"),
("therefore", "∴"),
("because", "∵"),
("aleph", "ℵ"),
("beth", "ℶ"),
("gimel", "ℷ"),
("daleth", "ℸ"),
("top", "⊤"),
("bot", "⊥"),
("triangle", "△"),
("square", "□"),
("lozenge", "◊"),
("checkmark", "✓"),
("complement", "∁"),
("wp", "℘"),
("prime", "′"),
("ldots", "…"),
("dots", "…"),
("cdots", "⋯"),
("vdots", "⋮"),
("ddots", "⋱"),
("ell", "ℓ"),
("hbar", "ℏ"),
("Im", "ℑ"),
("Re", "ℜ"),
("langle", "⟨"),
("rangle", "⟩"),
("vert", "|"),
("lvert", "|"),
("rvert", "|"),
("Vert", "‖"),
("lVert", "‖"),
("rVert", "‖"),
("lbrace", "{"),
("rbrace", "}"),
("backslash", "\\"),
("lfloor", "⌊"),
("rfloor", "⌋"),
("lceil", "⌈"),
("rceil", "⌉"),
("colon", ":"),
];
const NAMED_OPERATORS: &[&str] = &[
"arccos", "arcsin", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc", "deg", "det", "dim",
"exp", "gcd", "hom", "inf", "ker", "lg", "lim", "liminf", "limsup", "ln", "log", "max", "min",
"Pr", "sec", "sin", "sinh", "sup", "tan", "tanh",
];
const LIMIT_OPERATORS: &[&str] = &[
"argmax", "argmin", "inf", "injlim", "lim", "liminf", "limsup", "max", "min", "projlim", "sup",
];
const DISPLAY_LIMIT_SYMBOLS: &[&str] = &[
"bigcap",
"bigcup",
"bigodot",
"bigoplus",
"bigotimes",
"bigsqcup",
"biguplus",
"bigvee",
"bigwedge",
"coprod",
"int",
"iint",
"iiint",
"oint",
"prod",
"sum",
];
const RELATION_COMMANDS: &[&str] = &[
"Leftarrow",
"Leftrightarrow",
"Longleftarrow",
"Longleftrightarrow",
"Longrightarrow",
"Rightarrow",
"Vdash",
"Vvdash",
"approx",
"asymp",
"cong",
"dashv",
"doteq",
"downarrow",
"equiv",
"ge",
"geq",
"geqslant",
"gets",
"gg",
"hookleftarrow",
"hookrightarrow",
"iff",
"implies",
"in",
"leadsto",
"le",
"leftarrow",
"leftharpoondown",
"leftharpoonup",
"leftrightarrow",
"leftrightharpoons",
"leq",
"leqslant",
"ll",
"longleftarrow",
"longleftrightarrow",
"longmapsto",
"longrightarrow",
"mapsto",
"mid",
"models",
"ne",
"nearrow",
"neq",
"ni",
"notin",
"nvdash",
"nvDash",
"nwarrow",
"parallel",
"perp",
"prec",
"preceq",
"propto",
"rightharpoondown",
"rightharpoonup",
"rightleftharpoons",
"rightarrow",
"rightsquigarrow",
"searrow",
"sim",
"simeq",
"sqsubset",
"sqsubseteq",
"sqsupset",
"sqsupseteq",
"subset",
"subseteq",
"succ",
"succeq",
"supset",
"supseteq",
"swarrow",
"to",
"triangleleft",
"triangleright",
"twoheadleftarrow",
"twoheadrightarrow",
"uparrow",
"vdash",
];
const NEGATED_SYMBOLS: &[(char, &str)] = &[
('<', "≮"),
('>', "≯"),
('=', "≠"),
('∈', "∉"),
('∋', "∌"),
('∣', "∤"),
('∥', "∦"),
('∼', "≁"),
('≃', "≄"),
('≅', "≇"),
('≈', "≉"),
('≡', "≢"),
('≤', "≰"),
('≥', "≱"),
('≺', "⊀"),
('≻', "⊁"),
('⊂', "⊄"),
('⊃', "⊅"),
('⊆', "⊈"),
('⊇', "⊉"),
('⊢', "⊬"),
('⊨', "⊭"),
('↔', "↮"),
('←', "↚"),
('→', "↛"),
('⇒', "⇏"),
('⇐', "⇍"),
('⇔', "⇎"),
('≼', "⋠"),
('≽', "⋡"),
];
const BLACKBOARD: &[(char, &str)] = &[
('C', "ℂ"),
('H', "ℍ"),
('N', "ℕ"),
('P', "ℙ"),
('Q', "ℚ"),
('R', "ℝ"),
('Z', "ℤ"),
];
const SUPERSCRIPTS: &[(char, &str)] = &[
('0', "⁰"),
('1', "¹"),
('2', "²"),
('3', "³"),
('4', "⁴"),
('5', "⁵"),
('6', "⁶"),
('7', "⁷"),
('8', "⁸"),
('9', "⁹"),
('+', "⁺"),
('-', "⁻"),
('=', "⁼"),
('(', "⁽"),
(')', "⁾"),
('a', "ᵃ"),
('b', "ᵇ"),
('c', "ᶜ"),
('d', "ᵈ"),
('e', "ᵉ"),
('f', "ᶠ"),
('g', "ᵍ"),
('h', "ʰ"),
('i', "ⁱ"),
('j', "ʲ"),
('k', "ᵏ"),
('l', "ˡ"),
('m', "ᵐ"),
('n', "ⁿ"),
('o', "ᵒ"),
('p', "ᵖ"),
('r', "ʳ"),
('s', "ˢ"),
('t', "ᵗ"),
('u', "ᵘ"),
('v', "ᵛ"),
('w', "ʷ"),
('x', "ˣ"),
('y', "ʸ"),
('z', "ᶻ"),
];
const SUBSCRIPTS: &[(char, &str)] = &[
('0', "₀"),
('1', "₁"),
('2', "₂"),
('3', "₃"),
('4', "₄"),
('5', "₅"),
('6', "₆"),
('7', "₇"),
('8', "₈"),
('9', "₉"),
('+', "₊"),
('-', "₋"),
('=', "₌"),
('(', "₍"),
(')', "₎"),
('a', "ₐ"),
('e', "ₑ"),
('h', "ₕ"),
('i', "ᵢ"),
('j', "ⱼ"),
('k', "ₖ"),
('l', "ₗ"),
('m', "ₘ"),
('n', "ₙ"),
('o', "ₒ"),
('p', "ₚ"),
('r', "ᵣ"),
('s', "ₛ"),
('t', "ₜ"),
('u', "ᵤ"),
('v', "ᵥ"),
('x', "ₓ"),
];
const SPACING_COMMANDS: &[&str] = &[
",",
":",
";",
" ",
">",
"enspace",
"enskip",
"medspace",
"quad",
"qquad",
"thickspace",
"thinspace",
];
const NEGATIVE_SPACING_COMMANDS: &[&str] = &["!", "negmedspace", "negthickspace", "negthinspace"];
const IGNORED_COMMANDS: &[&str] = &[
"displaystyle",
"limits",
"nolimits",
"scriptstyle",
"scriptscriptstyle",
"textstyle",
];
const FONT_SWITCH_COMMANDS: &[&str] = &["bf", "cal", "it", "rm", "sf", "sl", "tt"];
const SIZE_COMMANDS: &[&str] = &[
"big", "Big", "bigg", "Bigg", "bigl", "Bigl", "biggl", "Biggl", "bigr", "Bigr", "biggr",
"Biggr",
];
const PLAIN_WRAPPERS: &[&str] = &[
"emph",
"mathcal",
"mathbf",
"mathfrak",
"mathit",
"mathrm",
"mathnormal",
"mathscr",
"mathsf",
"mathtt",
"mathup",
"mbox",
"overbrace",
"pmb",
"smash",
"substack",
"text",
"textbf",
"textit",
"textmd",
"textnormal",
"textrm",
"textsc",
"textsf",
"textsl",
"texttt",
"textup",
"underbrace",
"bm",
"boldsymbol",
];
const ACCENTS: &[(&str, &str)] = &[
("acute", "\u{0301}"),
("bar", "\u{0305}"),
("breve", "\u{0306}"),
("check", "\u{030c}"),
("ddot", "\u{0308}"),
("dot", "\u{0307}"),
("grave", "\u{0300}"),
("hat", "\u{0302}"),
("mathring", "\u{030a}"),
("overleftarrow", "\u{20d6}"),
("overleftrightarrow", "\u{20e1}"),
("overline", "\u{0305}"),
("overrightarrow", "\u{20d7}"),
("tilde", "\u{0303}"),
("underline", "\u{0332}"),
("vec", "\u{20d7}"),
("widehat", "\u{0302}"),
("widetilde", "\u{0303}"),
];
fn in_set(set: &[&str], s: &str) -> bool {
set.contains(&s)
}
fn find<'a>(table: &'a [(&'a str, &'a str)], key: &str) -> Option<&'a str> {
table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
}
fn find_char<'a>(table: &'a [(char, &'a str)], key: char) -> Option<&'a str> {
table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
}
fn strip_sign_spaces(s: &str) -> String {
let mut out = String::new();
let mut segment = String::new();
for c in s.chars() {
if c == '=' || c == '+' || c == '-' {
out.push_str(segment.trim());
out.push(c);
segment.clear();
} else {
segment.push(c);
}
}
out.push_str(segment.trim());
out
}
fn replace_characters(value: &str, replacements: &[(char, &str)]) -> Option<String> {
let mut result = String::new();
for c in value.chars() {
let r = find_char(replacements, c)?;
result.push_str(r);
}
Some(result)
}
fn format_script(value: &str, kind: &str) -> String {
let value = normalize_script_value(value);
if let Some(unicode) = format_unicode_script(&value, kind) {
return unicode;
}
let prefix = if kind == "sub" { "_" } else { "^" };
let char_count = value.chars().count();
if char_count == 1
|| (kind == "sub" && !value.is_empty() && value.chars().all(|c| c.is_ascii_alphabetic()))
{
return format!("{}{}", prefix, value);
}
format!("{}({})", prefix, value)
}
fn normalize_script_value(value: &str) -> String {
strip_sign_spaces(value.trim())
}
fn format_unicode_script(value: &str, kind: &str) -> Option<String> {
let replacements = if kind == "sub" {
SUBSCRIPTS
} else {
SUPERSCRIPTS
};
replace_characters(&normalize_script_value(value), replacements)
}
fn simple_text(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_alphabetic() || c.is_numeric() || c == '.')
}
fn format_fraction(numerator: &str, denominator: &str) -> String {
let numerator = numerator.trim();
let denominator = denominator.trim();
let simple_numerator = simple_text(numerator);
let simple_denominator = simple_text(denominator)
&& denominator.chars().all(|c| c.is_numeric() || c == '.')
|| denominator.chars().count() == 1;
let num_part = if simple_numerator {
numerator.to_string()
} else {
format!("({})", numerator)
};
let den_part = if simple_denominator {
denominator.to_string()
} else {
format!("({})", denominator)
};
format!("{}/{}", num_part, den_part)
}
fn format_root(value: &str, symbol: &str) -> String {
let value = value.trim();
if simple_text(value) {
format!("{}{}", symbol, value)
} else {
format!("{}({})", symbol, value)
}
}
fn find_substring(chars: &[char], needle: &str) -> Option<usize> {
let needle_chars: Vec<char> = needle.chars().collect();
if needle_chars.is_empty() || chars.len() < needle_chars.len() {
return None;
}
'outer: for i in 0..=chars.len() - needle_chars.len() {
for (j, &nc) in needle_chars.iter().enumerate() {
if chars[i + j] != nc {
continue 'outer;
}
}
return Some(i);
}
None
}
fn split_environment_rows(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let chars: Vec<char> = body.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' && chars.get(i + 1) == Some(&'\\') {
let mut j = i + 2;
if chars.get(j) == Some(&'[') {
let mut k = j + 1;
while k < chars.len() && chars[k] != ']' && chars[k] != '\n' {
k += 1;
}
if k < chars.len() && chars[k] == ']' {
j = k + 1;
}
}
out.push(std::mem::take(&mut cur));
i = j;
continue;
}
cur.push(chars[i]);
i += 1;
}
out.push(cur);
out
}
fn trim_leading_group(s: &str) -> String {
let t = s.trim_start();
if t.starts_with('{')
&& let Some(pos) = t.find('}')
{
return t[pos + 1..].to_string();
}
s.to_string()
}
fn collapse_spaces(s: &str) -> String {
let mut out = String::new();
let mut pending_space = false;
for c in s.chars() {
if c == ' ' || c == '\t' {
pending_space = true;
} else {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push(c);
}
}
out
}
fn is_op_left_space(c: char) -> bool {
c.is_alphabetic() || c.is_numeric() || c == ')' || c == '}' || c == ']' || c == LAYOUT_END
}
fn is_op_right_space(c: char) -> bool {
c.is_alphabetic() || c.is_numeric() || c == '√' || c == LAYOUT_START
}
fn normalize_output(value: &str) -> String {
let chars: Vec<char> = value.chars().collect();
let mut out = String::new();
for (i, &c) in chars.iter().enumerate() {
if c == NAMED_OP_START {
let prev = if i > 0 { chars[i - 1] } else { '\0' };
if is_op_left_space(prev) {
out.push(' ');
}
} else if c == NAMED_OP_END {
let next = chars.get(i + 1).copied().unwrap_or('\0');
if is_op_right_space(next) {
out.push(' ');
}
} else {
out.push(c);
}
}
let lines: Vec<&str> = out.split('\n').collect();
let mut kept: Vec<String> = Vec::new();
for (index, line) in lines.iter().enumerate() {
let collapsed = collapse_spaces(line).trim().to_string();
if !collapsed.is_empty() || (index > 0 && index < lines.len() - 1) {
kept.push(collapsed);
}
}
kept.join("\n").trim().to_string()
}
enum LayoutNode {
Fraction {
numerator: String,
denominator: String,
},
Operator {
operator: String,
lower: Option<String>,
upper: Option<String>,
},
Matrix {
lines: Vec<String>,
baseline: usize,
},
Script {
lower: Option<String>,
upper: Option<String>,
},
}
type NodeRef = Rc<RefCell<Vec<LayoutNode>>>;
struct Layout {
lines: Vec<String>,
width: usize,
baseline: usize,
}
fn pad_layout_line(line: &str, width: usize, centered: bool) -> String {
let padding = width.saturating_sub(display_width(line));
let left = if centered { padding / 2 } else { 0 };
format!("{}{}{}", " ".repeat(left), line, " ".repeat(padding - left))
}
fn join_layouts(layouts: &[Layout]) -> Layout {
if layouts.is_empty() {
return Layout {
lines: vec![String::new()],
width: 0,
baseline: 0,
};
}
let baseline = layouts.iter().map(|l| l.baseline).max().unwrap_or(0);
let below = layouts
.iter()
.map(|l| l.lines.len().saturating_sub(l.baseline + 1))
.max()
.unwrap_or(0);
let mut lines: Vec<String> = Vec::new();
for row in 0..=baseline + below {
let mut line = String::new();
for layout in layouts {
let source_row = row as isize - baseline as isize + layout.baseline as isize;
if source_row >= 0 && (source_row as usize) < layout.lines.len() {
line.push_str(&pad_layout_line(
&layout.lines[source_row as usize],
layout.width,
false,
));
} else {
line.push_str(&" ".repeat(layout.width));
}
}
lines.push(line.trim_end().to_string());
}
Layout {
lines,
width: layouts.iter().map(|l| l.width).sum(),
baseline,
}
}
fn render_layout(source: &str, nodes: &[LayoutNode]) -> Layout {
let mut rendered_lines: Vec<String> = Vec::new();
let mut first_baseline = 0usize;
for source_line in source.split('\n') {
let chars: Vec<char> = source_line.chars().collect();
let mut layouts: Vec<Layout> = Vec::new();
let mut position = 0usize;
let mut previous_node: Option<&LayoutNode> = None;
let mut i = 0;
while i < chars.len() {
if chars[i] != LAYOUT_START {
i += 1;
continue;
}
let mut j = i + 1;
let mut num: usize = 0;
let mut digits = false;
while j < chars.len() && chars[j].is_ascii_digit() {
num = num * 10 + (chars[j] as u32 - '0' as u32) as usize;
digits = true;
j += 1;
}
if !digits || j >= chars.len() || chars[j] != LAYOUT_END {
i += 1;
continue;
}
let Some(node) = nodes.get(num) else {
i = j + 1;
continue;
};
if i > position {
layouts.push(leading_text_layout(
&chars,
position,
i,
previous_node,
node,
));
}
layouts.push(render_layout_node(node, nodes));
position = j + 1;
previous_node = Some(node);
i = j + 1;
}
if position < chars.len() {
layouts.push(trailing_text_layout(&chars, position, previous_node));
}
let line_layout = join_layouts(&layouts);
if rendered_lines.is_empty() {
first_baseline = line_layout.baseline;
}
rendered_lines.extend(line_layout.lines);
}
let width = rendered_lines
.iter()
.map(|l| display_width(l))
.max()
.unwrap_or(0);
Layout {
lines: rendered_lines,
width,
baseline: first_baseline,
}
}
fn render_layout_node(node: &LayoutNode, nodes: &[LayoutNode]) -> Layout {
match node {
LayoutNode::Fraction {
numerator,
denominator,
} => {
let numerator_layout = render_layout(numerator, nodes);
let denominator_layout = render_layout(denominator, nodes);
let content_width = numerator_layout.width.max(denominator_layout.width).max(1);
let width = content_width + 2;
let mut lines: Vec<String> = numerator_layout
.lines
.iter()
.map(|l| pad_layout_line(l, width, true))
.collect();
lines.push(format!(" {} ", "─".repeat(content_width)));
lines.extend(
denominator_layout
.lines
.iter()
.map(|l| pad_layout_line(l, width, true)),
);
Layout {
lines,
width,
baseline: numerator_layout.lines.len(),
}
}
LayoutNode::Operator {
operator,
lower,
upper,
} => {
let content_width = display_width(operator)
.max(lower.as_ref().map(|s| display_width(s)).unwrap_or(0))
.max(upper.as_ref().map(|s| display_width(s)).unwrap_or(0));
let mut lines: Vec<String> = Vec::new();
if let Some(upper) = upper {
lines.push(format!("{} ", pad_layout_line(upper, content_width, true)));
}
lines.push(format!(
"{} ",
pad_layout_line(operator, content_width, true)
));
if let Some(lower) = lower {
lines.push(format!("{} ", pad_layout_line(lower, content_width, true)));
}
Layout {
lines,
width: content_width + 1,
baseline: if upper.is_some() { 1 } else { 0 },
}
}
LayoutNode::Matrix { lines, baseline } => {
let width = lines.iter().map(|l| display_width(l)).max().unwrap_or(0);
Layout {
lines: lines
.iter()
.map(|l| pad_layout_line(l, width, false))
.collect(),
width,
baseline: *baseline,
}
}
LayoutNode::Script { lower, upper } => {
let upper_layout = upper.as_ref().map(|u| render_layout(u, nodes));
let lower_layout = lower.as_ref().map(|l| render_layout(l, nodes));
let width = upper_layout
.as_ref()
.map(|l| l.width)
.unwrap_or(0)
.max(lower_layout.as_ref().map(|l| l.width).unwrap_or(0));
let mut lines: Vec<String> = Vec::new();
if let Some(l) = &upper_layout {
lines.extend(
l.lines
.iter()
.map(|line| pad_layout_line(line, width, false)),
);
}
lines.push(" ".repeat(width));
if let Some(l) = &lower_layout {
lines.extend(
l.lines
.iter()
.map(|line| pad_layout_line(line, width, false)),
);
}
Layout {
lines,
width,
baseline: upper_layout.as_ref().map(|l| l.lines.len()).unwrap_or(0),
}
}
}
}
fn leading_text_layout(
chars: &[char],
position: usize,
i: usize,
previous_node: Option<&LayoutNode>,
node: &LayoutNode,
) -> Layout {
let sliced: String = chars[position..i].iter().collect();
let trimmed = if previous_node.is_some() {
sliced.trim_start()
} else {
sliced.as_str()
}
.trim_end();
let preserve_leading = matches!(previous_node, Some(LayoutNode::Matrix { .. }))
&& sliced.chars().next().is_some_and(|c| c.is_whitespace());
let preserve_trailing = matches!(node, LayoutNode::Matrix { .. })
&& sliced
.chars()
.next_back()
.is_some_and(|c| c.is_whitespace());
let text = if !trimmed.is_empty() {
format!(
"{}{}{}",
if preserve_leading { " " } else { "" },
trimmed,
if preserve_trailing { " " } else { "" },
)
} else if preserve_leading || preserve_trailing {
" ".to_string()
} else {
String::new()
};
let text_w = display_width(&text);
Layout {
lines: vec![text],
width: text_w,
baseline: 0,
}
}
fn trailing_text_layout(
chars: &[char],
position: usize,
previous_node: Option<&LayoutNode>,
) -> Layout {
let sliced: String = chars[position..].iter().collect();
let trimmed = if previous_node.is_some() {
sliced.trim_start()
} else {
sliced.as_str()
};
let text = if matches!(previous_node, Some(LayoutNode::Matrix { .. }))
&& sliced.chars().next().is_some_and(|c| c.is_whitespace())
{
format!(" {}", trimmed)
} else {
trimmed.to_string()
};
let text_w = display_width(&text);
Layout {
lines: vec![text],
width: text_w,
baseline: 0,
}
}
fn trailing_layout_marker_index(s: &str) -> Option<usize> {
let chars: Vec<char> = s.chars().collect();
if chars.last() != Some(&LAYOUT_END) {
return None;
}
let mut j = chars.len() - 1;
while j > 0 && chars[j - 1].is_ascii_digit() {
j -= 1;
}
if j == 0 || chars[j - 1] != LAYOUT_START || j == chars.len() - 1 {
return None;
}
chars[j..chars.len() - 1]
.iter()
.collect::<String>()
.parse()
.ok()
}
struct LatexParser {
chars: Vec<char>,
position: usize,
nodes: NodeRef,
display: bool,
supported: bool,
stack_fractions: bool,
script_depth: usize,
}
impl LatexParser {
fn new(source: &str, nodes: NodeRef, display: bool) -> Self {
Self {
chars: source.chars().collect(),
position: 0,
nodes,
display,
supported: true,
stack_fractions: true,
script_depth: 0,
}
}
fn render(mut self) -> Option<String> {
let rendered = self.parse_sequence(None);
if !self.supported || self.position != self.chars.len() {
return None;
}
Some(normalize_output(&rendered))
}
fn parse_sequence(&mut self, end: Option<char>) -> String {
let mut result = String::new();
while self.position < self.chars.len() {
let character = self.chars[self.position];
if let Some(e) = end
&& character == e
{
self.position += 1;
return result;
}
if character == '}' {
self.supported = false;
return result;
}
if character == '{' {
self.position += 1;
result.push_str(&self.parse_sequence(Some('}')));
continue;
}
if character == '\\' {
let command = self.parse_command();
if command == "\u{0}" {
result = result.trim_end().to_string();
if result.ends_with(NAMED_OP_END) {
result.truncate(result.len() - NAMED_OP_END.len_utf8());
}
} else {
result.push_str(&command);
}
continue;
}
if character == '^' || character == '_' {
self.position += 1;
result = result.trim_end().to_string();
let script = self.parse_scripts(character);
if result.ends_with(NAMED_OP_END) {
result.truncate(result.len() - NAMED_OP_END.len_utf8());
result.push_str(&script);
result.push(NAMED_OP_END);
} else {
result.push_str(&script);
}
continue;
}
if character.is_whitespace() {
result.push_str(&self.parse_whitespace());
continue;
}
if character == '=' || character == '<' || character == '>' {
result = format!("{} {} ", result.trim_end(), character);
self.position += 1;
continue;
}
if character == '&' {
self.position += 1;
continue;
}
if character == '~' {
self.position += 1;
result.push(' ');
continue;
}
if character == '.'
&& let Some(index) = trailing_layout_marker_index(&result)
&& let Some(node) = self.nodes.borrow_mut().get_mut(index)
&& let LayoutNode::Matrix { lines, .. } = node
&& let Some(last) = lines.last_mut()
{
last.push('.');
self.position += 1;
continue;
}
result.push(character);
self.position += 1;
}
if end.is_some() {
self.supported = false;
}
result
}
fn parse_scripts(&mut self, initial_marker: char) -> String {
let mut sub: Option<String> = None;
let mut sup: Option<String> = None;
let mut order: Vec<char> = Vec::new();
self.parse_script_argument(initial_marker, &mut sub, &mut sup, &mut order);
let mut next_position = self.position;
while next_position < self.chars.len() && self.chars[next_position].is_whitespace() {
next_position += 1;
}
if let Some(&next_marker) = self.chars.get(next_position)
&& (next_marker == '^' || next_marker == '_')
&& next_marker != initial_marker
{
self.position = next_position + 1;
self.parse_script_argument(next_marker, &mut sub, &mut sup, &mut order);
}
let sub_unicode = sub.as_deref().and_then(|v| format_unicode_script(v, "sub"));
let sup_unicode = sup.as_deref().and_then(|v| format_unicode_script(v, "sup"));
let blocks_layout = |value: &Option<String>| -> bool {
match value {
None => false,
Some(v) => {
v.contains('/')
|| (!v.contains(LAYOUT_START)
&& v.chars().count() > 1
&& !v
.chars()
.any(|c| c.is_ascii_uppercase() || c == '*' || c == '∗'))
}
}
};
let can_use_layout = !(blocks_layout(&sub) || blocks_layout(&sup));
let needs_layout = self.display
&& can_use_layout
&& (self.script_depth > 0
|| (sub.is_some() && sub_unicode.is_none())
|| (sup.is_some() && sup_unicode.is_none()));
if !needs_layout {
return order
.iter()
.map(|kind| {
if *kind == '_' {
sub_unicode
.clone()
.unwrap_or_else(|| format_script(sub.as_deref().unwrap_or(""), "sub"))
} else {
sup_unicode
.clone()
.unwrap_or_else(|| format_script(sup.as_deref().unwrap_or(""), "sup"))
}
})
.collect();
}
let mut nodes = self.nodes.borrow_mut();
let index = nodes.len();
nodes.push(LayoutNode::Script {
lower: sub.as_deref().map(normalize_output),
upper: sup.as_deref().map(normalize_output),
});
format!("{}{}{}", LAYOUT_START, index, LAYOUT_END)
}
fn parse_script_argument(
&mut self,
marker: char,
sub: &mut Option<String>,
sup: &mut Option<String>,
order: &mut Vec<char>,
) {
self.script_depth += 1;
let value = self.parse_required_argument(false);
self.script_depth -= 1;
if marker == '_' {
*sub = Some(value);
} else {
*sup = Some(value);
}
order.push(marker);
}
fn parse_whitespace(&mut self) -> String {
while self.position < self.chars.len() && self.chars[self.position].is_whitespace() {
self.position += 1;
}
" ".to_string()
}
fn parse_command(&mut self) -> String {
self.position += 1;
if self.position >= self.chars.len() {
self.supported = false;
return String::new();
}
let first = self.chars[self.position];
if first == '\n' || first == '\r' {
self.position += 1;
if first == '\r'
&& self.position < self.chars.len()
&& self.chars[self.position] == '\n'
{
self.position += 1;
}
return " ".to_string();
}
let command: String;
if first.is_ascii_alphabetic() {
let start = self.position;
while self.position < self.chars.len()
&& self.chars[self.position].is_ascii_alphabetic()
{
self.position += 1;
}
command = self.chars[start..self.position].iter().collect();
} else {
command = first.to_string();
self.position += 1;
}
if let Some(value) = self.parse_command_constant(&command) {
return value;
}
if command == "not" {
return self.parse_command_not();
}
if let Some(value) = self.parse_command_symbol(&command) {
return value;
}
if command == "left" || command == "middle" || command == "right" {
return self.parse_command_delimiter();
}
if command == "frac" || command == "dfrac" || command == "tfrac" {
return self.parse_command_fraction(&command);
}
if command == "sqrt" {
return self.parse_command_sqrt();
}
if command == "boxed" || command == "fbox" {
return format!("[{}]", self.parse_required_argument(true).trim());
}
if command == "binom" || command == "dbinom" || command == "tbinom" {
return self.parse_command_binom();
}
if let Some(accent) = find(ACCENTS, &command) {
return self.parse_command_accent(&command, accent);
}
if command == "mathbb" {
return self.parse_command_mathbb();
}
if command == "operatorname" {
return self.parse_command_operatorname();
}
if command == "mod" || command == "bmod" {
return " mod ".to_string();
}
if command == "pmod" || command == "pod" {
return self.parse_command_pmod(&command);
}
if command == "overset" || command == "stackrel" || command == "underset" {
return self.parse_command_stack(&command);
}
if in_set(PLAIN_WRAPPERS, &command) {
let value = self.parse_required_argument(true);
return if command.starts_with("text") || command == "mbox" {
value
} else {
value.trim().to_string()
};
}
if command == "begin" || command == "end" {
return self.parse_command_begin_end(&command);
}
self.supported = false;
format!("\\{}", command)
}
fn parse_command_constant(&mut self, command: &str) -> Option<String> {
if command == "\\" {
return Some("\n".to_string());
}
if in_set(SPACING_COMMANDS, command) {
return Some(" ".to_string());
}
if in_set(NEGATIVE_SPACING_COMMANDS, command) {
return Some(NEGATIVE_SPACE.to_string());
}
if in_set(IGNORED_COMMANDS, command) {
return Some(String::new());
}
if in_set(FONT_SWITCH_COMMANDS, command) {
while self.position < self.chars.len() && self.chars[self.position].is_whitespace() {
self.position += 1;
}
return Some(String::new());
}
if command == "{"
|| command == "}"
|| command == "$"
|| command == "%"
|| command == "#"
|| command == "_"
|| command == "&"
{
return Some(command.to_string());
}
if command == "|" {
return Some("‖".to_string());
}
None
}
fn parse_command_symbol(&mut self, command: &str) -> Option<String> {
if in_set(LIMIT_OPERATORS, command) {
return Some(self.parse_operator(command, "bracket", true, true));
}
if let Some(symbol) = find(SYMBOLS, command) {
if in_set(DISPLAY_LIMIT_SYMBOLS, command) {
return Some(self.parse_operator(symbol, "script", true, false));
}
return Some(
if command == "cdot" || command == "times" || in_set(RELATION_COMMANDS, command) {
format!(" {} ", symbol)
} else {
symbol.to_string()
},
);
}
if in_set(NAMED_OPERATORS, command) {
return Some(format!("{}{}{}", NAMED_OP_START, command, NAMED_OP_END));
}
if in_set(SIZE_COMMANDS, command) {
return Some(String::new());
}
None
}
fn parse_command_delimiter(&mut self) -> String {
if self.position < self.chars.len() && self.chars[self.position] == '.' {
self.position += 1;
}
String::new()
}
fn parse_command_begin_end(&mut self, command: &str) -> String {
if command == "begin" {
self.parse_environment()
} else {
self.supported = false;
String::new()
}
}
fn parse_command_not(&mut self) -> String {
let value = self.parse_required_argument(false).trim().to_string();
let negated = value
.chars()
.next()
.filter(|_| value.chars().count() == 1)
.and_then(|c| find_char(NEGATED_SYMBOLS, c));
if let Some(negated) = negated {
return format!(" {} ", negated);
}
let chars: Vec<char> = value.chars().collect();
if chars.is_empty() {
self.supported = false;
return String::new();
}
let mut out = String::new();
out.push(' ');
out.push(chars[0]);
out.push('\u{0338}');
for c in &chars[1..] {
out.push(*c);
}
out.push(' ');
out
}
fn parse_command_fraction(&mut self, command: &str) -> String {
let should_stack = self.display && self.stack_fractions && command != "tfrac";
let numerator = self.parse_required_argument(!should_stack);
let denominator = self.parse_required_argument(!should_stack);
if should_stack {
let mut nodes = self.nodes.borrow_mut();
let index = nodes.len();
nodes.push(LayoutNode::Fraction {
numerator: normalize_output(&numerator),
denominator: normalize_output(&denominator),
});
return format!("{}{}{}", LAYOUT_START, index, LAYOUT_END);
}
format_fraction(&numerator, &denominator)
}
fn parse_command_sqrt(&mut self) -> String {
let degree = self.parse_optional_argument().map(|s| s.trim().to_string());
let value = self.parse_required_argument(true);
match degree.as_deref() {
None | Some("2") => format_root(&value, "√"),
Some("3") => format_root(&value, "∛"),
Some("4") => format_root(&value, "∜"),
Some(d) => format!("{}{}", format_script(d, "sup"), format_root(&value, "√")),
}
}
fn parse_command_binom(&mut self) -> String {
format!(
"({} choose {})",
self.parse_required_argument(true),
self.parse_required_argument(true),
)
}
fn parse_command_accent(&mut self, command: &str, accent: &str) -> String {
let value = self.parse_required_argument(true);
if value.chars().count() == 1 {
format!("{}{}", value, accent)
} else {
format!("{}({})", command, value)
}
}
fn parse_command_mathbb(&mut self) -> String {
let value = self.parse_required_argument(true);
let mut out = String::new();
for c in value.chars() {
match find_char(BLACKBOARD, c) {
Some(bb) => out.push_str(bb),
None => out.push(c),
}
}
out
}
fn parse_command_operatorname(&mut self) -> String {
let mut starred = false;
if self.position < self.chars.len() && self.chars[self.position] == '*' {
starred = true;
self.position += 1;
}
let operator = normalize_output(&self.parse_required_argument(true))
.trim()
.to_string();
self.parse_operator(&operator, "bracket", starred, true)
}
fn parse_command_pmod(&mut self, command: &str) -> String {
let value = self.parse_required_argument(true).trim().to_string();
if command == "pmod" {
format!(" (mod {})", value)
} else {
format!(" ({})", value)
}
}
fn parse_command_stack(&mut self, command: &str) -> String {
if command == "underset" {
let lower = self.parse_required_argument(true);
let value = self.parse_required_argument(true).trim().to_string();
return format!("{}{}", value, format_script(&lower, "sub"));
}
let upper = self.parse_required_argument(true);
let value = self.parse_required_argument(true).trim().to_string();
format!("{}{}", value, format_script(&upper, "sup"))
}
fn parse_operator(
&mut self,
operator: &str,
inline_lower_style: &str,
display_limits: bool,
spaced: bool,
) -> String {
let mut use_display_limits = display_limits;
let mut modifier_position = self.position;
while modifier_position < self.chars.len()
&& (self.chars[modifier_position] == ' ' || self.chars[modifier_position] == '\t')
{
modifier_position += 1;
}
let modifier: Option<(&str, usize)> = {
let rest: String = self.chars[modifier_position..].iter().collect();
let bytes = rest.as_bytes();
if rest.starts_with("\\limits") && (bytes.len() == 7 || !bytes[7].is_ascii_alphabetic())
{
Some(("limits", 7))
} else if rest.starts_with("\\nolimits")
&& (bytes.len() == 9 || !bytes[9].is_ascii_alphabetic())
{
Some(("nolimits", 9))
} else {
None
}
};
if let Some((m, len)) = modifier {
use_display_limits = m == "limits";
self.position = modifier_position + len;
}
let mut lower: Option<String> = None;
let mut upper: Option<String> = None;
loop {
let mut script_position = self.position;
while script_position < self.chars.len()
&& (self.chars[script_position] == ' ' || self.chars[script_position] == '\t')
{
script_position += 1;
}
let kind = self.chars.get(script_position).copied();
if kind != Some('_') && kind != Some('^') {
break;
}
self.position = script_position + 1;
let value = normalize_output(&self.parse_required_argument(false)).replace(' ', "");
if kind == Some('_') {
if lower.is_some() {
self.supported = false;
}
lower = Some(value);
} else {
if upper.is_some() {
self.supported = false;
}
upper = Some(value);
}
}
if self.display && use_display_limits && (lower.is_some() || upper.is_some()) {
let mut nodes = self.nodes.borrow_mut();
let index = nodes.len();
nodes.push(LayoutNode::Operator {
operator: operator.to_string(),
lower,
upper,
});
return format!("{}{}{}", LAYOUT_START, index, LAYOUT_END);
}
let mut rendered = operator.to_string();
if let Some(l) = &lower {
let part = if inline_lower_style == "bracket" {
format!("[{}]", l)
} else {
format_script(l, "sub")
};
rendered.push_str(&part);
}
if let Some(u) = &upper {
rendered.push_str(&format_script(u, "sup"));
}
if spaced {
format!(" {} ", rendered)
} else {
rendered
}
}
fn parse_required_argument(&mut self, stack_fractions: bool) -> String {
let previous = self.stack_fractions;
self.stack_fractions = previous && stack_fractions;
let value = self.parse_required_argument_value();
self.stack_fractions = previous;
value
}
fn parse_required_argument_value(&mut self) -> String {
while self.position < self.chars.len() && self.chars[self.position].is_whitespace() {
self.position += 1;
}
if self.position >= self.chars.len() {
self.supported = false;
return String::new();
}
if self.chars[self.position] == '{' {
self.position += 1;
return self.parse_sequence(Some('}'));
}
if self.chars[self.position] == '\\' {
return self.parse_command();
}
let value = self.chars[self.position].to_string();
self.position += 1;
value
}
fn parse_optional_argument(&mut self) -> Option<String> {
while self.position < self.chars.len()
&& (self.chars[self.position] == ' ' || self.chars[self.position] == '\t')
{
self.position += 1;
}
if self.chars.get(self.position) != Some(&'[') {
return None;
}
let mut end = None;
for (i, &c) in self.chars.iter().enumerate().skip(self.position + 1) {
if c == ']' {
end = Some(i);
break;
}
}
let end = end?;
let value: String = self.chars[self.position + 1..end].iter().collect();
self.position = end + 1;
Some(self.render_nested(&value, true))
}
fn read_raw_group(&mut self) -> Option<String> {
while self.position < self.chars.len() && self.chars[self.position].is_whitespace() {
self.position += 1;
}
if self.chars.get(self.position) != Some(&'{') {
self.supported = false;
return None;
}
self.position += 1;
let start = self.position;
let mut depth = 1usize;
while self.position < self.chars.len() {
let c = self.chars[self.position];
if c == '\\' {
self.position += 2;
continue;
}
if c == '{' {
depth += 1;
}
if c == '}' {
depth -= 1;
if depth == 0 {
let value: String = self.chars[start..self.position].iter().collect();
self.position += 1;
return Some(value);
}
}
self.position += 1;
}
self.supported = false;
None
}
fn parse_environment(&mut self) -> String {
let Some(environment) = self.read_raw_group() else {
return String::new();
};
let end_marker = format!("\\end{{{}}}", environment);
let Some(end) = find_substring(&self.chars[self.position..], &end_marker) else {
self.supported = false;
return String::new();
};
let body: String = self.chars[self.position..self.position + end]
.iter()
.collect();
self.position += end + end_marker.chars().count();
if environment == "equation" || environment == "equation*" || environment == "displaymath" {
return self.render_nested(&body, true).trim().to_string();
}
if matches!(
environment.as_str(),
"aligned"
| "align"
| "align*"
| "alignedat"
| "alignat"
| "alignat*"
| "gather"
| "gathered"
| "multline"
| "multline*"
| "split"
) {
return self.render_aligned_environment(&body, &environment);
}
if environment == "cases" || environment == "cases*" {
return self.render_cases_environment(&body);
}
if [
"array",
"matrix",
"smallmatrix",
"pmatrix",
"bmatrix",
"Bmatrix",
"vmatrix",
"Vmatrix",
]
.contains(&environment.as_str())
{
let matrix_body = if environment == "array" {
trim_leading_group(&body)
} else {
body
};
return self.render_matrix(&environment, &matrix_body);
}
self.supported = false;
body
}
fn render_aligned_environment(&mut self, body: &str, environment: &str) -> String {
let aligned_at = ["alignedat", "alignat", "alignat*"].contains(&environment);
let aligned_body = if aligned_at {
trim_leading_group(body)
} else {
body.to_string()
};
let mut parts: Vec<String> = Vec::new();
for row in split_environment_rows(&aligned_body) {
let cells: Vec<&str> = row.split('&').collect();
let source = if aligned_at {
let mut merged = String::new();
let pairs = cells.len().div_ceil(2);
for i in 0..pairs {
let from = i * 2;
let to = (i * 2 + 2).min(cells.len());
let joined = cells[from..to].join("");
if !merged.is_empty() {
merged.push(' ');
}
merged.push_str(&joined);
}
merged
} else {
cells.join("")
};
let rendered = self.render_nested(&source, true).trim().to_string();
if !rendered.is_empty() {
parts.push(rendered);
}
}
parts.join("\n")
}
fn render_cases_environment(&mut self, body: &str) -> String {
let rows: Vec<Vec<String>> = split_environment_rows(body)
.iter()
.map(|row| {
row.split('&')
.map(|cell| self.render_nested(cell, false).trim().to_string())
.collect()
})
.filter(|row: &Vec<String>| row.iter().any(|c| !c.is_empty()))
.collect();
let value_width = rows
.iter()
.map(|row| display_width(row.first().map(|s| s.trim_end_matches(',')).unwrap_or("")))
.max()
.unwrap_or(0);
let contents: Vec<String> = rows
.iter()
.map(|row| {
let value = row.first().map(|s| s.trim_end_matches(',')).unwrap_or("");
let condition = row.get(1).map(|s| s.as_str()).unwrap_or("");
if condition.is_empty() {
return value.to_string();
}
let prefix = if starts_with_case_word(condition) {
" "
} else {
" if "
};
let pad = value_width.saturating_sub(display_width(value));
format!(
"{}{}{}{}",
value,
PROTECTED_SPACE.to_string().repeat(pad),
prefix,
condition
)
})
.collect();
if contents.len() <= 1 {
return contents
.first()
.map(|c| format!("⎧ {}", c))
.unwrap_or_default();
}
let middle = contents.len() / 2;
let mut visual: Vec<Option<String>> = contents.into_iter().map(Some).collect();
if visual.len().is_multiple_of(2) {
visual.insert(middle, None);
}
let lines: Vec<String> = visual
.iter()
.enumerate()
.map(|(index, content)| {
let delimiter = if index == 0 {
"⎧"
} else if index == visual.len() - 1 {
"⎩"
} else {
"⎨"
};
match content {
None => delimiter.to_string(),
Some(c) => format!("{} {}", delimiter, c),
}
})
.collect();
let mut nodes = self.nodes.borrow_mut();
let index = nodes.len();
nodes.push(LayoutNode::Matrix {
lines,
baseline: middle,
});
format!("{}{}{}", LAYOUT_START, index, LAYOUT_END)
}
fn render_matrix(&mut self, environment: &str, body: &str) -> String {
let matrix: Vec<Vec<String>> = split_environment_rows(body)
.iter()
.map(|row| {
row.split('&')
.map(|cell| self.render_nested(cell, false).trim().to_string())
.collect()
})
.filter(|row: &Vec<String>| row.iter().any(|c| !c.is_empty()))
.collect();
let column_count = matrix.iter().map(|r| r.len()).max().unwrap_or(0);
let mut column_widths = vec![0usize; column_count];
for row in &matrix {
for (i, cell) in row.iter().enumerate() {
if i < column_count {
column_widths[i] = column_widths[i].max(display_width(cell));
}
}
}
let rows: Vec<String> = matrix
.iter()
.map(|row| {
let mut out = String::new();
for (i, &w) in column_widths.iter().enumerate() {
let cell = row.get(i).cloned().unwrap_or_default();
out.push_str(&cell);
let pad = w.saturating_sub(display_width(&cell));
for _ in 0..pad {
out.push(PROTECTED_SPACE);
}
if i < column_count - 1 {
out.push_str(" │ ");
}
}
out
})
.collect();
let lines: Vec<String> =
if environment == "array" || environment == "matrix" || environment == "smallmatrix" {
rows
} else {
let delimiters: &[(&str, [char; 6])] = &[
("pmatrix", ['⎛', '⎞', '⎜', '⎟', '⎝', '⎠']),
("bmatrix", ['⎡', '⎤', '⎢', '⎥', '⎣', '⎦']),
("Bmatrix", ['⎧', '⎫', '⎨', '⎬', '⎩', '⎭']),
("vmatrix", ['│', '│', '│', '│', '│', '│']),
("Vmatrix", ['║', '║', '║', '║', '║', '║']),
];
let Some((_, dl)) = delimiters.iter().find(|(n, _)| *n == environment) else {
self.supported = false;
return rows.join("\n");
};
rows.iter()
.enumerate()
.map(|(index, row)| {
let left = if index == 0 {
dl[0]
} else if index == rows.len() - 1 {
dl[4]
} else {
dl[2]
};
let right = if index == 0 {
dl[1]
} else if index == rows.len() - 1 {
dl[5]
} else {
dl[3]
};
format!("{} {} {}", left, row, right)
})
.collect()
};
if lines.len() <= 1 {
return lines.first().cloned().unwrap_or_default();
}
let mut nodes = self.nodes.borrow_mut();
let index = nodes.len();
nodes.push(LayoutNode::Matrix { lines, baseline: 0 });
format!("{}{}{}", LAYOUT_START, index, LAYOUT_END)
}
fn render_nested(&mut self, source: &str, stack_fractions: bool) -> String {
let rendered =
LatexParser::new(source, self.nodes.clone(), self.display && stack_fractions).render();
match rendered {
Some(r) => r,
None => {
self.supported = false;
source.to_string()
}
}
}
}
fn starts_with_case_word(condition: &str) -> bool {
let lower: Vec<char> = condition.to_lowercase().chars().collect();
for w in ["if", "when", "for", "otherwise"] {
let wchars: Vec<char> = w.chars().collect();
if lower.len() >= wchars.len()
&& lower[..wchars.len()] == wchars[..]
&& (lower.len() == wchars.len() || !lower[wchars.len()].is_alphanumeric())
{
return true;
}
}
false
}
pub fn render_latex(source: &str, display: bool) -> Option<String> {
let nodes: NodeRef = Rc::new(RefCell::new(Vec::new()));
let rendered = LatexParser::new(source, nodes.clone(), display).render()?;
if nodes.borrow().is_empty() {
return Some(rendered.replace(PROTECTED_SPACE, " "));
}
let layout = render_layout(&rendered, &nodes.borrow());
let indentation = layout
.lines
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
let mut out_lines: Vec<String> = Vec::new();
for l in &layout.lines {
let idx = l
.char_indices()
.nth(indentation)
.map(|(i, _)| i)
.unwrap_or(l.len());
out_lines.push(l[idx..].trim_end().to_string());
}
Some(
out_lines
.join("\n")
.trim_end()
.to_string()
.replace(PROTECTED_SPACE, " "),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inline_simple() {
assert_eq!(
render_latex("a^2 + b^2 = c^2", false).as_deref(),
Some("a² + b² = c²")
);
assert_eq!(render_latex("x_n", false).as_deref(), Some("xₙ"));
assert_eq!(render_latex("\\pi r^2", false).as_deref(), Some("π r²"));
assert_eq!(
render_latex("e^{i\\pi} + 1 = 0", false).as_deref(),
Some("e^(iπ) + 1 = 0")
);
}
#[test]
fn pythagorean() {
assert_eq!(
render_latex("a^2 + b^2 = c^2", false).as_deref(),
Some("a² + b² = c²")
);
}
#[test]
fn quadratic_formula_inline() {
assert_eq!(
render_latex("x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}", false).as_deref(),
Some("x = (-b ± √(b² - 4ac))/(2a)")
);
}
#[test]
fn taylor_inline() {
assert_eq!(
render_latex("e^x = \\sum_{n=0}^{\\infty} \\frac{x^n}{n!}", false).as_deref(),
Some("eˣ = ∑ₙ₌₀^∞ xⁿ/(n!)")
);
}
#[test]
fn fermat_inline() {
assert_eq!(
render_latex("a^{p-1} \\equiv 1 \\pmod{p}", false).as_deref(),
Some("aᵖ⁻¹ ≡ 1 (mod p)")
);
}
#[test]
fn normal_distribution_inline() {
assert_eq!(
render_latex(
"f(x) = \\frac{1}{\\sigma\\sqrt{2\\pi}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}",
false
)
.as_deref(),
Some("f(x) = 1/(σ√2π) e^(-((x-μ)²)/(2σ²))")
);
}
#[test]
fn stirling_inline() {
assert_eq!(
render_latex(
"n! \\approx \\sqrt{2\\pi n} \\left(\\frac{n}{e}\\right)^n",
false
)
.as_deref(),
Some("n! ≈ √(2π n) (n/e)ⁿ")
);
}
#[test]
fn geometric_series_inline() {
assert_eq!(
render_latex(
"\\sum_{n=0}^{\\infty} ar^n = \\frac{a}{1-r}, \\quad |r| < 1",
false
)
.as_deref(),
Some("∑ₙ₌₀^∞ arⁿ = a/(1-r), |r| < 1")
);
}
#[test]
fn display_fraction_stacks() {
let r = render_latex("\\frac{a}{b}", true).unwrap();
let lines: Vec<&str> = r.lines().collect();
assert_eq!(lines.len(), 3, "{:?}", r);
assert!(lines[0].contains('a'));
assert!(lines[1].contains('─'));
assert!(lines[2].contains('b'));
}
#[test]
fn display_sum_limits_stack() {
let r = render_latex("\\sum_{i=0}^{n} i", true).unwrap();
let lines: Vec<&str> = r.lines().collect();
assert!(lines.len() >= 3, "{:?}", r);
assert!(lines[0].contains('n'), "{:?}", r);
assert!(lines[1].contains('∑'), "{:?}", r);
assert!(lines[2].contains('i'), "{:?}", r);
}
#[test]
fn matrix_renders_multiline() {
let r = render_latex("\\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}", true).unwrap();
let lines: Vec<&str> = r.lines().collect();
assert!(lines.len() >= 2, "{:?}", r);
assert!(lines[0].contains('⎛') && lines[0].contains('1'), "{:?}", r);
assert!(
lines.last().unwrap().contains('⎞') || lines.last().unwrap().contains('⎝'),
"{:?}",
r
);
}
#[test]
fn unsupported_falls_back_none() {
assert_eq!(render_latex("\\unknowncommand{x}", false), None);
assert_eq!(render_latex("\\begin{unknown}", false), None);
}
#[test]
fn relation_spacing() {
assert_eq!(render_latex("a \\le b", false).as_deref(), Some("a ≤ b"));
assert_eq!(
render_latex("A \\subset B", false).as_deref(),
Some("A ⊂ B")
);
}
#[test]
fn named_operator_spacing() {
assert_eq!(
render_latex("\\sin x + \\cos x", false).as_deref(),
Some("sin x + cos x")
);
assert_eq!(render_latex("2\\sin x", false).as_deref(), Some("2 sin x"));
assert_eq!(render_latex("\\sin x", false).as_deref(), Some("sin x"));
assert_eq!(render_latex("\\ln n", false).as_deref(), Some("ln n"));
}
#[test]
fn blackboard_and_accents() {
assert_eq!(render_latex("\\mathbb{R}", false).as_deref(), Some("ℝ"));
assert_eq!(render_latex("\\hat{x}", false).as_deref(), Some("x\u{302}"));
assert_eq!(
render_latex("\\vec{v}", false).as_deref(),
Some("v\u{20d7}")
);
}
#[test]
fn legacy_font_switches_are_ignored() {
assert_eq!(
render_latex("{\\rm intrinsic}", false).as_deref(),
Some("intrinsic")
);
assert_eq!(render_latex("{\\bf bold}", false).as_deref(), Some("bold"));
assert_eq!(
render_latex("{\\cal calligraphic}", false).as_deref(),
Some("calligraphic")
);
assert_eq!(
render_latex("\\bf x + \\it y", false).as_deref(),
Some("x + y")
);
}
#[test]
fn not_negates() {
assert_eq!(render_latex("\\not\\leq", false).as_deref(), Some("≰"));
}
#[test]
fn cases_align_and_center_around_equation() {
assert_eq!(
render_latex(
r"f(x)=\begin{cases}a & x<0 \\ b & \text{if }x=0 \\ c & \text{otherwise}\end{cases}",
false,
)
.as_deref(),
Some(" ⎧ a if x < 0\nf(x) = ⎨ b if x = 0\n ⎩ c otherwise")
);
assert_eq!(
render_latex(
r"f(x) = \begin{cases} x^{2} & x \geq 0 \\ -x & x < 0 \end{cases}",
false,
)
.as_deref(),
Some(" ⎧ x² if x ≥ 0\nf(x) = ⎨\n ⎩ -x if x < 0")
);
}
#[test]
fn nested_and_unsupported_display_scripts_stack() {
assert_eq!(
render_latex(
r"\partial_tU_2(t,0)=Aj_*(1-t)^{-A-1}.\qquad x^{n^2}+x_{i_j}",
true,
)
.as_deref(),
Some(
" 2\n -A-1 n\n∂ₜU₂(t,0) = Aj (1-t) . x +x\n * i\n j"
)
);
assert_eq!(
render_latex(r"e^{\frac{1}{2}}+\tfrac{1}{2}", true).as_deref(),
Some("e^(1/2)+1/2")
);
}
}