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::{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}
469
470impl NodeDoc {
471    fn slots(&self) -> Vec<&Vec<NodeDoc>> {
472        match self {
473            NodeDoc::Atom(_) | NodeDoc::HostBox { .. } => Vec::new(),
474            NodeDoc::Frac { num, den, .. } => vec![num, den],
475            NodeDoc::Script { base, sub, sup } => {
476                let mut v = vec![base];
477                v.extend(sub.iter());
478                v.extend(sup.iter());
479                v
480            }
481            NodeDoc::BigOp { lower, upper, .. } => vec![lower, upper],
482            NodeDoc::Sqrt { index, radicand } => vec![index, radicand],
483            NodeDoc::Delim { body, .. } => vec![body],
484            NodeDoc::Accent { base, .. } => vec![base],
485            NodeDoc::UnderOver { base, over, under, .. } => {
486                let mut v = vec![base];
487                v.extend(over.iter());
488                v.extend(under.iter());
489                v
490            }
491            NodeDoc::Styled { content, .. } => vec![content],
492            NodeDoc::Matrix { rows, .. } => rows.iter().flatten().collect(),
493        }
494    }
495
496    fn slots_mut(&mut self) -> Vec<&mut 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_mut());
503                v.extend(sup.iter_mut());
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_mut());
513                v.extend(under.iter_mut());
514                v
515            }
516            NodeDoc::Styled { content, .. } => vec![content],
517            NodeDoc::Matrix { rows, .. } => rows.iter_mut().flatten().collect(),
518        }
519    }
520
521    /// Whether the node owns slots, as opposed to atoms and host boxes.
522    pub(crate) fn is_structural(&self) -> bool {
523        !matches!(self, NodeDoc::Atom(_) | NodeDoc::HostBox { .. })
524    }
525}
526
527impl Tree {
528    /// The id free document form of the live tree.
529    pub(crate) fn to_doc(&self) -> Document {
530        Document::new(self.seq_to_doc(self.root()))
531    }
532
533    pub(crate) fn seq_to_doc(&self, seq: SeqId) -> Vec<NodeDoc> {
534        self.items(seq).iter().filter_map(|&n| self.node_to_doc(n)).collect()
535    }
536
537    pub(crate) fn node_to_doc(&self, node: NodeId) -> Option<NodeDoc> {
538        let s = |seq: SeqId| self.seq_to_doc(seq);
539        let o = |seq: Option<SeqId>| seq.map(|q| self.seq_to_doc(q));
540        Some(match self.kind(node)? {
541            Kind::Atom(sym) => NodeDoc::Atom(sym.clone()),
542            Kind::HostBox { token } => NodeDoc::HostBox { token: *token },
543            Kind::Frac { num, den, style } => NodeDoc::Frac { num: s(*num), den: s(*den), style: *style },
544            Kind::Script { base, sub, sup } => NodeDoc::Script { base: s(*base), sub: o(*sub), sup: o(*sup) },
545            Kind::BigOp { op, lower, upper } => {
546                NodeDoc::BigOp { op: op.clone(), lower: s(*lower), upper: s(*upper) }
547            }
548            Kind::Sqrt { index, radicand } => NodeDoc::Sqrt { index: s(*index), radicand: s(*radicand) },
549            Kind::Delim { open, close, body } => NodeDoc::Delim { open: *open, close: *close, body: s(*body) },
550            Kind::Accent { mark, base } => NodeDoc::Accent { mark: *mark, base: s(*base) },
551            Kind::UnderOver { base, over, under, over_deco, under_deco } => NodeDoc::UnderOver {
552                base: s(*base),
553                over: o(*over),
554                under: o(*under),
555                over_deco: *over_deco,
556                under_deco: *under_deco,
557            },
558            Kind::Styled { variant, content } => NodeDoc::Styled { variant: *variant, content: s(*content) },
559            Kind::Matrix { env, rows } => NodeDoc::Matrix {
560                env: *env,
561                rows: rows.iter().map(|row| row.iter().map(|&c| s(c)).collect()).collect(),
562            },
563        })
564    }
565
566    /// Rebuild a tree with fresh ids, dropping whatever cannot be built safely, callers validate first.
567    pub(crate) fn from_doc(doc: &Document) -> Self {
568        let mut t = Tree::new();
569        let root = t.root();
570        for d in doc.root() {
571            let at = t.len(root);
572            t.build_node(root, at, d, 0);
573        }
574        t
575    }
576
577    /// Build `d` into `seq` at `index`, where `depth` is the depth of `seq`, `None` when dropped.
578    pub(crate) fn build_node(&mut self, seq: SeqId, index: usize, d: &NodeDoc, depth: usize) -> Option<NodeId> {
579        let inner = depth + 1;
580        if d.is_structural() && inner > MAX_DEPTH {
581            return None;
582        }
583        let kind = match d {
584            NodeDoc::Atom(sym) => {
585                check_latex(&sym.latex).ok()?;
586                Kind::Atom(sym.clone())
587            }
588            NodeDoc::HostBox { token } if *token > MAX_HOST_TOKEN => return None,
589            NodeDoc::HostBox { token } => Kind::HostBox { token: *token },
590            NodeDoc::Frac { num, den, style } => Kind::Frac {
591                num: self.build_seq(num, inner),
592                den: self.build_seq(den, inner),
593                style: *style,
594            },
595            NodeDoc::Script { base, sub, sup } => {
596                let base = self.build_seq(base, inner);
597                let sub = sub.as_ref().map(|s| self.build_seq(s, inner));
598                let mut sup = sup.as_ref().map(|s| self.build_seq(s, inner));
599                if sub.is_none() && sup.is_none() {
600                    sup = Some(self.alloc_seq(None));
601                }
602                Kind::Script { base, sub, sup }
603            }
604            NodeDoc::BigOp { op, lower, upper } => {
605                check_latex(&op.latex).ok()?;
606                Kind::BigOp { op: op.clone(), lower: self.build_seq(lower, inner), upper: self.build_seq(upper, inner) }
607            }
608            NodeDoc::Sqrt { index, radicand } => Kind::Sqrt {
609                index: self.build_seq(index, inner),
610                radicand: self.build_seq(radicand, inner),
611            },
612            NodeDoc::Delim { open, close, body } => {
613                let fix = |c: char| if DELIMITERS.contains(&c) { c } else { '.' };
614                Kind::Delim { open: fix(*open), close: fix(*close), body: self.build_seq(body, inner) }
615            }
616            NodeDoc::Accent { mark, base } => Kind::Accent { mark: *mark, base: self.build_seq(base, inner) },
617            NodeDoc::UnderOver { base, over, under, over_deco, under_deco } => Kind::UnderOver {
618                base: self.build_seq(base, inner),
619                over: over.as_ref().map(|s| self.build_seq(s, inner)),
620                under: under.as_ref().map(|s| self.build_seq(s, inner)),
621                over_deco: *over_deco,
622                under_deco: *under_deco,
623            },
624            NodeDoc::Styled { variant, content } => {
625                Kind::Styled { variant: *variant, content: self.build_seq(content, inner) }
626            }
627            NodeDoc::Matrix { env, rows } => {
628                // Ragged rows are padded and an empty grid becomes a single empty cell.
629                let cols = rows.iter().map(Vec::len).max().unwrap_or(0).max(1);
630                let nrows = rows.len().max(1);
631                let mut grid = Vec::with_capacity(nrows);
632                for r in 0..nrows {
633                    let mut row = Vec::with_capacity(cols);
634                    for c in 0..cols {
635                        let cell = rows.get(r).and_then(|row| row.get(c)).map_or(&[][..], Vec::as_slice);
636                        row.push(self.build_seq(cell, inner));
637                    }
638                    grid.push(row);
639                }
640                Kind::Matrix { env: *env, rows: grid }
641            }
642        };
643        Some(self.place(seq, index, kind))
644    }
645
646    fn build_seq(&mut self, docs: &[NodeDoc], depth: usize) -> SeqId {
647        let seq = self.alloc_seq(None);
648        for d in docs {
649            let at = self.len(seq);
650            self.build_node(seq, at, d, depth);
651        }
652        seq
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use crate::model::MathClass;
660
661    fn atom(c: &str) -> NodeDoc {
662        NodeDoc::Atom(Symbol { latex: c.into(), class: MathClass::Ord })
663    }
664
665    fn every_kind() -> Document {
666        let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
667        Document::new(vec![
668            NodeDoc::Script { base: vec![atom("a")], sub: None, sup: Some(vec![atom("2")]) },
669            NodeDoc::Frac { num: vec![atom("x")], den: vec![], style: FracStyle::Bar },
670            NodeDoc::Sqrt { index: vec![], radicand: vec![atom("y")] },
671            NodeDoc::BigOp { op, lower: vec![], upper: vec![atom("n")] },
672            NodeDoc::Delim { open: '(', close: ')', body: vec![atom("z")] },
673            NodeDoc::Accent { mark: Mark::Hat, base: vec![atom("b")] },
674            NodeDoc::Styled { variant: Variant::Bold, content: vec![atom("c")] },
675            NodeDoc::UnderOver {
676                base: vec![atom("d")],
677                over: Some(vec![]),
678                under: None,
679                over_deco: Deco::Brace,
680                under_deco: Deco::None,
681            },
682            NodeDoc::Matrix { env: MatrixEnv::Pmatrix, rows: vec![vec![vec![atom("e")], vec![]], vec![vec![], vec![]]] },
683            NodeDoc::HostBox { token: 7 },
684        ])
685    }
686
687    #[test]
688    fn every_kind_round_trips_through_json_and_the_tree() {
689        let d1 = every_kind();
690        let json = serde_json::to_string(&d1).unwrap();
691        let decoded: Document = serde_json::from_str(&json).unwrap();
692        assert_eq!(decoded, d1);
693        assert_eq!(Tree::from_doc(&decoded).to_doc(), d1);
694    }
695
696    #[test]
697    fn serde_field_order_is_canonical() {
698        let pos = |s: &str, key: &str| s.find(&format!("\"{key}\"")).unwrap_or_else(|| panic!("missing {key} in {s}"));
699        let s = serde_json::to_string(&every_kind().root()[3]).unwrap();
700        assert!(pos(&s, "op") < pos(&s, "lower") && pos(&s, "lower") < pos(&s, "upper"), "{s}");
701        let doc = every_kind();
702        let uo = &doc.root()[7];
703        let s = serde_json::to_string(uo).unwrap();
704        let order = ["base", "over", "under", "over_deco", "under_deco"];
705        assert!(order.windows(2).all(|w| pos(&s, w[0]) < pos(&s, w[1])), "{s}");
706        // `Some` holding empty data and `None` survive distinctly.
707        let back: NodeDoc = serde_json::from_str(&s).unwrap();
708        assert_eq!(&back, uo);
709    }
710
711    #[test]
712    fn document_json_has_a_stable_shape() {
713        let doc = Document::new(vec![atom("x"), NodeDoc::HostBox { token: 17 }]);
714        let json = serde_json::to_string(&doc).unwrap();
715        assert_eq!(
716            json,
717            r#"{"version":1,"root":[{"type":"atom","data":{"latex":"x","class":"ord"}},{"type":"host_box","data":{"token":17}}]}"#
718        );
719    }
720
721    #[test]
722    fn unknown_document_version_is_rejected() {
723        let error = serde_json::from_str::<Document>(r#"{"version":2,"root":[]}"#).unwrap_err().to_string();
724        assert!(error.contains("unsupported document version 2"), "{error}");
725    }
726
727    fn reject(json: &str) -> String {
728        serde_json::from_str::<Document>(json).unwrap_err().to_string()
729    }
730
731    #[test]
732    fn deserialization_rejects_ragged_empty_and_scriptless_structures() {
733        let ragged = r#"{"version":1,"root":[{"type":"matrix","data":{"env":"matrix","rows":[[[],[]],[[]]]}}]}"#;
734        assert!(reject(ragged).contains("differ in length"));
735        let empty = r#"{"version":1,"root":[{"type":"matrix","data":{"env":"matrix","rows":[]}}]}"#;
736        assert!(reject(empty).contains("no cells"));
737        let script = r#"{"version":1,"root":[{"type":"script","data":{"base":[],"sub":null,"sup":null}}]}"#;
738        assert!(reject(script).contains("neither"));
739        let delim = r#"{"version":1,"root":[{"type":"delim","data":{"open":"x","close":")","body":[]}}]}"#;
740        assert!(reject(delim).contains("delimiter"));
741    }
742
743    #[test]
744    fn deserialization_rejects_unsafe_atom_latex() {
745        for bad in ["", "{", "}{", "%", "a$b", "#", "&", "\\", "\u{7}", "x\ny"] {
746            let doc = Document::new(vec![atom(bad)]);
747            assert!(doc.validate().is_err(), "{bad:?} should be rejected");
748        }
749        for good in ["x", "\\%", "\\{", "\\mathbb{R}", "\\text{\\textasciicircum}", "\\ "] {
750            assert_eq!(Document::new(vec![atom(good)]).validate(), Ok(()), "{good:?}");
751        }
752    }
753
754    fn nested(depth: usize) -> Document {
755        let mut nodes = vec![atom("x")];
756        for _ in 0..depth {
757            nodes = vec![NodeDoc::Delim { open: '(', close: ')', body: nodes }];
758        }
759        Document::new(nodes)
760    }
761
762    #[test]
763    fn nesting_is_capped_and_the_cap_survives_json() {
764        let deepest = nested(MAX_DEPTH);
765        assert_eq!(deepest.validate(), Ok(()));
766        assert_eq!(nested(MAX_DEPTH + 1).validate(), Err(DocumentError::TooDeep));
767        // A matrix costs the most JSON levels per slot, so a full depth matrix chain must still parse.
768        let mut nodes = vec![atom("x")];
769        for _ in 0..MAX_DEPTH {
770            nodes = vec![NodeDoc::Matrix { env: MatrixEnv::Matrix, rows: vec![vec![nodes]] }];
771        }
772        let json = serde_json::to_string(&Document::new(nodes)).unwrap();
773        let back: Document = serde_json::from_str(&json).unwrap();
774        assert_eq!(back.height(), MAX_DEPTH);
775    }
776
777    #[test]
778    fn repair_fixes_every_rule_and_reports_each_change() {
779        let mut doc = Document::new(vec![
780            NodeDoc::Matrix { env: MatrixEnv::Matrix, rows: vec![vec![vec![atom("a")], vec![]], vec![]] },
781            NodeDoc::Matrix { env: MatrixEnv::Matrix, rows: vec![] },
782            NodeDoc::Script { base: vec![atom("b")], sub: None, sup: None },
783            NodeDoc::Delim { open: 'x', close: ')', body: vec![] },
784            atom("%"),
785            NodeDoc::HostBox { token: MAX_HOST_TOKEN + 1 },
786            NodeDoc::HostBox { token: MAX_HOST_TOKEN },
787        ]);
788        let repairs = doc.repair();
789        assert_eq!(
790            repairs,
791            [
792                Repair::PaddedMatrix,
793                Repair::PaddedMatrix,
794                Repair::AddedSuperscript,
795                Repair::ReplacedDelimiter { found: 'x' },
796                Repair::DroppedSymbol { latex: "%".into() },
797                Repair::DroppedHostBox { token: MAX_HOST_TOKEN + 1 },
798            ]
799        );
800        assert_eq!(doc.validate(), Ok(()));
801        assert_eq!(doc.len(), 5);
802        assert_eq!(doc.repair(), []);
803        let mut deep = nested(MAX_DEPTH + 3);
804        assert_eq!(deep.repair(), [Repair::DroppedTooDeep]);
805        assert_eq!(deep.validate(), Ok(()));
806        assert_eq!(deep.height(), MAX_DEPTH);
807    }
808
809    #[test]
810    fn host_tokens_above_the_tex_integer_range_are_rejected() {
811        let doc = Document::new(vec![NodeDoc::HostBox { token: 1 << 31 }]);
812        assert_eq!(doc.validate(), Err(DocumentError::HostTokenTooLarge(1 << 31)));
813        let json = r#"{"version":1,"root":[{"type":"host_box","data":{"token":2147483648}}]}"#;
814        assert!(serde_json::from_str::<Document>(json).unwrap_err().to_string().contains("2147483648"));
815        assert_eq!(Document::new(vec![NodeDoc::HostBox { token: MAX_HOST_TOKEN }]).validate(), Ok(()));
816    }
817
818    #[test]
819    fn host_tokens_reach_nested_slots_and_can_be_remapped() {
820        let mut doc = Document::new(vec![
821            NodeDoc::HostBox { token: 3 },
822            NodeDoc::Frac { num: vec![NodeDoc::HostBox { token: 7 }], den: vec![], style: FracStyle::Bar },
823            NodeDoc::Matrix { env: MatrixEnv::Pmatrix, rows: vec![vec![vec![NodeDoc::HostBox { token: 9 }]]] },
824        ]);
825        assert_eq!(doc.host_tokens().into_iter().collect::<Vec<_>>(), vec![3, 7, 9]);
826        doc.map_host_tokens(|t| t + 100);
827        assert_eq!(doc.host_tokens().into_iter().collect::<Vec<_>>(), vec![103, 107, 109]);
828        let mut count = 0;
829        doc.visit(|_| count += 1);
830        assert_eq!(count, 5);
831    }
832
833    #[test]
834    fn to_tex_substitutes_host_box_content() {
835        let doc = Document::new(vec![atom("a"), NodeDoc::HostBox { token: 4 }, NodeDoc::HostBox { token: 5 }]);
836        assert_eq!(doc.to_tex(), "a\\hostbox{4}\\hostbox{5}");
837        let tex = doc.to_tex_with(|t| (t == 4).then(|| "\\square".to_string()));
838        assert_eq!(tex, "a\\square\\hostbox{5}");
839    }
840}