1use serde::{de, Deserialize, Deserializer, Serialize};
4
5use crate::model::{Deco, FracStyle, Kind, Mark, MatrixEnv, Node, NodeId, SeqId, Symbol, Tree, Variant};
6
7pub const DOCUMENT_VERSION: u32 = 1;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Document {
21 #[serde(deserialize_with = "deserialize_version")]
22 version: u32,
23 pub root: Vec<NodeDoc>,
25}
26
27impl Document {
28 pub fn new(root: Vec<NodeDoc>) -> Self {
30 Self {
31 version: DOCUMENT_VERSION,
32 root,
33 }
34 }
35
36 pub fn version(&self) -> u32 {
38 self.version
39 }
40
41 pub fn root(&self) -> &[NodeDoc] {
43 &self.root
44 }
45
46 pub fn len(&self) -> usize {
48 self.root.len()
49 }
50
51 pub fn is_empty(&self) -> bool {
53 self.root.is_empty()
54 }
55
56 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
82pub type DocFragment = Vec<NodeDoc>;
84
85macro_rules! node_kinds {
87 (
88 $(
89 $variant:ident {
90 $( $field:ident : $mode:ident $ty:ty ),* $(,)?
91 }
92 ),* $(,)?
93 ) => {
94 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96 #[serde(tag = "type", content = "data", rename_all = "snake_case")]
97 pub enum NodeDoc {
98 Atom(Symbol),
100 $(
101 $variant {
103 $( $field : node_kinds!(@doc_ty $mode $ty) ),*
105 },
106 )*
107 Matrix {
109 env: MatrixEnv,
111 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 (@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 (@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 (@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
223pub 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 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 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 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")); let _ = c;
365 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 #[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 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 let c = t.insert_fraction(end(&t), FracStyle::Bar, None);
392 t.insert_atom(c, atom("x"));
393 let c = t.insert_sqrt(end(&t), None);
395 t.insert_atom(c, atom("y"));
396 t.insert_big_op(end(&t), Symbol { latex: "\\sum".into(), class: MathClass::Op });
398 let c = t.insert_delimiters(end(&t), '(', ')', None);
400 t.insert_atom(c, atom("z"));
401 let c = t.insert_accent(end(&t), Mark::Hat, None);
403 t.insert_atom(c, atom("b"));
404 let c = t.insert_styled(end(&t), Variant::Bold, None);
406 t.insert_atom(c, atom("c"));
407 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 let c = t.insert_matrix(end(&t), MatrixEnv::Pmatrix, 2, 2);
413 t.insert_atom(c, atom("e"));
414 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 #[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![]), 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 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 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 #[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}