Skip to main content

mathtex_editor_core/
doc.rs

1//! The document and clipboard format is an id-free nested tree rebuilt into slotmap storage on load.
2
3use serde::{de, Deserialize, Deserializer, Serialize};
4
5use crate::model::{Deco, FracStyle, Kind, Mark, MatrixEnv, Node, NodeId, SeqId, Symbol, Tree, Variant};
6
7/// The current serialized document format version.
8pub const DOCUMENT_VERSION: u32 = 1;
9
10/// A whole document on disk.
11///
12/// The live editor uses slotmap ids and parent links. This type deliberately contains neither,
13/// making the persisted representation deterministic and independent of allocation history.
14/// For example, an `x` atom is represented in JSON as:
15///
16/// ```text
17/// {"version":1,"root":[{"type":"atom","data":{"latex":"x","class":"ord"}}]}
18/// ```
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Document {
21    #[serde(deserialize_with = "deserialize_version")]
22    version: u32,
23    /// The nodes in the document's root sequence.
24    pub root: Vec<NodeDoc>,
25}
26
27impl Document {
28    /// Build a document containing `root` using the current format version.
29    pub fn new(root: Vec<NodeDoc>) -> Self {
30        Self {
31            version: DOCUMENT_VERSION,
32            root,
33        }
34    }
35
36    /// Return the serialized format version.
37    pub fn version(&self) -> u32 {
38        self.version
39    }
40
41    /// Return the nodes in the root sequence.
42    pub fn root(&self) -> &[NodeDoc] {
43        &self.root
44    }
45
46    /// Return the number of nodes in the root sequence.
47    pub fn len(&self) -> usize {
48        self.root.len()
49    }
50
51    /// Return whether the root sequence contains no nodes.
52    pub fn is_empty(&self) -> bool {
53        self.root.is_empty()
54    }
55
56    /// Consume the document and return its root sequence.
57    pub fn into_root(self) -> Vec<NodeDoc> {
58        self.root
59    }
60}
61
62impl Default for Document {
63    fn default() -> Self {
64        Self::new(Vec::new())
65    }
66}
67
68fn deserialize_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
69where
70    D: Deserializer<'de>,
71{
72    let version = u32::deserialize(deserializer)?;
73    if version == DOCUMENT_VERSION {
74        Ok(version)
75    } else {
76        Err(de::Error::custom(format_args!(
77            "unsupported document version {version}; expected {DOCUMENT_VERSION}"
78        )))
79    }
80}
81
82/// A clipboard or paste fragment.
83pub type DocFragment = Vec<NodeDoc>;
84
85/// Generates parallel node declarations for the live tree and document tree.
86macro_rules! node_kinds {
87    (
88        $(
89            $variant:ident {
90                $( $field:ident : $mode:ident $ty:ty ),* $(,)?
91            }
92        ),* $(,)?
93    ) => {
94        /// The serialized form of a node.
95        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96        #[serde(tag = "type", content = "data", rename_all = "snake_case")]
97        pub enum NodeDoc {
98            /// A serialized atom.
99            Atom(Symbol),
100            $(
101                /// A serialized generated node variant.
102                $variant {
103                    $( /// A serialized generated node field.
104                    $field : node_kinds!(@doc_ty $mode $ty) ),*
105                },
106            )*
107            /// A serialized matrix.
108            Matrix {
109                /// The matrix environment.
110                env: MatrixEnv,
111                /// The serialized matrix cells by row.
112                rows: Vec<Vec<Vec<NodeDoc>>>,
113            },
114        }
115
116        impl Tree {
117            fn node_to_doc(&self, node: NodeId) -> NodeDoc {
118                match self.kind(node).expect("live node") {
119                    Kind::Atom(s) => NodeDoc::Atom(s.clone()),
120                    $(
121                        Kind::$variant { $( $field ),* } => NodeDoc::$variant {
122                            $( $field : node_kinds!(@to_doc self $mode $field) ),*
123                        },
124                    )*
125                    Kind::Matrix { env, rows } => NodeDoc::Matrix {
126                        env: *env,
127                        rows: rows
128                            .iter()
129                            .map(|row| row.iter().map(|&c| self.seq_to_doc(c)).collect())
130                            .collect(),
131                    },
132                }
133            }
134
135            fn doc_kind(&mut self, d: &NodeDoc) -> Kind {
136                match d {
137                    NodeDoc::Atom(s) => Kind::Atom(s.clone()),
138                    $(
139                        NodeDoc::$variant { $( $field ),* } => Kind::$variant {
140                            $( $field : node_kinds!(@from_doc self $mode $field) ),*
141                        },
142                    )*
143                    NodeDoc::Matrix { env, rows } => {
144                        let mut grid = Vec::with_capacity(rows.len());
145                        for row in rows {
146                            let mut r = Vec::with_capacity(row.len());
147                            for cell in row {
148                                r.push(self.doc_seq(cell));
149                            }
150                            grid.push(r);
151                        }
152                        Kind::Matrix { env: *env, rows: grid }
153                    }
154                }
155            }
156        }
157    };
158
159    // Field type in NodeDoc.
160    (@doc_ty seq     $ty:ty) => { Vec<NodeDoc> };
161    (@doc_ty opt_seq $ty:ty) => { Option<Vec<NodeDoc>> };
162    (@doc_ty copy    $ty:ty) => { $ty };
163    (@doc_ty clone   $ty:ty) => { $ty };
164
165    // Live Kind field to document field.
166    (@to_doc $self:ident seq     $f:ident) => { $self.seq_to_doc(*$f) };
167    (@to_doc $self:ident opt_seq $f:ident) => { $self.opt_seq_to_doc(*$f) };
168    (@to_doc $self:ident copy    $f:ident) => { *$f };
169    (@to_doc $self:ident clone   $f:ident) => { $f.clone() };
170
171    // Document field to live Kind field.
172    (@from_doc $self:ident seq     $f:ident) => { $self.doc_seq($f) };
173    (@from_doc $self:ident opt_seq $f:ident) => { $self.doc_opt_seq($f) };
174    (@from_doc $self:ident copy    $f:ident) => { *$f };
175    (@from_doc $self:ident clone   $f:ident) => { $f.clone() };
176}
177
178node_kinds! {
179    Frac {
180        num: seq SeqId,
181        den: seq SeqId,
182        style: copy FracStyle,
183    },
184    Script {
185        base: seq SeqId,
186        sub: opt_seq SeqId,
187        sup: opt_seq SeqId,
188    },
189    BigOp {
190        op: clone Symbol,
191        lower: seq SeqId,
192        upper: seq SeqId,
193    },
194    Sqrt {
195        index: seq SeqId,
196        radicand: seq SeqId,
197    },
198    Delim {
199        open: copy char,
200        close: copy char,
201        body: seq SeqId,
202    },
203    Accent {
204        mark: copy Mark,
205        base: seq SeqId,
206    },
207    UnderOver {
208        base: seq SeqId,
209        over: opt_seq SeqId,
210        under: opt_seq SeqId,
211        over_deco: copy Deco,
212        under_deco: copy Deco,
213    },
214    Styled {
215        variant: copy Variant,
216        content: seq SeqId,
217    },
218    HostBox {
219        token: copy u32,
220    },
221}
222
223/// Collect every host object token reachable in `nodes` so hosts can dispose dead objects.
224pub fn collect_host_tokens(nodes: &[NodeDoc], out: &mut std::collections::BTreeSet<u32>) {
225    for d in nodes {
226        match d {
227            NodeDoc::Atom(_) => {}
228            NodeDoc::HostBox { token } => {
229                out.insert(*token);
230            }
231            NodeDoc::Frac { num, den, .. } => {
232                collect_host_tokens(num, out);
233                collect_host_tokens(den, out);
234            }
235            NodeDoc::Script { base, sub, sup } => {
236                collect_host_tokens(base, out);
237                for s in [sub, sup].into_iter().flatten() {
238                    collect_host_tokens(s, out);
239                }
240            }
241            NodeDoc::BigOp { lower, upper, .. } => {
242                collect_host_tokens(lower, out);
243                collect_host_tokens(upper, out);
244            }
245            NodeDoc::Sqrt { index, radicand } => {
246                collect_host_tokens(index, out);
247                collect_host_tokens(radicand, out);
248            }
249            NodeDoc::Delim { body, .. } => collect_host_tokens(body, out),
250            NodeDoc::Accent { base, .. } => collect_host_tokens(base, out),
251            NodeDoc::UnderOver { base, over, under, .. } => {
252                collect_host_tokens(base, out);
253                for s in [over, under].into_iter().flatten() {
254                    collect_host_tokens(s, out);
255                }
256            }
257            NodeDoc::Styled { content, .. } => collect_host_tokens(content, out),
258            NodeDoc::Matrix { rows, .. } => {
259                for cell in rows.iter().flatten() {
260                    collect_host_tokens(cell, out);
261                }
262            }
263        }
264    }
265}
266
267impl Tree {
268    /// Serialize the live tree to the id free document form.
269    pub fn to_doc(&self) -> Document {
270        Document::new(self.seq_to_doc(self.root()))
271    }
272
273    fn seq_to_doc(&self, seq: SeqId) -> Vec<NodeDoc> {
274        self.items(seq).iter().map(|&n| self.node_to_doc(n)).collect()
275    }
276
277    fn opt_seq_to_doc(&self, seq: Option<SeqId>) -> Option<Vec<NodeDoc>> {
278        seq.map(|s| self.seq_to_doc(s))
279    }
280
281    /// Rebuild a tree from the document form with fresh ids.
282    pub fn from_doc(doc: &Document) -> Self {
283        let mut t = Tree::new();
284        let root = t.root();
285        for d in doc.root() {
286            t.push_doc_node(root, d);
287        }
288        t
289    }
290
291    /// Splice a clipboard fragment into `at.seq` starting at `at.index`.
292    pub fn insert_fragment(&mut self, at: crate::model::Cursor, frag: &DocFragment) -> crate::model::Cursor {
293        let mut idx = at.index;
294        for d in frag {
295            let kind = self.doc_kind(d);
296            let node = self.nodes.insert(Node {
297                parent: at.seq,
298                kind,
299            });
300            for s in self.child_seqs(node) {
301                if let Some(sq) = self.seqs.get_mut(s) {
302                    sq.parent = Some(node);
303                }
304            }
305            if let Some(sq) = self.seqs.get_mut(at.seq) {
306                let i = idx.min(sq.items.len());
307                sq.items.insert(i, node);
308            }
309            idx += 1;
310        }
311        crate::model::Cursor {
312            seq: at.seq,
313            index: idx,
314        }
315    }
316
317    fn push_doc_node(&mut self, seq: SeqId, d: &NodeDoc) {
318        let kind = self.doc_kind(d);
319        let node = self.nodes.insert(Node { parent: seq, kind });
320        for s in self.child_seqs(node) {
321            if let Some(sq) = self.seqs.get_mut(s) {
322                sq.parent = Some(node);
323            }
324        }
325        if let Some(sq) = self.seqs.get_mut(seq) {
326            sq.items.push(node);
327        }
328    }
329
330    fn doc_seq(&mut self, docs: &[NodeDoc]) -> SeqId {
331        let seq = self.alloc_seq(None);
332        for d in docs {
333            self.push_doc_node(seq, d);
334        }
335        seq
336    }
337
338    fn doc_opt_seq(&mut self, docs: &Option<Vec<NodeDoc>>) -> Option<SeqId> {
339        match docs {
340            Some(d) => Some(self.doc_seq(d)),
341            None => None,
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::model::{Cursor, MathClass};
350
351    fn atom(c: &str) -> Symbol {
352        Symbol {
353            latex: c.into(),
354            class: MathClass::Ord,
355        }
356    }
357
358    #[test]
359    fn doc_roundtrip() {
360        let mut t = Tree::new();
361        let root = t.root();
362        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
363        let c = t.insert_atom(c, atom("a")); // Numerator is `a`.
364        let _ = c;
365        // Add a script in the denominator.
366        let frac = t.items(root)[0];
367        let den = t.child_seqs(frac)[1];
368        t.insert_atom(Cursor { seq: den, index: 0 }, atom("b"));
369
370        let d1 = t.to_doc();
371        let json = serde_json::to_string(&d1).unwrap();
372        let decoded = serde_json::from_str(&json).unwrap();
373        let t2 = Tree::from_doc(&decoded);
374        let d2 = t2.to_doc();
375        assert_eq!(d1, d2);
376    }
377
378    /// Round trips one of every kind so bad `node_kinds!` mode tags fail as assertions.
379    #[test]
380    fn doc_roundtrip_all_kinds() {
381        use crate::model::{Mark, MatrixEnv, ScriptSlot, UnderOverSpec, Variant};
382        let mut t = Tree::new();
383        let root = t.root();
384        let end = |t: &Tree| Cursor { seq: root, index: t.len(root) };
385
386        // Atom plus Script exercises `opt_seq` with content.
387        t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
388        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
389        t.insert_atom(c, atom("2"));
390        // Fraction.
391        let c = t.insert_fraction(end(&t), FracStyle::Bar, None);
392        t.insert_atom(c, atom("x"));
393        // Square root.
394        let c = t.insert_sqrt(end(&t), None);
395        t.insert_atom(c, atom("y"));
396        // Big operator.
397        t.insert_big_op(end(&t), Symbol { latex: "\\sum".into(), class: MathClass::Op });
398        // Delimiter pair.
399        let c = t.insert_delimiters(end(&t), '(', ')', None);
400        t.insert_atom(c, atom("z"));
401        // Accent.
402        let c = t.insert_accent(end(&t), Mark::Hat, None);
403        t.insert_atom(c, atom("b"));
404        // Styled content.
405        let c = t.insert_styled(end(&t), Variant::Bold, None);
406        t.insert_atom(c, atom("c"));
407        // UnderOver with an over slot exercises `opt_seq` with an empty value.
408        let spec = UnderOverSpec { over: true, under: false, over_deco: Deco::Brace, under_deco: Deco::None };
409        let c = t.insert_under_over(end(&t), spec, None);
410        t.insert_atom(c, atom("d"));
411        // Matrix exercises the handwritten arm.
412        let c = t.insert_matrix(end(&t), MatrixEnv::Pmatrix, 2, 2);
413        t.insert_atom(c, atom("e"));
414        // Host object atom keeps its token through the round trip.
415        t.insert_host_box(end(&t), 7);
416
417        let d1 = t.to_doc();
418        let json = serde_json::to_string(&d1).unwrap();
419        let decoded = serde_json::from_str(&json).unwrap();
420        let t2 = Tree::from_doc(&decoded);
421        let d2 = t2.to_doc();
422        assert_eq!(d1, d2);
423    }
424
425    /// Pins serde field order and the distinction between `Some` holding empty data and `None`.
426    #[test]
427    fn doc_serde_field_order_is_canonical() {
428        let pos = |s: &str, key: &str| s.find(&format!("\"{key}\"")).unwrap_or_else(|| panic!("missing {key} in {s}"));
429
430        let bigop = NodeDoc::BigOp {
431            op: Symbol { latex: "\\sum".into(), class: MathClass::Op },
432            lower: vec![],
433            upper: vec![],
434        };
435        let s = serde_json::to_string(&bigop).unwrap();
436        assert!(pos(&s, "op") < pos(&s, "lower") && pos(&s, "lower") < pos(&s, "upper"), "BigOp order: {s}");
437
438        let uo = NodeDoc::UnderOver {
439            base: vec![],
440            over: Some(vec![]), // Distinguishes `Some(empty)` from `None`.
441            under: None,
442            over_deco: Deco::Brace,
443            under_deco: Deco::None,
444        };
445        let s = serde_json::to_string(&uo).unwrap();
446        assert!(
447            pos(&s, "base") < pos(&s, "over")
448                && pos(&s, "over") < pos(&s, "under")
449                && pos(&s, "under") < pos(&s, "over_deco")
450                && pos(&s, "over_deco") < pos(&s, "under_deco"),
451            "UnderOver order: {s}"
452        );
453        // `Some` holding empty data and `None` survive serde round trip distinctly.
454        let back: NodeDoc = serde_json::from_str(&s).unwrap();
455        assert_eq!(back, uo);
456    }
457
458    #[test]
459    fn collect_host_tokens_reaches_nested_slots() {
460        let mut t = Tree::new();
461        let root = t.root();
462        // Top level, numerator, and a matrix cell each hold a host box.
463        t.insert_host_box(Cursor { seq: root, index: 0 }, 3);
464        let cnum = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None);
465        t.insert_host_box(cnum, 7);
466        let c = t.insert_matrix(Cursor { seq: root, index: 2 }, MatrixEnv::Pmatrix, 2, 2);
467        t.insert_host_box(c, 9);
468
469        let mut tokens = std::collections::BTreeSet::new();
470        collect_host_tokens(t.to_doc().root(), &mut tokens);
471        assert_eq!(tokens.into_iter().collect::<Vec<_>>(), vec![3, 7, 9]);
472    }
473
474    #[test]
475    fn document_json_has_a_stable_shape() {
476        let doc = Document::new(vec![NodeDoc::Atom(atom("x"))]);
477
478        let json = serde_json::to_string(&doc).unwrap();
479
480        assert_eq!(
481            json,
482            r#"{"version":1,"root":[{"type":"atom","data":{"latex":"x","class":"ord"}}]}"#
483        );
484    }
485
486    /// A clipboard fragment travels as a `Document`, so hosts embed this exact wire shape.
487    #[test]
488    fn host_box_fragment_json_has_a_stable_shape() {
489        let doc = Document::new(vec![NodeDoc::HostBox { token: 17 }]);
490
491        let json = serde_json::to_string(&doc).unwrap();
492
493        assert_eq!(json, r#"{"version":1,"root":[{"type":"host_box","data":{"token":17}}]}"#);
494        let back: Document = serde_json::from_str(&json).unwrap();
495        assert_eq!(back, doc);
496    }
497
498    #[test]
499    fn unknown_document_version_is_rejected() {
500        let error = serde_json::from_str::<Document>(r#"{"version":2,"root":[]}"#)
501            .unwrap_err()
502            .to_string();
503
504        assert!(error.contains("unsupported document version 2"), "{error}");
505    }
506}