use std::collections::HashMap;
use std::ops::Range;
use crate::model::{Deco, FracStyle, Kind, Mark, MatrixEnv, NodeId, Selection, SeqId, Tree, Variant};
#[derive(Debug, Clone, Default)]
pub struct SpanMap {
pub node: HashMap<NodeId, Range<usize>>,
pub seq: HashMap<SeqId, Range<usize>>,
}
impl Tree {
pub fn export_latex(&self) -> (String, SpanMap) {
let mut ex = Exporter::new(self, true);
ex.emit_seq(self.root());
(ex.out, ex.spans)
}
pub fn export_display(&self) -> String {
let mut ex = Exporter::new(self, false);
ex.emit_seq(self.root());
ex.out
}
pub fn selection_latex(&self, sel: Selection) -> String {
let lo = sel.anchor.min(sel.focus);
let hi = sel.anchor.max(sel.focus).min(self.len(sel.seq));
let mut ex = Exporter::new(self, false);
let items = self.items(sel.seq)[lo..hi].to_vec();
for n in items {
ex.emit_node(n);
}
ex.out
}
}
struct Exporter<'a> {
tree: &'a Tree,
out: String,
spans: SpanMap,
placeholders: bool,
}
impl<'a> Exporter<'a> {
fn new(tree: &'a Tree, placeholders: bool) -> Self {
Self {
tree,
out: String::new(),
spans: SpanMap::default(),
placeholders,
}
}
fn emit_seq(&mut self, seq: SeqId) {
let start = self.out.len();
if self.tree.is_empty(seq) {
if self.placeholders {
self.out.push_str("\\phantom{x}");
}
} else {
let items = self.tree.items(seq).to_vec();
for (i, &n) in items.iter().enumerate() {
self.emit_node(n);
if self.needs_separator(n, items.get(i + 1).copied()) {
self.out.push(' ');
}
}
}
self.spans.seq.insert(seq, start..self.out.len());
}
fn needs_separator(&self, cur: NodeId, next: Option<NodeId>) -> bool {
let cur_ends_in_control_word = match self.tree.kind(cur) {
Some(Kind::Atom(s)) => is_control_word(&s.latex),
Some(Kind::BigOp { op, lower, upper }) => {
!self.placeholders
&& self.tree.is_empty(*lower)
&& self.tree.is_empty(*upper)
&& is_control_word(&op.latex)
}
_ => false,
};
if !cur_ends_in_control_word {
return false;
}
next.and_then(|n| self.node_first_char(n))
.is_some_and(char::is_alphabetic)
}
fn node_first_char(&self, node: NodeId) -> Option<char> {
match self.tree.kind(node)? {
Kind::Atom(s) => s.latex.chars().next(),
Kind::Frac { style: FracStyle::Atop, .. } => Some('{'),
Kind::Frac { .. }
| Kind::Sqrt { .. }
| Kind::Delim { .. }
| Kind::Accent { .. }
| Kind::Styled { .. }
| Kind::Matrix { .. } => Some('\\'),
Kind::BigOp { .. } => Some('\\'),
Kind::Script { base, .. } => self.seq_first_char(*base),
Kind::UnderOver { base, over, under, .. } => {
if over.is_none() && under.is_none() {
self.seq_first_char(*base)
} else {
Some('\\')
}
}
}
}
fn seq_first_char(&self, seq: SeqId) -> Option<char> {
if self.tree.is_empty(seq) {
Some('\\')
} else {
self.node_first_char(self.tree.items(seq)[0])
}
}
fn emit_braced(&mut self, seq: SeqId) {
self.out.push('{');
self.emit_seq(seq);
self.out.push('}');
}
fn emit_arg(&mut self, seq: SeqId) {
let items = self.tree.items(seq);
let bare = items.len() == 1
&& matches!(
self.tree.kind(items[0]),
Some(Kind::Atom(s)) if s.latex.chars().count() == 1
);
if bare {
self.emit_seq(seq);
} else {
self.emit_braced(seq);
}
}
fn emit_node(&mut self, node: NodeId) {
let start = self.out.len();
let Some(kind) = self.tree.kind(node).cloned() else {
return;
};
match kind {
Kind::Atom(s) => {
self.out.push_str(&s.latex);
}
Kind::Frac { num, den, style } => match style {
FracStyle::Atop => {
self.out.push('{');
self.emit_seq(num);
self.out.push_str("\\atop ");
self.emit_seq(den);
self.out.push('}');
}
_ => {
self.out.push_str(frac_cmd(style));
self.emit_braced(num);
self.emit_braced(den);
}
},
Kind::Script { base, sub, sup } => {
self.emit_seq(base);
if let Some(s) = sub {
self.out.push('_');
self.emit_arg(s);
}
if let Some(s) = sup {
self.out.push('^');
self.emit_arg(s);
}
}
Kind::BigOp { op, lower, upper } => {
self.out.push_str(&op.latex);
if self.placeholders || !self.tree.is_empty(lower) {
self.out.push('_');
self.emit_arg(lower);
}
if self.placeholders || !self.tree.is_empty(upper) {
self.out.push('^');
self.emit_arg(upper);
}
}
Kind::Sqrt { index, radicand } => {
self.out.push_str("\\sqrt");
if self.placeholders || !self.tree.is_empty(index) {
self.out.push('[');
self.emit_seq(index);
self.out.push(']');
}
self.emit_braced(radicand);
}
Kind::Delim { open, close, body } => {
self.out.push_str("\\left");
self.out.push_str(&delim(open));
self.emit_seq(body);
self.out.push_str("\\right");
self.out.push_str(&delim(close));
}
Kind::Accent { mark, base } => {
self.out.push_str(accent_cmd(mark));
self.emit_braced(base);
}
Kind::UnderOver {
base,
over,
under,
over_deco,
under_deco,
} => self.emit_under_over(base, over, under, over_deco, under_deco),
Kind::Styled { variant, content } => {
self.out.push_str(variant_cmd(variant));
self.emit_braced(content);
}
Kind::Matrix { env, rows } => self.emit_matrix(env, &rows),
}
self.spans.node.insert(node, start..self.out.len());
}
fn emit_under_over(
&mut self,
base: SeqId,
over: Option<SeqId>,
under: Option<SeqId>,
over_deco: Deco,
under_deco: Deco,
) {
match (over, under) {
(Some(o), None) => match over_deco {
Deco::Brace => {
self.out.push_str("\\overbrace");
self.emit_braced(base);
self.out.push('^');
self.emit_braced(o);
}
_ => {
self.out.push_str("\\overset");
self.emit_braced(o);
self.emit_braced(base);
}
},
(None, Some(u)) => match under_deco {
Deco::Brace => {
self.out.push_str("\\underbrace");
self.emit_braced(base);
self.out.push('_');
self.emit_braced(u);
}
_ => {
self.out.push_str("\\underset");
self.emit_braced(u);
self.emit_braced(base);
}
},
(Some(o), Some(u)) => {
self.out.push_str("\\overset");
self.emit_braced(o);
self.out.push('{');
self.out.push_str("\\underset");
self.emit_braced(u);
self.emit_braced(base);
self.out.push('}');
}
(None, None) => self.emit_seq(base),
}
}
fn emit_matrix(&mut self, env: MatrixEnv, rows: &[Vec<SeqId>]) {
let name = matrix_env_name(env);
self.out.push_str("\\begin{");
self.out.push_str(name);
self.out.push('}');
if matches!(env, MatrixEnv::Array) {
let cols = rows.first().map_or(0, |r| r.len());
self.out.push('{');
self.out.push_str(&"c".repeat(cols));
self.out.push('}');
}
for (ri, row) in rows.iter().enumerate() {
if ri > 0 {
self.out.push_str(" \\\\ ");
}
for (ci, &cell) in row.iter().enumerate() {
if ci > 0 {
self.out.push_str(" & ");
}
self.emit_seq(cell);
}
}
self.out.push_str("\\end{");
self.out.push_str(name);
self.out.push('}');
}
}
fn is_control_word(latex: &str) -> bool {
latex.starts_with('\\')
&& latex
.chars()
.last()
.is_some_and(|c| c.is_ascii_alphabetic())
}
fn frac_cmd(style: FracStyle) -> &'static str {
match style {
FracStyle::Bar => "\\frac",
FracStyle::Display => "\\dfrac",
FracStyle::Text => "\\tfrac",
FracStyle::Binom => "\\binom",
FracStyle::Atop => "\\frac",
}
}
fn delim(c: char) -> String {
match c {
'{' => "\\{".to_string(),
'}' => "\\}".to_string(),
c => c.to_string(),
}
}
fn accent_cmd(mark: Mark) -> &'static str {
match mark {
Mark::Hat => "\\hat",
Mark::Vec => "\\vec",
Mark::Bar => "\\bar",
Mark::Tilde => "\\tilde",
Mark::Dot => "\\dot",
Mark::Ddot => "\\ddot",
Mark::Widehat => "\\widehat",
Mark::Widetilde => "\\widetilde",
Mark::Overline => "\\overline",
Mark::Underline => "\\underline",
Mark::Check => "\\check",
Mark::Breve => "\\breve",
}
}
fn variant_cmd(v: Variant) -> &'static str {
match v {
Variant::Normal => "\\mathnormal",
Variant::Bold => "\\mathbf",
Variant::Blackboard => "\\mathbb",
Variant::Calligraphic => "\\mathcal",
Variant::Fraktur => "\\mathfrak",
Variant::Roman => "\\mathrm",
Variant::SansSerif => "\\mathsf",
Variant::Typewriter => "\\mathtt",
Variant::Text => "\\text",
Variant::OperatorName => "\\operatorname",
}
}
fn matrix_env_name(env: MatrixEnv) -> &'static str {
match env {
MatrixEnv::Matrix => "matrix",
MatrixEnv::Pmatrix => "pmatrix",
MatrixEnv::Bmatrix => "bmatrix",
MatrixEnv::Vmatrix => "vmatrix",
MatrixEnv::Cases => "cases",
MatrixEnv::Aligned => "aligned",
MatrixEnv::Array => "array",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Cursor, MathClass, ScriptSlot, Symbol};
fn atom(c: &str) -> Symbol {
Symbol {
latex: c.into(),
class: MathClass::Ord,
}
}
#[test]
fn export_fraction_with_byte_spans() {
let mut t = Tree::new();
let root = t.root();
let cnum = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
t.insert_atom(cnum, atom("a"));
let frac = t.items(root)[0];
let (num, den) = (t.child_seqs(frac)[0], t.child_seqs(frac)[1]);
t.insert_atom(Cursor { seq: den, index: 0 }, atom("b"));
let (s, spans) = t.export_latex();
assert_eq!(s, "\\frac{a}{b}");
assert_eq!(&s[spans.node[&frac].clone()], "\\frac{a}{b}");
assert_eq!(&s[spans.seq[&num].clone()], "a");
assert_eq!(&s[spans.seq[&den].clone()], "b");
}
#[test]
fn empty_slot_emits_phantom_with_a_span() {
let mut t = Tree::new();
let root = t.root();
t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
let frac = t.items(root)[0];
let num = t.child_seqs(frac)[0];
let (s, spans) = t.export_latex();
assert_eq!(s, "\\frac{\\phantom{x}}{\\phantom{x}}");
assert_eq!(&s[spans.seq[&num].clone()], "\\phantom{x}");
}
#[test]
fn typed_specials_export_escaped() {
let mut t = Tree::new();
let root = t.root();
let mut c = Cursor { seq: root, index: 0 };
for ch in "50%&$".chars() {
c = t.insert_atom(c, Symbol::from_char(ch));
}
let (s, _) = t.export_latex();
assert_eq!(s, "50\\%\\&\\$");
}
#[test]
fn export_script_and_sqrt() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
t.insert_atom(c, atom("2"));
let (s, _) = t.export_latex();
assert_eq!(s, "x^2");
}
#[test]
fn control_word_before_scripted_letter_keeps_separator() {
let mut t = Tree::new();
let root = t.root();
let mut c = Cursor { seq: root, index: 0 };
c = t.insert_atom(c, atom("\\sum"));
c = t.insert_atom(c, atom("x"));
let sup = t.attach_script(c, ScriptSlot::Sup);
t.insert_atom(sup, atom("2"));
let (s, _) = t.export_latex();
assert_eq!(s, "\\sum x^2");
}
#[test]
fn control_word_before_command_needs_no_separator() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_atom(Cursor { seq: root, index: 0 }, atom("\\sum"));
let num = t.insert_fraction(c, FracStyle::Bar, None);
t.insert_atom(num, atom("a"));
let (s, _) = t.export_latex();
assert_eq!(s, "\\sum\\frac{a}{\\phantom{x}}");
}
#[test]
fn script_multichar_keeps_braces() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
let c = t.insert_atom(c, atom("1"));
t.insert_atom(c, atom("2"));
let (s, _) = t.export_latex();
assert_eq!(s, "x^{12}");
}
}