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}
219
220impl Tree {
221    /// Serialize the live tree to the id free document form.
222    pub fn to_doc(&self) -> Document {
223        Document::new(self.seq_to_doc(self.root()))
224    }
225
226    fn seq_to_doc(&self, seq: SeqId) -> Vec<NodeDoc> {
227        self.items(seq).iter().map(|&n| self.node_to_doc(n)).collect()
228    }
229
230    fn opt_seq_to_doc(&self, seq: Option<SeqId>) -> Option<Vec<NodeDoc>> {
231        seq.map(|s| self.seq_to_doc(s))
232    }
233
234    /// Rebuild a tree from the document form with fresh ids.
235    pub fn from_doc(doc: &Document) -> Self {
236        let mut t = Tree::new();
237        let root = t.root();
238        for d in doc.root() {
239            t.push_doc_node(root, d);
240        }
241        t
242    }
243
244    /// Splice a clipboard fragment into `at.seq` starting at `at.index`.
245    pub fn insert_fragment(&mut self, at: crate::model::Cursor, frag: &DocFragment) -> crate::model::Cursor {
246        let mut idx = at.index;
247        for d in frag {
248            let kind = self.doc_kind(d);
249            let node = self.nodes.insert(Node {
250                parent: at.seq,
251                kind,
252            });
253            for s in self.child_seqs(node) {
254                if let Some(sq) = self.seqs.get_mut(s) {
255                    sq.parent = Some(node);
256                }
257            }
258            if let Some(sq) = self.seqs.get_mut(at.seq) {
259                let i = idx.min(sq.items.len());
260                sq.items.insert(i, node);
261            }
262            idx += 1;
263        }
264        crate::model::Cursor {
265            seq: at.seq,
266            index: idx,
267        }
268    }
269
270    fn push_doc_node(&mut self, seq: SeqId, d: &NodeDoc) {
271        let kind = self.doc_kind(d);
272        let node = self.nodes.insert(Node { parent: seq, kind });
273        for s in self.child_seqs(node) {
274            if let Some(sq) = self.seqs.get_mut(s) {
275                sq.parent = Some(node);
276            }
277        }
278        if let Some(sq) = self.seqs.get_mut(seq) {
279            sq.items.push(node);
280        }
281    }
282
283    fn doc_seq(&mut self, docs: &[NodeDoc]) -> SeqId {
284        let seq = self.alloc_seq(None);
285        for d in docs {
286            self.push_doc_node(seq, d);
287        }
288        seq
289    }
290
291    fn doc_opt_seq(&mut self, docs: &Option<Vec<NodeDoc>>) -> Option<SeqId> {
292        match docs {
293            Some(d) => Some(self.doc_seq(d)),
294            None => None,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::model::{Cursor, MathClass};
303
304    fn atom(c: &str) -> Symbol {
305        Symbol {
306            latex: c.into(),
307            class: MathClass::Ord,
308        }
309    }
310
311    #[test]
312    fn doc_roundtrip() {
313        let mut t = Tree::new();
314        let root = t.root();
315        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
316        let c = t.insert_atom(c, atom("a")); // Numerator is `a`.
317        let _ = c;
318        // Add a script in the denominator.
319        let frac = t.items(root)[0];
320        let den = t.child_seqs(frac)[1];
321        t.insert_atom(Cursor { seq: den, index: 0 }, atom("b"));
322
323        let d1 = t.to_doc();
324        let json = serde_json::to_string(&d1).unwrap();
325        let decoded = serde_json::from_str(&json).unwrap();
326        let t2 = Tree::from_doc(&decoded);
327        let d2 = t2.to_doc();
328        assert_eq!(d1, d2);
329    }
330
331    /// Round trips one of every kind so bad `node_kinds!` mode tags fail as assertions.
332    #[test]
333    fn doc_roundtrip_all_kinds() {
334        use crate::model::{Mark, MatrixEnv, ScriptSlot, UnderOverSpec, Variant};
335        let mut t = Tree::new();
336        let root = t.root();
337        let end = |t: &Tree| Cursor { seq: root, index: t.len(root) };
338
339        // Atom plus Script exercises `opt_seq` with content.
340        t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
341        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
342        t.insert_atom(c, atom("2"));
343        // Fraction.
344        let c = t.insert_fraction(end(&t), FracStyle::Bar, None);
345        t.insert_atom(c, atom("x"));
346        // Square root.
347        let c = t.insert_sqrt(end(&t), None);
348        t.insert_atom(c, atom("y"));
349        // Big operator.
350        t.insert_big_op(end(&t), Symbol { latex: "\\sum".into(), class: MathClass::Op });
351        // Delimiter pair.
352        let c = t.insert_delimiters(end(&t), '(', ')', None);
353        t.insert_atom(c, atom("z"));
354        // Accent.
355        let c = t.insert_accent(end(&t), Mark::Hat, None);
356        t.insert_atom(c, atom("b"));
357        // Styled content.
358        let c = t.insert_styled(end(&t), Variant::Bold, None);
359        t.insert_atom(c, atom("c"));
360        // UnderOver with an over slot exercises `opt_seq` with an empty value.
361        let spec = UnderOverSpec { over: true, under: false, over_deco: Deco::Brace, under_deco: Deco::None };
362        let c = t.insert_under_over(end(&t), spec, None);
363        t.insert_atom(c, atom("d"));
364        // Matrix exercises the handwritten arm.
365        let c = t.insert_matrix(end(&t), MatrixEnv::Pmatrix, 2, 2);
366        t.insert_atom(c, atom("e"));
367
368        let d1 = t.to_doc();
369        let json = serde_json::to_string(&d1).unwrap();
370        let decoded = serde_json::from_str(&json).unwrap();
371        let t2 = Tree::from_doc(&decoded);
372        let d2 = t2.to_doc();
373        assert_eq!(d1, d2);
374    }
375
376    /// Pins serde field order and the distinction between `Some` holding empty data and `None`.
377    #[test]
378    fn doc_serde_field_order_is_canonical() {
379        let pos = |s: &str, key: &str| s.find(&format!("\"{key}\"")).unwrap_or_else(|| panic!("missing {key} in {s}"));
380
381        let bigop = NodeDoc::BigOp {
382            op: Symbol { latex: "\\sum".into(), class: MathClass::Op },
383            lower: vec![],
384            upper: vec![],
385        };
386        let s = serde_json::to_string(&bigop).unwrap();
387        assert!(pos(&s, "op") < pos(&s, "lower") && pos(&s, "lower") < pos(&s, "upper"), "BigOp order: {s}");
388
389        let uo = NodeDoc::UnderOver {
390            base: vec![],
391            over: Some(vec![]), // Distinguishes `Some(empty)` from `None`.
392            under: None,
393            over_deco: Deco::Brace,
394            under_deco: Deco::None,
395        };
396        let s = serde_json::to_string(&uo).unwrap();
397        assert!(
398            pos(&s, "base") < pos(&s, "over")
399                && pos(&s, "over") < pos(&s, "under")
400                && pos(&s, "under") < pos(&s, "over_deco")
401                && pos(&s, "over_deco") < pos(&s, "under_deco"),
402            "UnderOver order: {s}"
403        );
404        // `Some` holding empty data and `None` survive serde round trip distinctly.
405        let back: NodeDoc = serde_json::from_str(&s).unwrap();
406        assert_eq!(back, uo);
407    }
408
409    #[test]
410    fn document_json_has_a_stable_shape() {
411        let doc = Document::new(vec![NodeDoc::Atom(atom("x"))]);
412
413        let json = serde_json::to_string(&doc).unwrap();
414
415        assert_eq!(
416            json,
417            r#"{"version":1,"root":[{"type":"atom","data":{"latex":"x","class":"ord"}}]}"#
418        );
419    }
420
421    #[test]
422    fn unknown_document_version_is_rejected() {
423        let error = serde_json::from_str::<Document>(r#"{"version":2,"root":[]}"#)
424            .unwrap_err()
425            .to_string();
426
427        assert!(error.contains("unsupported document version 2"), "{error}");
428    }
429}