Skip to main content

mathtex_editor_core/
doc.rs

1//! The document and clipboard format, an id free nested tree rebuilt into slotmap storage on load.
2
3use std::collections::BTreeSet;
4use std::fmt;
5
6use serde::{de, Deserialize, Deserializer, Serialize};
7
8use crate::model::{Arrow, Deco, FracStyle, Kind, Mark, MatrixEnv, NodeId, SeqId, Symbol, Tree, Variant};
9
10/// The current serialized document format version.
11pub const DOCUMENT_VERSION: u32 = 1;
12
13/// Deepest allowed slot nesting, sized so a document stays under serde_json's recursion limit of 128.
14pub const MAX_DEPTH: usize = 20;
15
16/// Largest host box token, since TeX reads the `N` of `\hostbox{N}` as an integer of at most 2^31 - 1.
17pub const MAX_HOST_TOKEN: u32 = i32::MAX as u32;
18
19/// Delimiter characters a `Delim` node may carry, `.` is the invisible delimiter.
20pub(crate) const DELIMITERS: &[char] =
21    &['(', ')', '[', ']', '{', '}', '|', '‖', '/', '.', '⌈', '⌉', '⌊', '⌋', '⟨', '⟩'];
22
23/// A whole document, the unit of persistence and of the clipboard, see the README for its JSON shape.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "RawDocument")]
26pub struct Document {
27    version: u32,
28    root: Vec<NodeDoc>,
29}
30
31#[derive(Deserialize)]
32struct RawDocument {
33    #[serde(deserialize_with = "deserialize_version")]
34    version: u32,
35    root: Vec<NodeDoc>,
36}
37
38impl TryFrom<RawDocument> for Document {
39    type Error = DocumentError;
40
41    fn try_from(raw: RawDocument) -> Result<Self, Self::Error> {
42        let doc = Document { version: raw.version, root: raw.root };
43        doc.validate()?;
44        Ok(doc)
45    }
46}
47
48fn deserialize_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
49where
50    D: Deserializer<'de>,
51{
52    let version = u32::deserialize(deserializer)?;
53    if version == DOCUMENT_VERSION {
54        Ok(version)
55    } else {
56        Err(de::Error::custom(format_args!(
57            "unsupported document version {version}, expected {DOCUMENT_VERSION}"
58        )))
59    }
60}
61
62impl Default for Document {
63    fn default() -> Self {
64        Self::new(Vec::new())
65    }
66}
67
68impl Document {
69    /// Build a document holding `root` in the current format version.
70    pub fn new(root: Vec<NodeDoc>) -> Self {
71        Self { version: DOCUMENT_VERSION, root }
72    }
73
74    /// The serialized format version.
75    pub fn version(&self) -> u32 {
76        self.version
77    }
78
79    /// The nodes in the root sequence.
80    pub fn root(&self) -> &[NodeDoc] {
81        &self.root
82    }
83
84    /// Number of nodes in the root sequence.
85    pub fn len(&self) -> usize {
86        self.root.len()
87    }
88
89    /// Whether the root sequence is empty.
90    pub fn is_empty(&self) -> bool {
91        self.root.is_empty()
92    }
93
94    /// Consume the document and return its root sequence.
95    pub fn into_root(self) -> Vec<NodeDoc> {
96        self.root
97    }
98
99    /// Check every structural rule that deserialization enforces.
100    pub fn validate(&self) -> Result<(), DocumentError> {
101        validate_seq(&self.root, 0)
102    }
103
104    /// Call `f` on every node in document order, parents before their slots.
105    pub fn visit(&self, mut f: impl FnMut(&NodeDoc)) {
106        visit_nodes(&self.root, &mut f);
107    }
108
109    /// Fix everything [`Document::validate`] rejects and report each change, in document order.
110    pub fn repair(&mut self) -> Vec<Repair> {
111        let mut out = Vec::new();
112        repair_seq(&mut self.root, 0, &mut out);
113        out
114    }
115
116    /// Call `f` on every node in document order, [`Document::validate`] tells whether the result is still valid.
117    pub fn visit_mut(&mut self, mut f: impl FnMut(&mut NodeDoc)) {
118        visit_nodes_mut(&mut self.root, &mut f);
119    }
120
121    /// Every host box token in the document.
122    pub fn host_tokens(&self) -> BTreeSet<u32> {
123        let mut out = BTreeSet::new();
124        self.visit(|n| {
125            if let NodeDoc::HostBox { token } = n {
126                out.insert(*token);
127            }
128        });
129        out
130    }
131
132    /// Rewrite every host box token, for example after minting fresh tokens on paste.
133    pub fn map_host_tokens(&mut self, mut f: impl FnMut(u32) -> u32) {
134        self.visit_mut(|n| {
135            if let NodeDoc::HostBox { token } = n {
136                *token = f(*token);
137            }
138        });
139    }
140
141    /// Clean LaTeX with empty placeholders dropped, host boxes stay `\hostbox{N}`.
142    pub fn to_tex(&self) -> String {
143        self.to_tex_with(|_| None)
144    }
145
146    /// Clean LaTeX where `host_box` supplies each host box's content, `None` keeps `\hostbox{N}`.
147    pub fn to_tex_with(&self, mut host_box: impl FnMut(u32) -> Option<String>) -> String {
148        let tree = Tree::from_doc(self);
149        crate::export::clean_tex(&tree, Some(&mut host_box))
150    }
151
152    /// Extra slot levels the nodes add below the sequence they are pasted into.
153    pub(crate) fn height(&self) -> usize {
154        seq_height(&self.root)
155    }
156}
157
158/// A rule broken by a document.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum DocumentError {
161    /// An atom or big operator symbol whose LaTeX is unsafe to splice into the output.
162    InvalidSymbol {
163        /// The offending LaTeX.
164        latex: String,
165        /// What is wrong with it.
166        reason: &'static str,
167    },
168    /// A matrix without any row or column.
169    EmptyMatrix,
170    /// A matrix whose rows differ in length.
171    RaggedMatrix,
172    /// A script with neither a subscript nor a superscript.
173    EmptyScript,
174    /// Slots nest deeper than [`MAX_DEPTH`].
175    TooDeep,
176    /// A delimiter outside the supported set.
177    UnsupportedDelimiter(char),
178    /// A host box token above [`MAX_HOST_TOKEN`].
179    HostTokenTooLarge(u32),
180}
181
182impl fmt::Display for DocumentError {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match self {
185            DocumentError::InvalidSymbol { latex, reason } => write!(f, "symbol {latex:?} {reason}"),
186            DocumentError::EmptyMatrix => write!(f, "matrix has no cells"),
187            DocumentError::RaggedMatrix => write!(f, "matrix rows differ in length"),
188            DocumentError::EmptyScript => write!(f, "script has neither subscript nor superscript"),
189            DocumentError::TooDeep => write!(f, "slots nest deeper than {MAX_DEPTH}"),
190            DocumentError::UnsupportedDelimiter(c) => write!(f, "unsupported delimiter {c:?}"),
191            DocumentError::HostTokenTooLarge(t) => write!(f, "host box token {t} is above {MAX_HOST_TOKEN}"),
192        }
193    }
194}
195
196impl std::error::Error for DocumentError {}
197
198/// One change [`Document::repair`] made.
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum Repair {
201    /// Dropped an atom or big operator whose symbol LaTeX is unsafe.
202    DroppedSymbol {
203        /// The dropped LaTeX.
204        latex: String,
205    },
206    /// Dropped a structure whose slots would nest deeper than [`MAX_DEPTH`].
207    DroppedTooDeep,
208    /// Dropped a host box whose token is above [`MAX_HOST_TOKEN`].
209    DroppedHostBox {
210        /// The dropped token.
211        token: u32,
212    },
213    /// Gave a script with neither a subscript nor a superscript an empty superscript.
214    AddedSuperscript,
215    /// Replaced an unsupported delimiter with the invisible `.`.
216    ReplacedDelimiter {
217        /// The unsupported delimiter.
218        found: char,
219    },
220    /// Padded ragged matrix rows with empty cells, or gave a matrix without cells one empty cell.
221    PaddedMatrix,
222}
223
224impl fmt::Display for Repair {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            Repair::DroppedSymbol { latex } => write!(f, "dropped symbol {latex:?}"),
228            Repair::DroppedTooDeep => write!(f, "dropped a structure nested deeper than {MAX_DEPTH}"),
229            Repair::DroppedHostBox { token } => write!(f, "dropped host box token {token}"),
230            Repair::AddedSuperscript => write!(f, "gave a script without scripts an empty superscript"),
231            Repair::ReplacedDelimiter { found } => write!(f, "replaced unsupported delimiter {found:?}"),
232            Repair::PaddedMatrix => write!(f, "padded a matrix to a rectangle"),
233        }
234    }
235}
236
237fn repair_seq(nodes: &mut Vec<NodeDoc>, depth: usize, out: &mut Vec<Repair>) {
238    nodes.retain_mut(|n| repair_node(n, depth, out));
239}
240
241/// Repair one node in a sequence at `depth` and its slots, `false` drops it.
242fn repair_node(n: &mut NodeDoc, depth: usize, out: &mut Vec<Repair>) -> bool {
243    if n.is_structural() && depth + 1 > MAX_DEPTH {
244        out.push(Repair::DroppedTooDeep);
245        return false;
246    }
247    match n {
248        NodeDoc::Atom(s) | NodeDoc::BigOp { op: s, .. } if check_latex(&s.latex).is_err() => {
249            out.push(Repair::DroppedSymbol { latex: s.latex.clone() });
250            return false;
251        }
252        NodeDoc::HostBox { token } if *token > MAX_HOST_TOKEN => {
253            out.push(Repair::DroppedHostBox { token: *token });
254            return false;
255        }
256        NodeDoc::Script { sub: None, sup: sup @ None, .. } => {
257            *sup = Some(Vec::new());
258            out.push(Repair::AddedSuperscript);
259        }
260        NodeDoc::Delim { open, close, .. } => {
261            for c in [open, close] {
262                if !DELIMITERS.contains(&*c) {
263                    out.push(Repair::ReplacedDelimiter { found: *c });
264                    *c = '.';
265                }
266            }
267        }
268        NodeDoc::Matrix { rows, .. } => {
269            let cols = rows.iter().map(Vec::len).max().unwrap_or(0).max(1);
270            if rows.is_empty() || rows.iter().any(|r| r.len() != cols) {
271                rows.resize_with(rows.len().max(1), Vec::new);
272                for row in rows.iter_mut() {
273                    row.resize_with(cols, Vec::new);
274                }
275                out.push(Repair::PaddedMatrix);
276            }
277        }
278        _ => {}
279    }
280    for slot in n.slots_mut() {
281        repair_seq(slot, depth + 1, out);
282    }
283    true
284}
285
286/// Reject LaTeX that could escape its atom: stray braces, comment or math shift characters, controls.
287pub(crate) fn check_latex(latex: &str) -> Result<(), &'static str> {
288    if latex.is_empty() {
289        return Err("is empty");
290    }
291    let mut depth = 0usize;
292    let mut chars = latex.chars();
293    while let Some(c) = chars.next() {
294        if c.is_control() {
295            return Err("contains a control character");
296        }
297        match c {
298            '\\' => match chars.next() {
299                None => return Err("ends in a lone backslash"),
300                Some(n) if n.is_control() => return Err("contains a control character"),
301                Some(_) => {}
302            },
303            '{' => depth += 1,
304            '}' => depth = depth.checked_sub(1).ok_or("has unbalanced braces")?,
305            '%' | '$' | '#' | '&' => return Err("contains an unescaped % $ # or &"),
306            _ => {}
307        }
308    }
309    if depth == 0 { Ok(()) } else { Err("has unbalanced braces") }
310}
311
312fn check_symbol(s: &Symbol) -> Result<(), DocumentError> {
313    check_latex(&s.latex).map_err(|reason| DocumentError::InvalidSymbol { latex: s.latex.clone(), reason })
314}
315
316fn validate_seq(nodes: &[NodeDoc], depth: usize) -> Result<(), DocumentError> {
317    for n in nodes {
318        match n {
319            NodeDoc::Atom(s) => check_symbol(s)?,
320            NodeDoc::BigOp { op, .. } => check_symbol(op)?,
321            NodeDoc::HostBox { token } if *token > MAX_HOST_TOKEN => {
322                return Err(DocumentError::HostTokenTooLarge(*token));
323            }
324            NodeDoc::Script { sub: None, sup: None, .. } => return Err(DocumentError::EmptyScript),
325            NodeDoc::Delim { open, close, .. } => {
326                for c in [*open, *close] {
327                    if !DELIMITERS.contains(&c) {
328                        return Err(DocumentError::UnsupportedDelimiter(c));
329                    }
330                }
331            }
332            NodeDoc::Matrix { rows, .. } => {
333                let cols = rows.first().map_or(0, Vec::len);
334                if cols == 0 {
335                    return Err(DocumentError::EmptyMatrix);
336                }
337                if rows.iter().any(|r| r.len() != cols) {
338                    return Err(DocumentError::RaggedMatrix);
339                }
340            }
341            _ => {}
342        }
343        let slots = n.slots();
344        if !slots.is_empty() && depth + 1 > MAX_DEPTH {
345            return Err(DocumentError::TooDeep);
346        }
347        for s in slots {
348            validate_seq(s, depth + 1)?;
349        }
350    }
351    Ok(())
352}
353
354fn seq_height(nodes: &[NodeDoc]) -> usize {
355    nodes
356        .iter()
357        .flat_map(|n| n.slots().into_iter().map(|s| 1 + seq_height(s)))
358        .max()
359        .unwrap_or(0)
360}
361
362fn visit_nodes(nodes: &[NodeDoc], f: &mut dyn FnMut(&NodeDoc)) {
363    for n in nodes {
364        f(n);
365        for s in n.slots() {
366            visit_nodes(s, f);
367        }
368    }
369}
370
371fn visit_nodes_mut(nodes: &mut [NodeDoc], f: &mut dyn FnMut(&mut NodeDoc)) {
372    for n in nodes {
373        f(n);
374        for s in n.slots_mut() {
375            visit_nodes_mut(s, f);
376        }
377    }
378}
379
380/// The serialized form of one node.
381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(tag = "type", content = "data", rename_all = "snake_case")]
383pub enum NodeDoc {
384    /// A single leaf token.
385    Atom(Symbol),
386    /// A fraction.
387    Frac {
388        /// Numerator nodes.
389        num: Vec<NodeDoc>,
390        /// Denominator nodes.
391        den: Vec<NodeDoc>,
392        /// Visual style.
393        style: FracStyle,
394    },
395    /// Subscript and superscript on a base, at least one of the two is present.
396    Script {
397        /// Base nodes.
398        base: Vec<NodeDoc>,
399        /// Subscript nodes when present.
400        sub: Option<Vec<NodeDoc>>,
401        /// Superscript nodes when present.
402        sup: Option<Vec<NodeDoc>>,
403    },
404    /// A big operator with limits.
405    BigOp {
406        /// The operator symbol.
407        op: Symbol,
408        /// Lower limit nodes.
409        lower: Vec<NodeDoc>,
410        /// Upper limit nodes.
411        upper: Vec<NodeDoc>,
412    },
413    /// A radical.
414    Sqrt {
415        /// Degree nodes, empty for a square root.
416        index: Vec<NodeDoc>,
417        /// Radicand nodes.
418        radicand: Vec<NodeDoc>,
419    },
420    /// Stretchy delimiters around a body.
421    Delim {
422        /// Opening delimiter character.
423        open: char,
424        /// Closing delimiter character.
425        close: char,
426        /// Body nodes.
427        body: Vec<NodeDoc>,
428    },
429    /// An accent over a base.
430    Accent {
431        /// The accent mark.
432        mark: Mark,
433        /// Base nodes.
434        base: Vec<NodeDoc>,
435    },
436    /// A base with optional labels above and below.
437    UnderOver {
438        /// Base nodes.
439        base: Vec<NodeDoc>,
440        /// Label above when present.
441        over: Option<Vec<NodeDoc>>,
442        /// Label below when present.
443        under: Option<Vec<NodeDoc>>,
444        /// Decoration between the base and the label above.
445        over_deco: Deco,
446        /// Decoration between the base and the label below.
447        under_deco: Deco,
448    },
449    /// Content in a font variant or in text mode.
450    Styled {
451        /// The variant.
452        variant: Variant,
453        /// Content nodes.
454        content: Vec<NodeDoc>,
455    },
456    /// An opaque host owned object.
457    HostBox {
458        /// The host minted token.
459        token: u32,
460    },
461    /// A rectangular grid of cells.
462    Matrix {
463        /// The environment.
464        env: MatrixEnv,
465        /// Cells by row, each cell a node list.
466        rows: Vec<Vec<Vec<NodeDoc>>>,
467    },
468    /// Two terms joined by a ratio colon spaced as a relation.
469    Ratio {
470        /// First term nodes.
471        left: Vec<NodeDoc>,
472        /// Second term nodes.
473        right: Vec<NodeDoc>,
474    },
475    /// An evaluation bar after a body, as in `F(x)` evaluated from `a` to `b`.
476    EvalBar {
477        /// Body nodes left of the bar.
478        body: Vec<NodeDoc>,
479        /// Lower limit nodes.
480        lower: Vec<NodeDoc>,
481        /// Upper limit nodes.
482        upper: Vec<NodeDoc>,
483    },
484    /// An extensible arrow with labels above and below.
485    LabeledArrow {
486        /// The arrow drawn.
487        arrow: Arrow,
488        /// Label above nodes.
489        over: Vec<NodeDoc>,
490        /// Label below nodes, empty for none.
491        under: Vec<NodeDoc>,
492    },
493}
494
495impl NodeDoc {
496    fn slots(&self) -> Vec<&Vec<NodeDoc>> {
497        match self {
498            NodeDoc::Atom(_) | NodeDoc::HostBox { .. } => Vec::new(),
499            NodeDoc::Frac { num, den, .. } => vec![num, den],
500            NodeDoc::Script { base, sub, sup } => {
501                let mut v = vec![base];
502                v.extend(sub.iter());
503                v.extend(sup.iter());
504                v
505            }
506            NodeDoc::BigOp { lower, upper, .. } => vec![lower, upper],
507            NodeDoc::Sqrt { index, radicand } => vec![index, radicand],
508            NodeDoc::Delim { body, .. } => vec![body],
509            NodeDoc::Accent { base, .. } => vec![base],
510            NodeDoc::UnderOver { base, over, under, .. } => {
511                let mut v = vec![base];
512                v.extend(over.iter());
513                v.extend(under.iter());
514                v
515            }
516            NodeDoc::Styled { content, .. } => vec![content],
517            NodeDoc::Matrix { rows, .. } => rows.iter().flatten().collect(),
518            NodeDoc::Ratio { left, right } => vec![left, right],
519            NodeDoc::EvalBar { body, lower, upper } => vec![body, lower, upper],
520            NodeDoc::LabeledArrow { over, under, .. } => vec![over, under],
521        }
522    }
523
524    pub(crate) fn slots_mut(&mut self) -> Vec<&mut Vec<NodeDoc>> {
525        match self {
526            NodeDoc::Atom(_) | NodeDoc::HostBox { .. } => Vec::new(),
527            NodeDoc::Frac { num, den, .. } => vec![num, den],
528            NodeDoc::Script { base, sub, sup } => {
529                let mut v = vec![base];
530                v.extend(sub.iter_mut());
531                v.extend(sup.iter_mut());
532                v
533            }
534            NodeDoc::BigOp { lower, upper, .. } => vec![lower, upper],
535            NodeDoc::Sqrt { index, radicand } => vec![index, radicand],
536            NodeDoc::Delim { body, .. } => vec![body],
537            NodeDoc::Accent { base, .. } => vec![base],
538            NodeDoc::UnderOver { base, over, under, .. } => {
539                let mut v = vec![base];
540                v.extend(over.iter_mut());
541                v.extend(under.iter_mut());
542                v
543            }
544            NodeDoc::Styled { content, .. } => vec![content],
545            NodeDoc::Matrix { rows, .. } => rows.iter_mut().flatten().collect(),
546            NodeDoc::Ratio { left, right } => vec![left, right],
547            NodeDoc::EvalBar { body, lower, upper } => vec![body, lower, upper],
548            NodeDoc::LabeledArrow { over, under, .. } => vec![over, under],
549        }
550    }
551
552    /// Whether the node owns slots, as opposed to atoms and host boxes.
553    pub(crate) fn is_structural(&self) -> bool {
554        !matches!(self, NodeDoc::Atom(_) | NodeDoc::HostBox { .. })
555    }
556}
557
558impl Tree {
559    /// The id free document form of the live tree.
560    pub(crate) fn to_doc(&self) -> Document {
561        Document::new(self.seq_to_doc(self.root()))
562    }
563
564    pub(crate) fn seq_to_doc(&self, seq: SeqId) -> Vec<NodeDoc> {
565        self.items(seq).iter().filter_map(|&n| self.node_to_doc(n)).collect()
566    }
567
568    pub(crate) fn node_to_doc(&self, node: NodeId) -> Option<NodeDoc> {
569        let s = |seq: SeqId| self.seq_to_doc(seq);
570        let o = |seq: Option<SeqId>| seq.map(|q| self.seq_to_doc(q));
571        Some(match self.kind(node)? {
572            Kind::Atom(sym) => NodeDoc::Atom(sym.clone()),
573            Kind::HostBox { token } => NodeDoc::HostBox { token: *token },
574            Kind::Frac { num, den, style } => NodeDoc::Frac { num: s(*num), den: s(*den), style: *style },
575            Kind::Script { base, sub, sup } => NodeDoc::Script { base: s(*base), sub: o(*sub), sup: o(*sup) },
576            Kind::BigOp { op, lower, upper } => {
577                NodeDoc::BigOp { op: op.clone(), lower: s(*lower), upper: s(*upper) }
578            }
579            Kind::Sqrt { index, radicand } => NodeDoc::Sqrt { index: s(*index), radicand: s(*radicand) },
580            Kind::Delim { open, close, body } => NodeDoc::Delim { open: *open, close: *close, body: s(*body) },
581            Kind::Accent { mark, base } => NodeDoc::Accent { mark: *mark, base: s(*base) },
582            Kind::UnderOver { base, over, under, over_deco, under_deco } => NodeDoc::UnderOver {
583                base: s(*base),
584                over: o(*over),
585                under: o(*under),
586                over_deco: *over_deco,
587                under_deco: *under_deco,
588            },
589            Kind::Styled { variant, content } => NodeDoc::Styled { variant: *variant, content: s(*content) },
590            Kind::Matrix { env, rows } => NodeDoc::Matrix {
591                env: *env,
592                rows: rows.iter().map(|row| row.iter().map(|&c| s(c)).collect()).collect(),
593            },
594            Kind::Ratio { left, right } => NodeDoc::Ratio { left: s(*left), right: s(*right) },
595            Kind::EvalBar { body, lower, upper } => {
596                NodeDoc::EvalBar { body: s(*body), lower: s(*lower), upper: s(*upper) }
597            }
598            Kind::LabeledArrow { arrow, over, under } => {
599                NodeDoc::LabeledArrow { arrow: *arrow, over: s(*over), under: s(*under) }
600            }
601        })
602    }
603
604    /// Rebuild a tree with fresh ids, dropping whatever cannot be built safely, callers validate first.
605    pub(crate) fn from_doc(doc: &Document) -> Self {
606        let mut t = Tree::new();
607        let root = t.root();
608        for d in doc.root() {
609            let at = t.len(root);
610            t.build_node(root, at, d, 0);
611        }
612        t
613    }
614
615    /// Build `d` into `seq` at `index`, where `depth` is the depth of `seq`, `None` when dropped.
616    pub(crate) fn build_node(&mut self, seq: SeqId, index: usize, d: &NodeDoc, depth: usize) -> Option<NodeId> {
617        let inner = depth + 1;
618        if d.is_structural() && inner > MAX_DEPTH {
619            return None;
620        }
621        let kind = match d {
622            NodeDoc::Atom(sym) => {
623                check_latex(&sym.latex).ok()?;
624                Kind::Atom(sym.clone())
625            }
626            NodeDoc::HostBox { token } if *token > MAX_HOST_TOKEN => return None,
627            NodeDoc::HostBox { token } => Kind::HostBox { token: *token },
628            NodeDoc::Frac { num, den, style } => Kind::Frac {
629                num: self.build_seq(num, inner),
630                den: self.build_seq(den, inner),
631                style: *style,
632            },
633            NodeDoc::Script { base, sub, sup } => {
634                let base = self.build_seq(base, inner);
635                let sub = sub.as_ref().map(|s| self.build_seq(s, inner));
636                let mut sup = sup.as_ref().map(|s| self.build_seq(s, inner));
637                if sub.is_none() && sup.is_none() {
638                    sup = Some(self.alloc_seq(None));
639                }
640                Kind::Script { base, sub, sup }
641            }
642            NodeDoc::BigOp { op, lower, upper } => {
643                check_latex(&op.latex).ok()?;
644                Kind::BigOp { op: op.clone(), lower: self.build_seq(lower, inner), upper: self.build_seq(upper, inner) }
645            }
646            NodeDoc::Sqrt { index, radicand } => Kind::Sqrt {
647                index: self.build_seq(index, inner),
648                radicand: self.build_seq(radicand, inner),
649            },
650            NodeDoc::Delim { open, close, body } => {
651                let fix = |c: char| if DELIMITERS.contains(&c) { c } else { '.' };
652                Kind::Delim { open: fix(*open), close: fix(*close), body: self.build_seq(body, inner) }
653            }
654            NodeDoc::Accent { mark, base } => Kind::Accent { mark: *mark, base: self.build_seq(base, inner) },
655            NodeDoc::UnderOver { base, over, under, over_deco, under_deco } => Kind::UnderOver {
656                base: self.build_seq(base, inner),
657                over: over.as_ref().map(|s| self.build_seq(s, inner)),
658                under: under.as_ref().map(|s| self.build_seq(s, inner)),
659                over_deco: *over_deco,
660                under_deco: *under_deco,
661            },
662            NodeDoc::Styled { variant, content } => {
663                Kind::Styled { variant: *variant, content: self.build_seq(content, inner) }
664            }
665            NodeDoc::Matrix { env, rows } => {
666                // Ragged rows are padded and an empty grid becomes a single empty cell.
667                let cols = rows.iter().map(Vec::len).max().unwrap_or(0).max(1);
668                let nrows = rows.len().max(1);
669                let mut grid = Vec::with_capacity(nrows);
670                for r in 0..nrows {
671                    let mut row = Vec::with_capacity(cols);
672                    for c in 0..cols {
673                        let cell = rows.get(r).and_then(|row| row.get(c)).map_or(&[][..], Vec::as_slice);
674                        row.push(self.build_seq(cell, inner));
675                    }
676                    grid.push(row);
677                }
678                Kind::Matrix { env: *env, rows: grid }
679            }
680            NodeDoc::Ratio { left, right } => {
681                Kind::Ratio { left: self.build_seq(left, inner), right: self.build_seq(right, inner) }
682            }
683            NodeDoc::EvalBar { body, lower, upper } => Kind::EvalBar {
684                body: self.build_seq(body, inner),
685                lower: self.build_seq(lower, inner),
686                upper: self.build_seq(upper, inner),
687            },
688            NodeDoc::LabeledArrow { arrow, over, under } => Kind::LabeledArrow {
689                arrow: *arrow,
690                over: self.build_seq(over, inner),
691                under: self.build_seq(under, inner),
692            },
693        };
694        Some(self.place(seq, index, kind))
695    }
696
697    fn build_seq(&mut self, docs: &[NodeDoc], depth: usize) -> SeqId {
698        let seq = self.alloc_seq(None);
699        for d in docs {
700            let at = self.len(seq);
701            self.build_node(seq, at, d, depth);
702        }
703        seq
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710    use crate::model::MathClass;
711
712    fn atom(c: &str) -> NodeDoc {
713        NodeDoc::Atom(Symbol { latex: c.into(), class: MathClass::Ord })
714    }
715
716    fn every_kind() -> Document {
717        let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
718        Document::new(vec![
719            NodeDoc::Script { base: vec![atom("a")], sub: None, sup: Some(vec![atom("2")]) },
720            NodeDoc::Frac { num: vec![atom("x")], den: vec![], style: FracStyle::Bar },
721            NodeDoc::Sqrt { index: vec![], radicand: vec![atom("y")] },
722            NodeDoc::BigOp { op, lower: vec![], upper: vec![atom("n")] },
723            NodeDoc::Delim { open: '(', close: ')', body: vec![atom("z")] },
724            NodeDoc::Accent { mark: Mark::Hat, base: vec![atom("b")] },
725            NodeDoc::Styled { variant: Variant::Bold, content: vec![atom("c")] },
726            NodeDoc::UnderOver {
727                base: vec![atom("d")],
728                over: Some(vec![]),
729                under: None,
730                over_deco: Deco::Brace,
731                under_deco: Deco::None,
732            },
733            NodeDoc::Matrix { env: MatrixEnv::Pmatrix, rows: vec![vec![vec![atom("e")], vec![]], vec![vec![], vec![]]] },
734            NodeDoc::HostBox { token: 7 },
735            NodeDoc::Ratio { left: vec![atom("f")], right: vec![] },
736            NodeDoc::EvalBar { body: vec![atom("g")], lower: vec![], upper: vec![atom("h")] },
737            NodeDoc::LabeledArrow { arrow: Arrow::Left, over: vec![atom("k")], under: vec![] },
738        ])
739    }
740
741    #[test]
742    fn every_kind_round_trips_through_json_and_the_tree() {
743        let d1 = every_kind();
744        let json = serde_json::to_string(&d1).unwrap();
745        let decoded: Document = serde_json::from_str(&json).unwrap();
746        assert_eq!(decoded, d1);
747        assert_eq!(Tree::from_doc(&decoded).to_doc(), d1);
748    }
749
750    #[test]
751    fn serde_field_order_is_canonical() {
752        let pos = |s: &str, key: &str| s.find(&format!("\"{key}\"")).unwrap_or_else(|| panic!("missing {key} in {s}"));
753        let s = serde_json::to_string(&every_kind().root()[3]).unwrap();
754        assert!(pos(&s, "op") < pos(&s, "lower") && pos(&s, "lower") < pos(&s, "upper"), "{s}");
755        let doc = every_kind();
756        let uo = &doc.root()[7];
757        let s = serde_json::to_string(uo).unwrap();
758        let order = ["base", "over", "under", "over_deco", "under_deco"];
759        assert!(order.windows(2).all(|w| pos(&s, w[0]) < pos(&s, w[1])), "{s}");
760        // `Some` holding empty data and `None` survive distinctly.
761        let back: NodeDoc = serde_json::from_str(&s).unwrap();
762        assert_eq!(&back, uo);
763    }
764
765    #[test]
766    fn document_json_has_a_stable_shape() {
767        let doc = Document::new(vec![atom("x"), NodeDoc::HostBox { token: 17 }]);
768        let json = serde_json::to_string(&doc).unwrap();
769        assert_eq!(
770            json,
771            r#"{"version":1,"root":[{"type":"atom","data":{"latex":"x","class":"ord"}},{"type":"host_box","data":{"token":17}}]}"#
772        );
773    }
774
775    #[test]
776    fn new_kinds_only_add_to_the_json_shape() {
777        let ratio = r#"{"type":"ratio","data":{"left":[],"right":[]}}"#;
778        let bar = r#"{"type":"eval_bar","data":{"body":[],"lower":[],"upper":[]}}"#;
779        let arrow = r#"{"type":"labeled_arrow","data":{"arrow":"right","over":[],"under":[]}}"#;
780        let ring = r#"{"type":"accent","data":{"mark":"ring","base":[]}}"#;
781        let json = format!(r#"{{"version":1,"root":[{ratio},{bar},{arrow},{ring}]}}"#);
782        let doc: Document = serde_json::from_str(&json).unwrap();
783        assert_eq!(serde_json::to_string(&doc).unwrap(), json);
784    }
785
786    #[test]
787    fn unknown_document_version_is_rejected() {
788        let error = serde_json::from_str::<Document>(r#"{"version":2,"root":[]}"#).unwrap_err().to_string();
789        assert!(error.contains("unsupported document version 2"), "{error}");
790    }
791
792    fn reject(json: &str) -> String {
793        serde_json::from_str::<Document>(json).unwrap_err().to_string()
794    }
795
796    #[test]
797    fn deserialization_rejects_ragged_empty_and_scriptless_structures() {
798        let ragged = r#"{"version":1,"root":[{"type":"matrix","data":{"env":"matrix","rows":[[[],[]],[[]]]}}]}"#;
799        assert!(reject(ragged).contains("differ in length"));
800        let empty = r#"{"version":1,"root":[{"type":"matrix","data":{"env":"matrix","rows":[]}}]}"#;
801        assert!(reject(empty).contains("no cells"));
802        let script = r#"{"version":1,"root":[{"type":"script","data":{"base":[],"sub":null,"sup":null}}]}"#;
803        assert!(reject(script).contains("neither"));
804        let delim = r#"{"version":1,"root":[{"type":"delim","data":{"open":"x","close":")","body":[]}}]}"#;
805        assert!(reject(delim).contains("delimiter"));
806    }
807
808    #[test]
809    fn deserialization_rejects_unsafe_atom_latex() {
810        for bad in ["", "{", "}{", "%", "a$b", "#", "&", "\\", "\u{7}", "x\ny"] {
811            let doc = Document::new(vec![atom(bad)]);
812            assert!(doc.validate().is_err(), "{bad:?} should be rejected");
813        }
814        for good in ["x", "\\%", "\\{", "\\mathbb{R}", "\\text{\\textasciicircum}", "\\ "] {
815            assert_eq!(Document::new(vec![atom(good)]).validate(), Ok(()), "{good:?}");
816        }
817    }
818
819    fn nested(depth: usize) -> Document {
820        let mut nodes = vec![atom("x")];
821        for _ in 0..depth {
822            nodes = vec![NodeDoc::Delim { open: '(', close: ')', body: nodes }];
823        }
824        Document::new(nodes)
825    }
826
827    #[test]
828    fn nesting_is_capped_and_the_cap_survives_json() {
829        let deepest = nested(MAX_DEPTH);
830        assert_eq!(deepest.validate(), Ok(()));
831        assert_eq!(nested(MAX_DEPTH + 1).validate(), Err(DocumentError::TooDeep));
832        // A matrix costs the most JSON levels per slot, so a full depth matrix chain must still parse.
833        let mut nodes = vec![atom("x")];
834        for _ in 0..MAX_DEPTH {
835            nodes = vec![NodeDoc::Matrix { env: MatrixEnv::Matrix, rows: vec![vec![nodes]] }];
836        }
837        let json = serde_json::to_string(&Document::new(nodes)).unwrap();
838        let back: Document = serde_json::from_str(&json).unwrap();
839        assert_eq!(back.height(), MAX_DEPTH);
840    }
841
842    #[test]
843    fn repair_fixes_every_rule_and_reports_each_change() {
844        let mut doc = Document::new(vec![
845            NodeDoc::Matrix { env: MatrixEnv::Matrix, rows: vec![vec![vec![atom("a")], vec![]], vec![]] },
846            NodeDoc::Matrix { env: MatrixEnv::Matrix, rows: vec![] },
847            NodeDoc::Script { base: vec![atom("b")], sub: None, sup: None },
848            NodeDoc::Delim { open: 'x', close: ')', body: vec![] },
849            atom("%"),
850            NodeDoc::HostBox { token: MAX_HOST_TOKEN + 1 },
851            NodeDoc::HostBox { token: MAX_HOST_TOKEN },
852        ]);
853        let repairs = doc.repair();
854        assert_eq!(
855            repairs,
856            [
857                Repair::PaddedMatrix,
858                Repair::PaddedMatrix,
859                Repair::AddedSuperscript,
860                Repair::ReplacedDelimiter { found: 'x' },
861                Repair::DroppedSymbol { latex: "%".into() },
862                Repair::DroppedHostBox { token: MAX_HOST_TOKEN + 1 },
863            ]
864        );
865        assert_eq!(doc.validate(), Ok(()));
866        assert_eq!(doc.len(), 5);
867        assert_eq!(doc.repair(), []);
868        let mut deep = nested(MAX_DEPTH + 3);
869        assert_eq!(deep.repair(), [Repair::DroppedTooDeep]);
870        assert_eq!(deep.validate(), Ok(()));
871        assert_eq!(deep.height(), MAX_DEPTH);
872    }
873
874    #[test]
875    fn host_tokens_above_the_tex_integer_range_are_rejected() {
876        let doc = Document::new(vec![NodeDoc::HostBox { token: 1 << 31 }]);
877        assert_eq!(doc.validate(), Err(DocumentError::HostTokenTooLarge(1 << 31)));
878        let json = r#"{"version":1,"root":[{"type":"host_box","data":{"token":2147483648}}]}"#;
879        assert!(serde_json::from_str::<Document>(json).unwrap_err().to_string().contains("2147483648"));
880        assert_eq!(Document::new(vec![NodeDoc::HostBox { token: MAX_HOST_TOKEN }]).validate(), Ok(()));
881    }
882
883    #[test]
884    fn host_tokens_reach_nested_slots_and_can_be_remapped() {
885        let mut doc = Document::new(vec![
886            NodeDoc::HostBox { token: 3 },
887            NodeDoc::Frac { num: vec![NodeDoc::HostBox { token: 7 }], den: vec![], style: FracStyle::Bar },
888            NodeDoc::Matrix { env: MatrixEnv::Pmatrix, rows: vec![vec![vec![NodeDoc::HostBox { token: 9 }]]] },
889        ]);
890        assert_eq!(doc.host_tokens().into_iter().collect::<Vec<_>>(), vec![3, 7, 9]);
891        doc.map_host_tokens(|t| t + 100);
892        assert_eq!(doc.host_tokens().into_iter().collect::<Vec<_>>(), vec![103, 107, 109]);
893        let mut count = 0;
894        doc.visit(|_| count += 1);
895        assert_eq!(count, 5);
896    }
897
898    #[test]
899    fn to_tex_substitutes_host_box_content() {
900        let doc = Document::new(vec![atom("a"), NodeDoc::HostBox { token: 4 }, NodeDoc::HostBox { token: 5 }]);
901        assert_eq!(doc.to_tex(), "a\\hostbox{4}\\hostbox{5}");
902        let tex = doc.to_tex_with(|t| (t == 4).then(|| "\\square".to_string()));
903        assert_eq!(tex, "a\\square\\hostbox{5}");
904    }
905}