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 { .. } => Some('\\'),
114 Kind::BigOp { .. } => Some('\\'),
116 Kind::Script { base, .. } => self.seq_first_char(*base),
117 Kind::UnderOver { base, over, under, .. } => {
118 if over.is_none() && under.is_none() {
119 self.seq_first_char(*base)
120 } else {
121 Some('\\')
122 }
123 }
124 }
125 }
126
127 fn seq_first_char(&self, seq: SeqId) -> Option<char> {
129 if self.tree.is_empty(seq) {
130 Some('\\')
131 } else {
132 self.node_first_char(self.tree.items(seq)[0])
133 }
134 }
135
136 fn emit_braced(&mut self, seq: SeqId) {
137 self.out.push('{');
138 self.emit_seq(seq);
139 self.out.push('}');
140 }
141
142 fn emit_arg(&mut self, seq: SeqId) {
144 let items = self.tree.items(seq);
145 let bare = items.len() == 1
146 && matches!(
147 self.tree.kind(items[0]),
148 Some(Kind::Atom(s)) if s.latex.chars().count() == 1
149 );
150 if bare {
151 self.emit_seq(seq);
152 } else {
153 self.emit_braced(seq);
154 }
155 }
156
157 fn emit_node(&mut self, node: NodeId) {
158 let start = self.out.len();
159 let Some(kind) = self.tree.kind(node).cloned() else {
160 return;
161 };
162 match kind {
163 Kind::Atom(s) => {
164 self.out.push_str(&s.latex);
165 }
166 Kind::Frac { num, den, style } => match style {
167 FracStyle::Atop => {
168 self.out.push('{');
169 self.emit_seq(num);
170 self.out.push_str("\\atop ");
171 self.emit_seq(den);
172 self.out.push('}');
173 }
174 _ => {
175 self.out.push_str(frac_cmd(style));
176 self.emit_braced(num);
177 self.emit_braced(den);
178 }
179 },
180 Kind::Script { base, sub, sup } => {
181 self.emit_seq(base);
182 if let Some(s) = sub {
183 self.out.push('_');
184 self.emit_arg(s);
185 }
186 if let Some(s) = sup {
187 self.out.push('^');
188 self.emit_arg(s);
189 }
190 }
191 Kind::BigOp { op, lower, upper } => {
192 self.out.push_str(&op.latex);
194 if self.placeholders || !self.tree.is_empty(lower) {
195 self.out.push('_');
196 self.emit_arg(lower);
197 }
198 if self.placeholders || !self.tree.is_empty(upper) {
199 self.out.push('^');
200 self.emit_arg(upper);
201 }
202 }
203 Kind::Sqrt { index, radicand } => {
204 self.out.push_str("\\sqrt");
205 if self.placeholders || !self.tree.is_empty(index) {
207 self.out.push('[');
208 self.emit_seq(index);
209 self.out.push(']');
210 }
211 self.emit_braced(radicand);
212 }
213 Kind::Delim { open, close, body } => {
214 self.out.push_str("\\left");
215 self.out.push_str(&delim(open));
216 self.emit_seq(body);
217 self.out.push_str("\\right");
218 self.out.push_str(&delim(close));
219 }
220 Kind::Accent { mark, base } => {
221 self.out.push_str(accent_cmd(mark));
222 self.emit_braced(base);
223 }
224 Kind::UnderOver {
225 base,
226 over,
227 under,
228 over_deco,
229 under_deco,
230 } => self.emit_under_over(base, over, under, over_deco, under_deco),
231 Kind::Styled { variant, content } => {
232 self.out.push_str(variant_cmd(variant));
233 self.emit_braced(content);
234 }
235 Kind::Matrix { env, rows } => self.emit_matrix(env, &rows),
236 }
237 self.spans.node.insert(node, start..self.out.len());
238 }
239
240 fn emit_under_over(
241 &mut self,
242 base: SeqId,
243 over: Option<SeqId>,
244 under: Option<SeqId>,
245 over_deco: Deco,
246 under_deco: Deco,
247 ) {
248 match (over, under) {
249 (Some(o), None) => match over_deco {
250 Deco::Brace => {
251 self.out.push_str("\\overbrace");
252 self.emit_braced(base);
253 self.out.push('^');
254 self.emit_braced(o);
255 }
256 _ => {
257 self.out.push_str("\\overset");
258 self.emit_braced(o);
259 self.emit_braced(base);
260 }
261 },
262 (None, Some(u)) => match under_deco {
263 Deco::Brace => {
264 self.out.push_str("\\underbrace");
265 self.emit_braced(base);
266 self.out.push('_');
267 self.emit_braced(u);
268 }
269 _ => {
270 self.out.push_str("\\underset");
271 self.emit_braced(u);
272 self.emit_braced(base);
273 }
274 },
275 (Some(o), Some(u)) => {
276 self.out.push_str("\\overset");
277 self.emit_braced(o);
278 self.out.push('{');
279 self.out.push_str("\\underset");
280 self.emit_braced(u);
281 self.emit_braced(base);
282 self.out.push('}');
283 }
284 (None, None) => self.emit_seq(base),
285 }
286 }
287
288 fn emit_matrix(&mut self, env: MatrixEnv, rows: &[Vec<SeqId>]) {
289 let name = matrix_env_name(env);
290 self.out.push_str("\\begin{");
291 self.out.push_str(name);
292 self.out.push('}');
293 if matches!(env, MatrixEnv::Array) {
294 let cols = rows.first().map_or(0, |r| r.len());
295 self.out.push('{');
296 self.out.push_str(&"c".repeat(cols));
297 self.out.push('}');
298 }
299 for (ri, row) in rows.iter().enumerate() {
300 if ri > 0 {
301 self.out.push_str(" \\\\ ");
302 }
303 for (ci, &cell) in row.iter().enumerate() {
304 if ci > 0 {
305 self.out.push_str(" & ");
306 }
307 self.emit_seq(cell);
308 }
309 }
310 self.out.push_str("\\end{");
311 self.out.push_str(name);
312 self.out.push('}');
313 }
314}
315
316fn is_control_word(latex: &str) -> bool {
317 latex.starts_with('\\')
318 && latex
319 .chars()
320 .last()
321 .is_some_and(|c| c.is_ascii_alphabetic())
322}
323
324fn frac_cmd(style: FracStyle) -> &'static str {
325 match style {
326 FracStyle::Bar => "\\frac",
327 FracStyle::Display => "\\dfrac",
328 FracStyle::Text => "\\tfrac",
329 FracStyle::Binom => "\\binom",
330 FracStyle::Atop => "\\frac",
331 }
332}
333
334fn delim(c: char) -> String {
335 match c {
336 '{' => "\\{".to_string(),
337 '}' => "\\}".to_string(),
338 c => c.to_string(),
339 }
340}
341
342fn accent_cmd(mark: Mark) -> &'static str {
343 match mark {
344 Mark::Hat => "\\hat",
345 Mark::Vec => "\\vec",
346 Mark::Bar => "\\bar",
347 Mark::Tilde => "\\tilde",
348 Mark::Dot => "\\dot",
349 Mark::Ddot => "\\ddot",
350 Mark::Widehat => "\\widehat",
351 Mark::Widetilde => "\\widetilde",
352 Mark::Overline => "\\overline",
353 Mark::Underline => "\\underline",
354 Mark::Check => "\\check",
355 Mark::Breve => "\\breve",
356 }
357}
358
359fn variant_cmd(v: Variant) -> &'static str {
360 match v {
361 Variant::Normal => "\\mathnormal",
362 Variant::Bold => "\\mathbf",
363 Variant::Blackboard => "\\mathbb",
364 Variant::Calligraphic => "\\mathcal",
365 Variant::Fraktur => "\\mathfrak",
366 Variant::Roman => "\\mathrm",
367 Variant::SansSerif => "\\mathsf",
368 Variant::Typewriter => "\\mathtt",
369 Variant::Text => "\\text",
370 Variant::OperatorName => "\\operatorname",
371 }
372}
373
374fn matrix_env_name(env: MatrixEnv) -> &'static str {
375 match env {
376 MatrixEnv::Matrix => "matrix",
377 MatrixEnv::Pmatrix => "pmatrix",
378 MatrixEnv::Bmatrix => "bmatrix",
379 MatrixEnv::Vmatrix => "vmatrix",
380 MatrixEnv::Cases => "cases",
381 MatrixEnv::Aligned => "aligned",
382 MatrixEnv::Array => "array",
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::model::{Cursor, MathClass, ScriptSlot, Symbol};
390
391 fn atom(c: &str) -> Symbol {
392 Symbol {
393 latex: c.into(),
394 class: MathClass::Ord,
395 }
396 }
397
398 #[test]
399 fn export_fraction_with_byte_spans() {
400 let mut t = Tree::new();
401 let root = t.root();
402 let cnum = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
403 t.insert_atom(cnum, atom("a"));
404 let frac = t.items(root)[0];
405 let (num, den) = (t.child_seqs(frac)[0], t.child_seqs(frac)[1]);
406 t.insert_atom(Cursor { seq: den, index: 0 }, atom("b"));
407
408 let (s, spans) = t.export_latex();
409 assert_eq!(s, "\\frac{a}{b}");
410 assert_eq!(&s[spans.node[&frac].clone()], "\\frac{a}{b}");
411 assert_eq!(&s[spans.seq[&num].clone()], "a");
412 assert_eq!(&s[spans.seq[&den].clone()], "b");
413 }
414
415 #[test]
416 fn empty_slot_emits_phantom_with_a_span() {
417 let mut t = Tree::new();
418 let root = t.root();
419 t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
420 let frac = t.items(root)[0];
421 let num = t.child_seqs(frac)[0];
422 let (s, spans) = t.export_latex();
423 assert_eq!(s, "\\frac{\\phantom{x}}{\\phantom{x}}");
425 assert_eq!(&s[spans.seq[&num].clone()], "\\phantom{x}");
426 }
427
428 #[test]
429 fn typed_specials_export_escaped() {
430 let mut t = Tree::new();
431 let root = t.root();
432 let mut c = Cursor { seq: root, index: 0 };
433 for ch in "50%&$".chars() {
434 c = t.insert_atom(c, Symbol::from_char(ch));
435 }
436 let (s, _) = t.export_latex();
437 assert_eq!(s, "50\\%\\&\\$");
438 }
439
440 #[test]
441 fn export_script_and_sqrt() {
442 let mut t = Tree::new();
443 let root = t.root();
444 t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
445 let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
446 t.insert_atom(c, atom("2"));
447 let (s, _) = t.export_latex();
448 assert_eq!(s, "x^2");
449 }
450
451 #[test]
452 fn control_word_before_scripted_letter_keeps_separator() {
453 let mut t = Tree::new();
455 let root = t.root();
456 let mut c = Cursor { seq: root, index: 0 };
457 c = t.insert_atom(c, atom("\\sum"));
458 c = t.insert_atom(c, atom("x"));
459 let sup = t.attach_script(c, ScriptSlot::Sup);
460 t.insert_atom(sup, atom("2"));
461 let (s, _) = t.export_latex();
462 assert_eq!(s, "\\sum x^2");
463 }
464
465 #[test]
466 fn control_word_before_command_needs_no_separator() {
467 let mut t = Tree::new();
469 let root = t.root();
470 let c = t.insert_atom(Cursor { seq: root, index: 0 }, atom("\\sum"));
471 let num = t.insert_fraction(c, FracStyle::Bar, None);
472 t.insert_atom(num, atom("a"));
473 let (s, _) = t.export_latex();
474 assert_eq!(s, "\\sum\\frac{a}{\\phantom{x}}");
475 }
476
477 #[test]
478 fn script_multichar_keeps_braces() {
479 let mut t = Tree::new();
480 let root = t.root();
481 t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
482 let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
483 let c = t.insert_atom(c, atom("1"));
484 t.insert_atom(c, atom("2"));
485 let (s, _) = t.export_latex();
486 assert_eq!(s, "x^{12}");
487 }
488}