1use std::collections::HashMap;
4use std::ops::Range;
5
6use crate::model::{Deco, FracStyle, Kind, Mark, MatrixEnv, NodeId, Selection, SeqId, Tree, Variant};
7
8#[derive(Debug, Clone, Default)]
10pub struct SpanMap {
11 pub node: HashMap<NodeId, Range<usize>>,
13 pub seq: HashMap<SeqId, Range<usize>>,
15}
16
17impl Tree {
18 pub fn export_latex(&self) -> (String, SpanMap) {
20 let mut ex = Exporter::new(self, true);
21 ex.emit_seq(self.root());
22 (ex.out, ex.spans)
23 }
24
25 pub fn export_display(&self) -> String {
27 let mut ex = Exporter::new(self, false);
28 ex.emit_seq(self.root());
29 ex.out
30 }
31
32 pub fn selection_latex(&self, sel: Selection) -> String {
34 let lo = sel.anchor.min(sel.focus);
35 let hi = sel.anchor.max(sel.focus).min(self.len(sel.seq));
36 let mut ex = Exporter::new(self, false);
37 let items = self.items(sel.seq)[lo..hi].to_vec();
38 for n in items {
39 ex.emit_node(n);
40 }
41 ex.out
42 }
43}
44
45struct Exporter<'a> {
46 tree: &'a Tree,
47 out: String,
48 spans: SpanMap,
49 placeholders: bool,
51}
52
53impl<'a> Exporter<'a> {
54 fn new(tree: &'a Tree, placeholders: bool) -> Self {
55 Self {
56 tree,
57 out: String::new(),
58 spans: SpanMap::default(),
59 placeholders,
60 }
61 }
62
63 fn emit_seq(&mut self, seq: SeqId) {
65 let start = self.out.len();
66 if self.tree.is_empty(seq) {
67 if self.placeholders {
69 self.out.push_str("\\phantom{x}");
70 }
71 } else {
72 let items = self.tree.items(seq).to_vec();
73 for (i, &n) in items.iter().enumerate() {
74 self.emit_node(n);
75 if self.needs_separator(n, items.get(i + 1).copied()) {
76 self.out.push(' ');
77 }
78 }
79 }
80 self.spans.seq.insert(seq, start..self.out.len());
81 }
82
83 fn needs_separator(&self, cur: NodeId, next: Option<NodeId>) -> bool {
85 let cur_ends_in_control_word = match self.tree.kind(cur) {
86 Some(Kind::Atom(s)) => is_control_word(&s.latex),
87 Some(Kind::BigOp { op, lower, upper }) => {
89 !self.placeholders
90 && self.tree.is_empty(*lower)
91 && self.tree.is_empty(*upper)
92 && is_control_word(&op.latex)
93 }
94 _ => false,
95 };
96 if !cur_ends_in_control_word {
97 return false;
98 }
99 next.and_then(|n| self.node_first_char(n))
100 .is_some_and(char::is_alphabetic)
101 }
102
103 fn node_first_char(&self, node: NodeId) -> Option<char> {
105 match self.tree.kind(node)? {
106 Kind::Atom(s) => s.latex.chars().next(),
107 Kind::Frac { style: FracStyle::Atop, .. } => Some('{'),
108 Kind::Frac { .. }
109 | Kind::Sqrt { .. }
110 | Kind::Delim { .. }
111 | Kind::Accent { .. }
112 | Kind::Styled { .. }
113 | Kind::Matrix { .. }
114 | Kind::HostBox { .. } => Some('\\'),
115 Kind::BigOp { .. } => Some('\\'),
117 Kind::Script { base, .. } => self.seq_first_char(*base),
118 Kind::UnderOver { base, over, under, .. } => {
119 if over.is_none() && under.is_none() {
120 self.seq_first_char(*base)
121 } else {
122 Some('\\')
123 }
124 }
125 }
126 }
127
128 fn seq_first_char(&self, seq: SeqId) -> Option<char> {
130 if self.tree.is_empty(seq) {
131 Some('\\')
132 } else {
133 self.node_first_char(self.tree.items(seq)[0])
134 }
135 }
136
137 fn emit_braced(&mut self, seq: SeqId) {
138 self.out.push('{');
139 self.emit_seq(seq);
140 self.out.push('}');
141 }
142
143 fn emit_arg(&mut self, seq: SeqId) {
145 let items = self.tree.items(seq);
146 let bare = items.len() == 1
147 && matches!(
148 self.tree.kind(items[0]),
149 Some(Kind::Atom(s)) if s.latex.chars().count() == 1
150 );
151 if bare {
152 self.emit_seq(seq);
153 } else {
154 self.emit_braced(seq);
155 }
156 }
157
158 fn emit_node(&mut self, node: NodeId) {
159 let start = self.out.len();
160 let Some(kind) = self.tree.kind(node).cloned() else {
161 return;
162 };
163 match kind {
164 Kind::Atom(s) => {
165 self.out.push_str(&s.latex);
166 }
167 Kind::HostBox { token } => {
169 self.out.push_str("\\hostbox{");
170 self.out.push_str(&token.to_string());
171 self.out.push('}');
172 }
173 Kind::Frac { num, den, style } => match style {
174 FracStyle::Atop => {
175 self.out.push('{');
176 self.emit_seq(num);
177 self.out.push_str("\\atop ");
178 self.emit_seq(den);
179 self.out.push('}');
180 }
181 _ => {
182 self.out.push_str(frac_cmd(style));
183 self.emit_braced(num);
184 self.emit_braced(den);
185 }
186 },
187 Kind::Script { base, sub, sup } => {
188 self.emit_seq(base);
189 if let Some(s) = sub {
190 self.out.push('_');
191 self.emit_arg(s);
192 }
193 if let Some(s) = sup {
194 self.out.push('^');
195 self.emit_arg(s);
196 }
197 }
198 Kind::BigOp { op, lower, upper } => {
199 self.out.push_str(&op.latex);
201 if self.placeholders || !self.tree.is_empty(lower) {
202 self.out.push('_');
203 self.emit_arg(lower);
204 }
205 if self.placeholders || !self.tree.is_empty(upper) {
206 self.out.push('^');
207 self.emit_arg(upper);
208 }
209 }
210 Kind::Sqrt { index, radicand } => {
211 self.out.push_str("\\sqrt");
212 if self.placeholders || !self.tree.is_empty(index) {
214 self.out.push('[');
215 self.emit_seq(index);
216 self.out.push(']');
217 }
218 self.emit_braced(radicand);
219 }
220 Kind::Delim { open, close, body } => {
221 self.out.push_str("\\left");
222 self.out.push_str(&delim(open));
223 self.emit_seq(body);
224 self.out.push_str("\\right");
225 self.out.push_str(&delim(close));
226 }
227 Kind::Accent { mark, base } => {
228 self.out.push_str(accent_cmd(mark));
229 self.emit_braced(base);
230 }
231 Kind::UnderOver {
232 base,
233 over,
234 under,
235 over_deco,
236 under_deco,
237 } => self.emit_under_over(base, over, under, over_deco, under_deco),
238 Kind::Styled { variant, content } => {
239 self.out.push_str(variant_cmd(variant));
240 self.emit_braced(content);
241 }
242 Kind::Matrix { env, rows } => self.emit_matrix(env, &rows),
243 }
244 self.spans.node.insert(node, start..self.out.len());
245 }
246
247 fn emit_under_over(
248 &mut self,
249 base: SeqId,
250 over: Option<SeqId>,
251 under: Option<SeqId>,
252 over_deco: Deco,
253 under_deco: Deco,
254 ) {
255 match (over, under) {
256 (Some(o), None) => match over_deco {
257 Deco::Brace => {
258 self.out.push_str("\\overbrace");
259 self.emit_braced(base);
260 self.out.push('^');
261 self.emit_braced(o);
262 }
263 _ => {
264 self.out.push_str("\\overset");
265 self.emit_braced(o);
266 self.emit_braced(base);
267 }
268 },
269 (None, Some(u)) => match under_deco {
270 Deco::Brace => {
271 self.out.push_str("\\underbrace");
272 self.emit_braced(base);
273 self.out.push('_');
274 self.emit_braced(u);
275 }
276 _ => {
277 self.out.push_str("\\underset");
278 self.emit_braced(u);
279 self.emit_braced(base);
280 }
281 },
282 (Some(o), Some(u)) => {
283 self.out.push_str("\\overset");
284 self.emit_braced(o);
285 self.out.push('{');
286 self.out.push_str("\\underset");
287 self.emit_braced(u);
288 self.emit_braced(base);
289 self.out.push('}');
290 }
291 (None, None) => self.emit_seq(base),
292 }
293 }
294
295 fn emit_matrix(&mut self, env: MatrixEnv, rows: &[Vec<SeqId>]) {
296 let name = matrix_env_name(env);
297 self.out.push_str("\\begin{");
298 self.out.push_str(name);
299 self.out.push('}');
300 if matches!(env, MatrixEnv::Array) {
301 let cols = rows.first().map_or(0, |r| r.len());
302 self.out.push('{');
303 self.out.push_str(&"c".repeat(cols));
304 self.out.push('}');
305 }
306 for (ri, row) in rows.iter().enumerate() {
307 if ri > 0 {
308 self.out.push_str(" \\\\ ");
309 }
310 for (ci, &cell) in row.iter().enumerate() {
311 if ci > 0 {
312 self.out.push_str(" & ");
313 }
314 self.emit_seq(cell);
315 }
316 }
317 self.out.push_str("\\end{");
318 self.out.push_str(name);
319 self.out.push('}');
320 }
321}
322
323fn is_control_word(latex: &str) -> bool {
324 latex.starts_with('\\')
325 && latex
326 .chars()
327 .last()
328 .is_some_and(|c| c.is_ascii_alphabetic())
329}
330
331fn frac_cmd(style: FracStyle) -> &'static str {
332 match style {
333 FracStyle::Bar => "\\frac",
334 FracStyle::Display => "\\dfrac",
335 FracStyle::Text => "\\tfrac",
336 FracStyle::Binom => "\\binom",
337 FracStyle::Atop => "\\frac",
338 }
339}
340
341fn delim(c: char) -> String {
342 match c {
343 '{' => "\\{".to_string(),
344 '}' => "\\}".to_string(),
345 c => c.to_string(),
346 }
347}
348
349fn accent_cmd(mark: Mark) -> &'static str {
350 match mark {
351 Mark::Hat => "\\hat",
352 Mark::Vec => "\\vec",
353 Mark::Bar => "\\bar",
354 Mark::Tilde => "\\tilde",
355 Mark::Dot => "\\dot",
356 Mark::Ddot => "\\ddot",
357 Mark::Widehat => "\\widehat",
358 Mark::Widetilde => "\\widetilde",
359 Mark::Overline => "\\overline",
360 Mark::Underline => "\\underline",
361 Mark::Check => "\\check",
362 Mark::Breve => "\\breve",
363 }
364}
365
366fn variant_cmd(v: Variant) -> &'static str {
367 match v {
368 Variant::Normal => "\\mathnormal",
369 Variant::Bold => "\\mathbf",
370 Variant::Blackboard => "\\mathbb",
371 Variant::Calligraphic => "\\mathcal",
372 Variant::Fraktur => "\\mathfrak",
373 Variant::Roman => "\\mathrm",
374 Variant::SansSerif => "\\mathsf",
375 Variant::Typewriter => "\\mathtt",
376 Variant::Text => "\\text",
377 Variant::OperatorName => "\\operatorname",
378 }
379}
380
381fn matrix_env_name(env: MatrixEnv) -> &'static str {
382 match env {
383 MatrixEnv::Matrix => "matrix",
384 MatrixEnv::Pmatrix => "pmatrix",
385 MatrixEnv::Bmatrix => "bmatrix",
386 MatrixEnv::Vmatrix => "vmatrix",
387 MatrixEnv::Cases => "cases",
388 MatrixEnv::Aligned => "aligned",
389 MatrixEnv::Array => "array",
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::model::{Cursor, MathClass, ScriptSlot, Symbol};
397
398 fn atom(c: &str) -> Symbol {
399 Symbol {
400 latex: c.into(),
401 class: MathClass::Ord,
402 }
403 }
404
405 #[test]
406 fn export_fraction_with_byte_spans() {
407 let mut t = Tree::new();
408 let root = t.root();
409 let cnum = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
410 t.insert_atom(cnum, atom("a"));
411 let frac = t.items(root)[0];
412 let (num, den) = (t.child_seqs(frac)[0], t.child_seqs(frac)[1]);
413 t.insert_atom(Cursor { seq: den, index: 0 }, atom("b"));
414
415 let (s, spans) = t.export_latex();
416 assert_eq!(s, "\\frac{a}{b}");
417 assert_eq!(&s[spans.node[&frac].clone()], "\\frac{a}{b}");
418 assert_eq!(&s[spans.seq[&num].clone()], "a");
419 assert_eq!(&s[spans.seq[&den].clone()], "b");
420 }
421
422 #[test]
423 fn empty_slot_emits_phantom_with_a_span() {
424 let mut t = Tree::new();
425 let root = t.root();
426 t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
427 let frac = t.items(root)[0];
428 let num = t.child_seqs(frac)[0];
429 let (s, spans) = t.export_latex();
430 assert_eq!(s, "\\frac{\\phantom{x}}{\\phantom{x}}");
432 assert_eq!(&s[spans.seq[&num].clone()], "\\phantom{x}");
433 }
434
435 #[test]
436 fn typed_specials_export_escaped() {
437 let mut t = Tree::new();
438 let root = t.root();
439 let mut c = Cursor { seq: root, index: 0 };
440 for ch in "50%&$".chars() {
441 c = t.insert_atom(c, Symbol::from_char(ch));
442 }
443 let (s, _) = t.export_latex();
444 assert_eq!(s, "50\\%\\&\\$");
445 }
446
447 #[test]
448 fn export_script_and_sqrt() {
449 let mut t = Tree::new();
450 let root = t.root();
451 t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
452 let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
453 t.insert_atom(c, atom("2"));
454 let (s, _) = t.export_latex();
455 assert_eq!(s, "x^2");
456 }
457
458 #[test]
459 fn control_word_before_scripted_letter_keeps_separator() {
460 let mut t = Tree::new();
462 let root = t.root();
463 let mut c = Cursor { seq: root, index: 0 };
464 c = t.insert_atom(c, atom("\\sum"));
465 c = t.insert_atom(c, atom("x"));
466 let sup = t.attach_script(c, ScriptSlot::Sup);
467 t.insert_atom(sup, atom("2"));
468 let (s, _) = t.export_latex();
469 assert_eq!(s, "\\sum x^2");
470 }
471
472 #[test]
473 fn control_word_before_command_needs_no_separator() {
474 let mut t = Tree::new();
476 let root = t.root();
477 let c = t.insert_atom(Cursor { seq: root, index: 0 }, atom("\\sum"));
478 let num = t.insert_fraction(c, FracStyle::Bar, None);
479 t.insert_atom(num, atom("a"));
480 let (s, _) = t.export_latex();
481 assert_eq!(s, "\\sum\\frac{a}{\\phantom{x}}");
482 }
483
484 #[test]
485 fn export_host_box_with_one_node_span() {
486 let mut t = Tree::new();
487 let root = t.root();
488 let c = t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
489 t.insert_host_box(c, 17);
490 let (s, spans) = t.export_latex();
491 assert_eq!(s, "a\\hostbox{17}");
492 let hb = t.items(root)[1];
494 assert_eq!(&s[spans.node[&hb].clone()], "\\hostbox{17}");
495 }
496
497 #[test]
498 fn export_host_box_nested_in_numerator_superscript_and_text() {
499 let mut t = Tree::new();
500 let root = t.root();
501 let cnum = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
502 t.insert_host_box(cnum, 17);
503 let (s, _) = t.export_latex();
504 assert_eq!(s, "\\frac{\\hostbox{17}}{\\phantom{x}}");
505
506 let mut t = Tree::new();
507 let root = t.root();
508 t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
509 let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
510 t.insert_host_box(c, 17);
511 let (s, _) = t.export_latex();
512 assert_eq!(s, "x^{\\hostbox{17}}");
513
514 let mut t = Tree::new();
515 let root = t.root();
516 let c = t.insert_styled(Cursor { seq: root, index: 0 }, crate::model::Variant::Text, None);
517 t.insert_host_box(c, 17);
518 let (s, _) = t.export_latex();
519 assert_eq!(s, "\\text{\\hostbox{17}}");
520 }
521
522 #[test]
523 fn script_multichar_keeps_braces() {
524 let mut t = Tree::new();
525 let root = t.root();
526 t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
527 let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
528 let c = t.insert_atom(c, atom("1"));
529 t.insert_atom(c, atom("2"));
530 let (s, _) = t.export_latex();
531 assert_eq!(s, "x^{12}");
532 }
533}