1use 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
10pub const DOCUMENT_VERSION: u32 = 1;
12
13pub const MAX_DEPTH: usize = 20;
15
16pub const MAX_HOST_TOKEN: u32 = i32::MAX as u32;
18
19pub(crate) const DELIMITERS: &[char] =
21 &['(', ')', '[', ']', '{', '}', '|', '‖', '/', '.', '⌈', '⌉', '⌊', '⌋', '⟨', '⟩'];
22
23#[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 pub fn new(root: Vec<NodeDoc>) -> Self {
71 Self { version: DOCUMENT_VERSION, root }
72 }
73
74 pub fn version(&self) -> u32 {
76 self.version
77 }
78
79 pub fn root(&self) -> &[NodeDoc] {
81 &self.root
82 }
83
84 pub fn len(&self) -> usize {
86 self.root.len()
87 }
88
89 pub fn is_empty(&self) -> bool {
91 self.root.is_empty()
92 }
93
94 pub fn into_root(self) -> Vec<NodeDoc> {
96 self.root
97 }
98
99 pub fn validate(&self) -> Result<(), DocumentError> {
101 validate_seq(&self.root, 0)
102 }
103
104 pub fn visit(&self, mut f: impl FnMut(&NodeDoc)) {
106 visit_nodes(&self.root, &mut f);
107 }
108
109 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 pub fn visit_mut(&mut self, mut f: impl FnMut(&mut NodeDoc)) {
118 visit_nodes_mut(&mut self.root, &mut f);
119 }
120
121 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 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 pub fn to_tex(&self) -> String {
143 self.to_tex_with(|_| None)
144 }
145
146 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 pub(crate) fn height(&self) -> usize {
154 seq_height(&self.root)
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum DocumentError {
161 InvalidSymbol {
163 latex: String,
165 reason: &'static str,
167 },
168 EmptyMatrix,
170 RaggedMatrix,
172 EmptyScript,
174 TooDeep,
176 UnsupportedDelimiter(char),
178 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#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum Repair {
201 DroppedSymbol {
203 latex: String,
205 },
206 DroppedTooDeep,
208 DroppedHostBox {
210 token: u32,
212 },
213 AddedSuperscript,
215 ReplacedDelimiter {
217 found: char,
219 },
220 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
241fn 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
286pub(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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(tag = "type", content = "data", rename_all = "snake_case")]
383pub enum NodeDoc {
384 Atom(Symbol),
386 Frac {
388 num: Vec<NodeDoc>,
390 den: Vec<NodeDoc>,
392 style: FracStyle,
394 },
395 Script {
397 base: Vec<NodeDoc>,
399 sub: Option<Vec<NodeDoc>>,
401 sup: Option<Vec<NodeDoc>>,
403 },
404 BigOp {
406 op: Symbol,
408 lower: Vec<NodeDoc>,
410 upper: Vec<NodeDoc>,
412 },
413 Sqrt {
415 index: Vec<NodeDoc>,
417 radicand: Vec<NodeDoc>,
419 },
420 Delim {
422 open: char,
424 close: char,
426 body: Vec<NodeDoc>,
428 },
429 Accent {
431 mark: Mark,
433 base: Vec<NodeDoc>,
435 },
436 UnderOver {
438 base: Vec<NodeDoc>,
440 over: Option<Vec<NodeDoc>>,
442 under: Option<Vec<NodeDoc>>,
444 over_deco: Deco,
446 under_deco: Deco,
448 },
449 Styled {
451 variant: Variant,
453 content: Vec<NodeDoc>,
455 },
456 HostBox {
458 token: u32,
460 },
461 Matrix {
463 env: MatrixEnv,
465 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 pub(crate) fn is_structural(&self) -> bool {
523 !matches!(self, NodeDoc::Atom(_) | NodeDoc::HostBox { .. })
524 }
525}
526
527impl Tree {
528 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 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 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 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 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 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}