use crate::ast::{Node, Row};
pub fn row_to_latex(row: &Row) -> String {
let mut s = String::new();
for (i, n) in row.iter().enumerate() {
let next_script = match row[i + 1..].iter().find(|n| **n != Node::Spacer) {
Some(Node::Sup { .. }) => Some(true),
Some(Node::Sub { .. }) => Some(false),
_ => None,
};
let absorbs = match n {
Node::Sym(c) if crate::symbols::is_bigop(*c) => next_script.is_some(),
Node::BigOpSym { .. } | Node::BigOp { .. } => next_script.is_some(),
Node::Brace { over, .. } => next_script == Some(*over),
_ => false,
};
if absorbs {
s.push('{');
s.push_str(node_to_latex(n).trim_end());
s.push('}');
} else {
s.push_str(&node_to_latex(n));
}
}
s
}
fn braced(row: &Row) -> String {
format!("{{{}}}", row_to_latex(row))
}
fn escaped(c: char) -> Option<String> {
match c {
'#' | '$' | '%' | '&' | '{' | '}' | '_' => Some(format!("\\{}", c)),
'\\' => Some("\\textbackslash{}".into()),
'^' => Some("\\textasciicircum{}".into()),
'~' => Some("\\textasciitilde{}".into()),
_ => None,
}
}
fn text_to_latex(t: &str) -> String {
t.chars()
.map(|c| escaped(c).unwrap_or_else(|| c.to_string()))
.collect()
}
fn sym_to_latex(c: char) -> String {
if c == '␣' {
return "\\ ".into();
}
if let Some(e) = escaped(c) {
return e;
}
crate::symbols::latex_of(c).unwrap_or_else(|| c.to_string())
}
fn limited(base: &str, lower: &Row, upper: &Row) -> String {
let mut s = base.to_string();
if !lower.is_empty() {
s.push_str(&format!("_{}", braced(lower)));
}
if !upper.is_empty() {
s.push_str(&format!("^{}", braced(upper)));
}
s
}
fn node_to_latex(node: &Node) -> String {
match node {
Node::Spacer => String::new(),
Node::Break => " \\\\ ".into(),
Node::Sym(c) => sym_to_latex(*c),
Node::Func(name) => format!(
"\\operatorname{{{}}}",
crate::symbols::func_latex_text(name)
),
Node::WideAccent {
overs,
unders,
base,
} => {
let sole_accent = matches!(base[..], [Node::Accent { .. } | Node::WideAccent { .. }]);
let mut s = if sole_accent {
format!("{{{}}}", row_to_latex(base))
} else {
row_to_latex(base)
};
for &m in unders.iter().chain(overs.iter()) {
s = format!("\\{}{{{}}}", m.wide_latex(), s);
}
s
}
Node::Accent {
overs,
unders,
base,
} => {
let mut s = if *base == '␣' {
sym_to_latex(*base)
} else {
sym_to_latex(*base).trim_end().to_string()
};
for &m in unders.iter().chain(overs.iter()) {
let cmd = m.latex();
s = format!("\\{}{{{}}}", cmd, s);
}
s
}
Node::Frac { num, den } => format!("\\frac{}{}", braced(num), braced(den)),
Node::Sqrt { arg, index } => match index.latex_index() {
None => format!("\\sqrt{}", braced(arg)),
Some(i) => format!("\\sqrt[{}]{}", i, braced(arg)),
},
Node::Norm { arg } => {
if let [Node::Array { cols, cells, .. }] = &arg[..] {
return format!(
"\\begin{{Vmatrix}} {} \\end{{Vmatrix}}",
array_body(*cols, cells)
);
}
format!("\\left\\|{}\\right\\|", row_to_latex(arg))
}
Node::Sup { arg } => format!("^{}", braced(arg)),
Node::Sub { arg } => format!("_{}", braced(arg)),
Node::BigOpSym { op, lower, upper } => limited(sym_to_latex(*op).trim_end(), lower, upper),
Node::BigOp { name, lower, upper } => limited(
&format!(
"\\operatorname*{{{}}}",
crate::symbols::func_latex_text(name)
),
lower,
upper,
),
Node::Roman(c) => format!("\\mathrm{{{}}}", c),
Node::Text(t) => format!("\\text{{{}}}", text_to_latex(t)),
Node::Brace { over, arg, label } => {
let (cmd, att) = if *over {
("overbrace", '^')
} else {
("underbrace", '_')
};
let mut s = format!("\\{}{}", cmd, braced(arg));
if !label.is_empty() {
s.push_str(&format!("{}{}", att, braced(label)));
}
s
}
Node::Arrow { op, over, under } => {
let cmd = op.latex();
let mut s = format!("\\{}", cmd);
if !under.is_empty() {
s.push_str(&format!("[{}]", row_to_latex(under)));
}
s.push_str(&braced(over));
s
}
Node::Delim {
left,
right,
mids,
segs,
} => {
if *mids == 0
&& let [seg] = &segs[..]
&& let [Node::Array { cols, cells, .. }] = &seg[..]
{
let env = crate::editor::grid_env_name(*left, *right, *cols);
if let Some(env) = env {
return format!(
"\\begin{{{env}}} {} \\end{{{env}}}",
array_body(*cols, cells)
);
}
}
let mut s = format!("\\left{}", left.latex(true));
for (k, seg) in segs.iter().enumerate() {
if k > 0 {
s.push_str("\\middle|");
}
s.push_str(&row_to_latex(seg));
}
s.push_str(&format!("\\right{}", right.latex(false)));
s
}
Node::Array { cols, cells, .. } => {
format!(
"\\begin{{matrix}} {} \\end{{matrix}}",
array_body(*cols, cells)
)
}
}
}
fn array_body(cols: usize, cells: &[Row]) -> String {
cells
.chunks(cols)
.map(|row| row.iter().map(row_to_latex).collect::<Vec<_>>().join(" & "))
.collect::<Vec<_>>()
.join(" \\\\ ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbols::Accent;
#[test]
fn serializes_basic_formula() {
let root = vec![
Node::Sym('x'),
Node::Sup {
arg: vec![Node::Sym('2')],
},
Node::Sym('+'),
Node::Frac {
num: vec![Node::Sym('α')],
den: vec![Node::Sym('2')],
},
];
assert_eq!(row_to_latex(&root), "x^{2}+\\frac{\\alpha }{2}");
}
#[test]
fn serializes_bigop_limits() {
let root = vec![Node::BigOpSym {
op: '∑',
lower: vec![Node::Sym('i')],
upper: vec![Node::Sym('n')],
}];
assert_eq!(row_to_latex(&root), "\\sum_{i}^{n}");
}
#[test]
fn serializes_matrix_func_accent() {
let root = vec![
Node::Func("sin".into()),
Node::Accent {
overs: vec![Accent::Vec],
unders: vec![],
base: 'v',
},
Node::Delim {
left: crate::symbols::Delim::Col(crate::symbols::ColDelim::Bracket),
right: crate::symbols::Delim::Col(crate::symbols::ColDelim::Bracket),
mids: 0,
segs: vec![vec![Node::Array {
rows: 1,
cols: 2,
cells: vec![vec![Node::Sym('a')], vec![Node::Sym('b')]],
}]],
},
Node::Sqrt {
arg: vec![Node::Sym('x')],
index: crate::symbols::Radical::Cbrt,
},
];
assert_eq!(
row_to_latex(&root),
"\\operatorname{sin}\\vec{v}\\begin{bmatrix} a & b \\end{bmatrix}\\sqrt[3]{x}"
);
}
}