mos_core/lib.rs
1//! Core types for the Mosaic typesetting engine.
2//!
3//! Implements the document model (manifest §5) and diagnostics surface
4//! (manifest §31). Every other crate depends on this one; nothing here
5//! depends on parsing, layout, or backends.
6
7#![doc(
8 html_logo_url = "https://mosaic.kjanat.dev/assets/A4.svg",
9 html_favicon_url = "https://mosaic.kjanat.dev/assets/A4.svg"
10)]
11
12use std::collections::BTreeMap;
13use std::path::PathBuf;
14use std::sync::Arc;
15
16/// Stable identifier for a document node.
17///
18/// Per manifest §5.1, IDs should ideally be derived from
19/// `hash(file path + syntactic position + explicit label + local structure)`
20/// rather than parse order. The MVP 0 lowerer (`mos-eval`) hands out
21/// monotonic IDs through `Document::alloc`; the hash-based derivation is
22/// deferred to MVP 5 when stable IDs become observable through the cache.
23///
24/// # Examples
25///
26/// ```
27/// use mos_core::NodeId;
28///
29/// let root = NodeId(0);
30///
31/// assert_eq!(root.0, 0);
32/// ```
33#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
34pub struct NodeId(pub u64);
35
36/// Opaque content / dependency hash.
37///
38/// # Examples
39///
40/// ```
41/// use mos_core::ContentHash;
42///
43/// let hash = ContentHash::default();
44///
45/// assert_eq!(hash.0, 0);
46/// ```
47#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
48pub struct ContentHash(pub u128);
49
50/// Identifier for a resolved style bundle.
51///
52/// # Examples
53///
54/// ```
55/// use mos_core::StyleId;
56///
57/// let style = StyleId::default();
58///
59/// assert_eq!(style.0, 0);
60/// ```
61#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
62pub struct StyleId(pub u32);
63
64/// The kinds of nodes Mosaic recognises (manifest §5.1).
65///
66/// # Examples
67///
68/// ```
69/// use mos_core::NodeKind;
70///
71/// let kind = NodeKind::Paragraph;
72///
73/// assert_eq!(kind, NodeKind::Paragraph);
74/// ```
75#[derive(Copy, Clone, Eq, PartialEq, Debug)]
76pub enum NodeKind {
77 Document,
78 Section,
79 Paragraph,
80 Text,
81 Emphasis,
82 Strong,
83 BoldItalic,
84 Math,
85 Equation,
86 /// A captioned container — an image plus a caption paragraph, laid
87 /// out together with the caption beneath. Cross-references via
88 /// `@fig:foo` will target this kind once MVP 3 lands.
89 Figure,
90 /// A raster image (PNG / JPEG in MVP 1.5). The decoded pixel data
91 /// and natural dimensions live on the node's attributes; see the
92 /// `mos-eval` resolver for the exact attribute names.
93 Image,
94 Table,
95 Citation,
96 Reference,
97 Theorem,
98 Footnote,
99 Bibliography,
100 Raw,
101 /// A bullet or numbered list. The `ordered` attribute distinguishes
102 /// the two kinds and child nodes are [`NodeKind::ListItem`]s.
103 List,
104 /// One entry inside a [`NodeKind::List`]. Inline children carry the
105 /// item's text; nested [`NodeKind::List`] children describe deeper
106 /// levels.
107 ListItem,
108}
109
110/// A semantic document node (manifest §5.1).
111///
112/// # Examples
113///
114/// ```
115/// use std::path::PathBuf;
116///
117/// use mos_core::{AttrMap, ContentHash, Node, NodeId, NodeKind, SourceSpan, StyleId};
118///
119/// let file = PathBuf::from("main.mos");
120/// let node = Node {
121/// id: NodeId(1),
122/// kind: NodeKind::Paragraph,
123/// span: SourceSpan::placeholder(file),
124/// content_hash: ContentHash::default(),
125/// style_id: StyleId::default(),
126/// children: Vec::new(),
127/// attributes: AttrMap::new(),
128/// };
129///
130/// assert_eq!(node.kind, NodeKind::Paragraph);
131/// ```
132#[derive(Clone, Debug)]
133pub struct Node {
134 pub id: NodeId,
135 pub kind: NodeKind,
136 pub span: SourceSpan,
137 pub content_hash: ContentHash,
138 pub style_id: StyleId,
139 pub children: Vec<NodeId>,
140 pub attributes: AttrMap,
141}
142
143/// Attribute map carried on each node. Keys are interned strings in a
144/// later iteration; for now plain `String` keys are fine for the stub.
145pub type AttrMap = BTreeMap<String, AttrValue>;
146
147/// Attribute value carried on a semantic [`Node`].
148///
149/// # Examples
150///
151/// ```
152/// use mos_core::AttrValue;
153///
154/// let value = AttrValue::Str("intro".to_owned());
155///
156/// assert_eq!(value, AttrValue::Str("intro".to_owned()));
157/// ```
158#[derive(Clone, Debug, PartialEq)]
159pub enum AttrValue {
160 Bool(bool),
161 Int(i64),
162 Float(f64),
163 Str(String),
164 List(Vec<Self>),
165 /// A length already resolved to PDF points. The parser carries
166 /// unit-tagged literals (`mm`, `pt`, `em`); the lowerer converts
167 /// them to a single canonical scalar so layout never has to know
168 /// about units.
169 Length(f64),
170 /// Opaque binary payload — currently used to carry decoded raster
171 /// image pixels (RGB8) onto an [`NodeKind::Image`] node so the PDF
172 /// backend can emit them as an Image `XObject` without re-reading the
173 /// source file.
174 ///
175 /// Stored as `Arc<[u8]>` so a node carrying decoded pixels is cheap
176 /// to clone (e.g. across cache boundaries or when the same image
177 /// would otherwise be duplicated through the document graph). The
178 /// layout engine still dedups by resolved path, so most documents
179 /// hold one buffer per image regardless; the `Arc` is insurance
180 /// against accidental copies on the eval → layout boundary.
181 Bytes(Arc<[u8]>),
182}
183
184/// A byte-range location in a source file (manifest §6 stage 1).
185///
186/// # Examples
187///
188/// ```
189/// use std::path::PathBuf;
190///
191/// use mos_core::SourceSpan;
192///
193/// let span = SourceSpan::new(PathBuf::from("main.mos"), 2, 8);
194///
195/// assert_eq!(span.start, 2);
196/// ```
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub struct SourceSpan {
199 pub file: PathBuf,
200 pub start: usize,
201 pub end: usize,
202}
203
204impl SourceSpan {
205 /// Construct a span covering `start..end` in `file`.
206 ///
207 /// # Examples
208 ///
209 /// ```
210 /// use std::path::PathBuf;
211 ///
212 /// use mos_core::SourceSpan;
213 ///
214 /// let span = SourceSpan::new(PathBuf::from("main.mos"), 4, 9);
215 ///
216 /// assert_eq!(span.end, 9);
217 /// ```
218 #[must_use]
219 pub fn new(file: PathBuf, start: usize, end: usize) -> Self {
220 Self { file, start, end }
221 }
222
223 /// A zero-length placeholder span anchored at the start of `file`.
224 ///
225 /// # Examples
226 ///
227 /// ```
228 /// use std::path::PathBuf;
229 ///
230 /// use mos_core::SourceSpan;
231 ///
232 /// let span = SourceSpan::placeholder(PathBuf::from("main.mos"));
233 ///
234 /// assert_eq!((span.start, span.end), (0, 0));
235 /// ```
236 #[must_use]
237 pub fn placeholder(file: PathBuf) -> Self {
238 Self {
239 file,
240 start: 0,
241 end: 0,
242 }
243 }
244}
245
246/// Diagnostic severity (manifest §31).
247///
248/// # Examples
249///
250/// ```
251/// use mos_core::Severity;
252///
253/// let severity = Severity::Error;
254///
255/// assert_eq!(severity, Severity::Error);
256/// ```
257#[derive(Copy, Clone, Eq, PartialEq, Debug)]
258pub enum Severity {
259 Error,
260 Warning,
261 Note,
262 Help,
263}
264
265/// Stable diagnostic code (e.g. `E041`, `W203`, manifest §16).
266///
267/// # Examples
268///
269/// ```
270/// use mos_core::DiagnosticCode;
271///
272/// let code = DiagnosticCode("E001");
273///
274/// assert_eq!(code.0, "E001");
275/// ```
276#[derive(Copy, Clone, Eq, PartialEq, Debug)]
277pub struct DiagnosticCode(pub &'static str);
278
279/// Extra diagnostic context.
280///
281/// # Examples
282///
283/// ```
284/// use mos_core::DiagnosticNote;
285///
286/// let note = DiagnosticNote {
287/// message: "while parsing heading".to_owned(),
288/// span: None,
289/// };
290///
291/// assert!(note.span.is_none());
292/// ```
293#[derive(Clone, Debug)]
294pub struct DiagnosticNote {
295 pub message: String,
296 pub span: Option<SourceSpan>,
297}
298
299/// Suggested source edit for a diagnostic.
300///
301/// # Examples
302///
303/// ```
304/// use mos_core::Suggestion;
305///
306/// let suggestion = Suggestion {
307/// message: "insert closing marker".to_owned(),
308/// replacement: Some("]".to_owned()),
309/// span: None,
310/// };
311///
312/// assert_eq!(suggestion.replacement.as_deref(), Some("]"));
313/// ```
314#[derive(Clone, Debug)]
315pub struct Suggestion {
316 pub message: String,
317 pub replacement: Option<String>,
318 pub span: Option<SourceSpan>,
319}
320
321/// A user-facing diagnostic (manifest §16, §31).
322///
323/// # Examples
324///
325/// ```
326/// use mos_core::{Diagnostic, DiagnosticCode, Severity};
327///
328/// let diagnostic = Diagnostic::error(DiagnosticCode("E001"), "boom");
329///
330/// assert_eq!(diagnostic.severity, Severity::Error);
331/// ```
332#[derive(Clone, Debug)]
333pub struct Diagnostic {
334 pub severity: Severity,
335 pub code: DiagnosticCode,
336 pub message: String,
337 pub span: Option<SourceSpan>,
338 pub notes: Vec<DiagnosticNote>,
339 pub suggestions: Vec<Suggestion>,
340}
341
342impl Diagnostic {
343 /// Construct an error diagnostic without a span.
344 ///
345 /// # Examples
346 ///
347 /// ```
348 /// use mos_core::{Diagnostic, DiagnosticCode, Severity};
349 ///
350 /// let diagnostic = Diagnostic::error(DiagnosticCode("E001"), "boom");
351 ///
352 /// assert_eq!(diagnostic.severity, Severity::Error);
353 /// ```
354 pub fn error(code: DiagnosticCode, message: impl Into<String>) -> Self {
355 Self {
356 severity: Severity::Error,
357 code,
358 message: message.into(),
359 span: None,
360 notes: Vec::new(),
361 suggestions: Vec::new(),
362 }
363 }
364
365 /// Attach a span to a diagnostic.
366 ///
367 /// # Examples
368 ///
369 /// ```
370 /// use std::path::PathBuf;
371 ///
372 /// use mos_core::{Diagnostic, DiagnosticCode, SourceSpan};
373 ///
374 /// let diagnostic = Diagnostic::error(DiagnosticCode("E001"), "boom")
375 /// .with_span(SourceSpan::placeholder(PathBuf::from("main.mos")));
376 ///
377 /// assert!(diagnostic.span.is_some());
378 /// ```
379 #[must_use]
380 pub fn with_span(mut self, span: SourceSpan) -> Self {
381 self.span = Some(span);
382 self
383 }
384}
385
386impl std::fmt::Display for Diagnostic {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 write!(f, "[{}] {}", self.code.0, self.message)
389 }
390}
391
392/// Convert a byte offset into a 1-based `(line, column)` pair.
393///
394/// `src` is treated as UTF-8; columns are counted in *Unicode scalar
395/// values* (i.e. `char`s), not bytes, so a span pointing at the byte
396/// after `µ` reports column 2 rather than 3. Both the returned line
397/// and column are at least 1, and offsets past the end of `src` are
398/// clamped to the end. Offsets that fall in the middle of a UTF-8
399/// code-point round down to the start of that code-point.
400///
401/// # Examples
402///
403/// ```
404/// use mos_core::linecol;
405///
406/// assert_eq!(linecol("a\nb", 2), (2, 1));
407/// ```
408#[must_use]
409pub fn linecol(src: &str, byte_offset: usize) -> (usize, usize) {
410 let mut clamped = byte_offset.min(src.len());
411 while clamped > 0 && !src.is_char_boundary(clamped) {
412 clamped -= 1;
413 }
414 let mut line = 1_usize;
415 let mut line_start = 0_usize;
416 for (i, b) in src.as_bytes().iter().enumerate().take(clamped) {
417 if *b == b'\n' {
418 line += 1;
419 line_start = i + 1;
420 }
421 }
422 let column = src[line_start..clamped].chars().count() + 1;
423 (line, column)
424}
425
426impl std::error::Error for Diagnostic {}
427
428/// Convenience top-level error type for crates that want a single
429/// `Result` alias without inventing their own.
430///
431/// # Examples
432///
433/// ```
434/// use mos_core::CoreError;
435///
436/// let err = CoreError::Unimplemented("cache");
437///
438/// assert_eq!(err.to_string(), "not yet implemented: cache");
439/// ```
440#[derive(thiserror::Error, Debug)]
441pub enum CoreError {
442 #[error("not yet implemented: {0}")]
443 Unimplemented(&'static str),
444
445 #[error(transparent)]
446 Diagnostic(Box<Diagnostic>),
447}
448
449pub type Result<T> = std::result::Result<T, CoreError>;
450
451/// The lowered semantic document graph (manifest §5, §6 stage 2).
452///
453/// Owns every [`Node`] and exposes them through their stable [`NodeId`].
454/// MVP 0 stores nodes in insertion order; the manifest §5.1 hash-derived
455/// IDs land alongside the cache work in MVP 5.
456///
457/// # Examples
458///
459/// ```
460/// use std::path::PathBuf;
461///
462/// use mos_core::{Document, NodeId};
463///
464/// let doc = Document::new(PathBuf::from("main.mos"));
465///
466/// assert_eq!(doc.root, NodeId(0));
467/// ```
468#[derive(Debug)]
469pub struct Document {
470 pub root: NodeId,
471 pub file: PathBuf,
472 nodes: BTreeMap<NodeId, Node>,
473 next_id: u64,
474}
475
476impl Document {
477 /// Create an empty document rooted at `file`. Allocates the
478 /// `Document` root node (`NodeId(0)`) eagerly so callers can append
479 /// children to it immediately.
480 ///
481 /// # Examples
482 ///
483 /// ```
484 /// use std::path::PathBuf;
485 ///
486 /// use mos_core::Document;
487 ///
488 /// let doc = Document::new(PathBuf::from("main.mos"));
489 ///
490 /// assert_eq!(doc.len(), 1);
491 /// ```
492 #[must_use]
493 pub fn new(file: PathBuf) -> Self {
494 let root_id = NodeId(0);
495 let root_node = Node {
496 id: root_id,
497 kind: NodeKind::Document,
498 span: SourceSpan::placeholder(file.clone()),
499 content_hash: ContentHash::default(),
500 style_id: StyleId::default(),
501 children: Vec::new(),
502 attributes: AttrMap::new(),
503 };
504 let mut nodes = BTreeMap::new();
505 nodes.insert(root_id, root_node);
506 Self {
507 root: root_id,
508 file,
509 nodes,
510 next_id: 1,
511 }
512 }
513
514 /// Allocate `node` in the arena and return its assigned [`NodeId`].
515 /// The `id` field on the input is overwritten with the fresh ID.
516 ///
517 /// # Examples
518 ///
519 /// ```
520 /// use std::path::PathBuf;
521 ///
522 /// use mos_core::{AttrMap, ContentHash, Document, Node, NodeId, NodeKind, SourceSpan, StyleId};
523 ///
524 /// let file = PathBuf::from("main.mos");
525 /// let mut doc = Document::new(file.clone());
526 /// let id = doc.alloc(Node {
527 /// id: NodeId::default(),
528 /// kind: NodeKind::Paragraph,
529 /// span: SourceSpan::placeholder(file),
530 /// content_hash: ContentHash::default(),
531 /// style_id: StyleId::default(),
532 /// children: Vec::new(),
533 /// attributes: AttrMap::new(),
534 /// });
535 ///
536 /// assert_eq!(id, NodeId(1));
537 /// ```
538 pub fn alloc(&mut self, mut node: Node) -> NodeId {
539 let id = NodeId(self.next_id);
540 self.next_id += 1;
541 node.id = id;
542 self.nodes.insert(id, node);
543 id
544 }
545
546 /// Allocate `node` as a child of `parent` and return its [`NodeId`].
547 ///
548 /// # Panics
549 ///
550 /// Panics if `parent` is not a node already allocated by this
551 /// `Document`. Silently producing detached nodes would hide lowerer
552 /// bugs in release builds, so this is intentionally a release-time
553 /// assertion rather than a `debug_assert!`.
554 ///
555 /// # Examples
556 ///
557 /// ```
558 /// use std::path::PathBuf;
559 ///
560 /// use mos_core::{AttrMap, ContentHash, Document, Node, NodeId, NodeKind, SourceSpan, StyleId};
561 ///
562 /// let file = PathBuf::from("main.mos");
563 /// let mut doc = Document::new(file.clone());
564 /// let child = doc.alloc_child(doc.root, Node {
565 /// id: NodeId::default(),
566 /// kind: NodeKind::Paragraph,
567 /// span: SourceSpan::placeholder(file),
568 /// content_hash: ContentHash::default(),
569 /// style_id: StyleId::default(),
570 /// children: Vec::new(),
571 /// attributes: AttrMap::new(),
572 /// });
573 ///
574 /// assert_eq!(doc.get(doc.root).map(|node| node.children.as_slice()), Some(&[child][..]));
575 /// ```
576 pub fn alloc_child(&mut self, parent: NodeId, node: Node) -> NodeId {
577 assert!(
578 self.nodes.contains_key(&parent),
579 "Document::alloc_child: unknown parent {parent:?}"
580 );
581 let child_id = self.alloc(node);
582 // Safe to index: we just verified the key exists, and `alloc`
583 // doesn't remove existing entries.
584 if let Some(parent_node) = self.nodes.get_mut(&parent) {
585 parent_node.children.push(child_id);
586 }
587 child_id
588 }
589
590 /// Get a node by id.
591 ///
592 /// # Examples
593 ///
594 /// ```
595 /// use std::path::PathBuf;
596 ///
597 /// use mos_core::{Document, NodeKind};
598 ///
599 /// let doc = Document::new(PathBuf::from("main.mos"));
600 ///
601 /// assert_eq!(doc.get(doc.root).map(|node| node.kind), Some(NodeKind::Document));
602 /// ```
603 #[must_use]
604 pub fn get(&self, id: NodeId) -> Option<&Node> {
605 self.nodes.get(&id)
606 }
607
608 /// Mutable accessor for a single node. Used by the resolver
609 /// (manifest §6 stage 3) to back-patch attributes like `number`
610 /// onto sections and `text` onto `@label` references.
611 ///
612 /// # Examples
613 ///
614 /// ```
615 /// use std::path::PathBuf;
616 ///
617 /// use mos_core::{AttrValue, Document};
618 ///
619 /// let mut doc = Document::new(PathBuf::from("main.mos"));
620 /// if let Some(root) = doc.get_mut(doc.root) {
621 /// root.attributes.insert("title".to_owned(), AttrValue::Str("Demo".to_owned()));
622 /// }
623 ///
624 /// assert!(doc.get(doc.root).is_some_and(|node| node.attributes.contains_key("title")));
625 /// ```
626 #[must_use]
627 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> {
628 self.nodes.get_mut(&id)
629 }
630
631 /// Iterate over every node in the arena in insertion order.
632 ///
633 /// # Examples
634 ///
635 /// ```
636 /// use std::path::PathBuf;
637 ///
638 /// use mos_core::{Document, NodeKind};
639 ///
640 /// let doc = Document::new(PathBuf::from("main.mos"));
641 /// let kinds: Vec<NodeKind> = doc.nodes().map(|node| node.kind).collect();
642 ///
643 /// assert_eq!(kinds, vec![NodeKind::Document]);
644 /// ```
645 pub fn nodes(&self) -> impl Iterator<Item = &Node> {
646 self.nodes.values()
647 }
648
649 /// Total number of nodes including the document root.
650 ///
651 /// # Examples
652 ///
653 /// ```
654 /// use std::path::PathBuf;
655 ///
656 /// use mos_core::Document;
657 ///
658 /// let doc = Document::new(PathBuf::from("main.mos"));
659 ///
660 /// assert_eq!(doc.len(), 1);
661 /// ```
662 #[must_use]
663 pub fn len(&self) -> usize {
664 self.nodes.len()
665 }
666
667 /// Return whether the document has no semantic content beyond the root.
668 ///
669 /// # Examples
670 ///
671 /// ```
672 /// use std::path::PathBuf;
673 ///
674 /// use mos_core::Document;
675 ///
676 /// let doc = Document::new(PathBuf::from("main.mos"));
677 ///
678 /// assert!(doc.is_empty());
679 /// ```
680 #[must_use]
681 pub fn is_empty(&self) -> bool {
682 // The root always exists, so `Document` is never truly empty;
683 // expose the conventional method anyway for clippy compliance.
684 self.len() <= 1
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn linecol_handles_ascii_offsets() {
694 let src = "ab\ncd\nef";
695 assert_eq!(linecol(src, 0), (1, 1));
696 assert_eq!(linecol(src, 1), (1, 2));
697 assert_eq!(linecol(src, 2), (1, 3));
698 assert_eq!(linecol(src, 3), (2, 1));
699 assert_eq!(linecol(src, 6), (3, 1));
700 assert_eq!(linecol(src, 7), (3, 2));
701 // Past the end clamps.
702 assert_eq!(linecol(src, 9999), (3, 3));
703 }
704
705 #[test]
706 fn linecol_counts_chars_not_bytes() {
707 // `µ` is 2 bytes in UTF-8, `字` is 3 bytes. The column for the
708 // byte after them should still be 2, not 3 / 4.
709 let src = "µx\n字y\n";
710 assert_eq!(linecol(src, 0), (1, 1));
711 assert_eq!(linecol(src, 2), (1, 2)); // after `µ`
712 assert_eq!(linecol(src, 3), (1, 3)); // after `µx`
713 assert_eq!(linecol(src, 4), (2, 1)); // start of line 2
714 assert_eq!(linecol(src, 7), (2, 2)); // after `字`
715 }
716
717 #[test]
718 fn linecol_offsets_inside_codepoints_round_down() {
719 // Pointing at the second byte of `µ` should still report
720 // column 1 of line 1, not panic.
721 let src = "µ";
722 assert_eq!(linecol(src, 1), (1, 1));
723 }
724
725 #[test]
726 #[should_panic(expected = "unknown parent")]
727 fn alloc_child_panics_on_unknown_parent() {
728 let mut doc = Document::new(PathBuf::from("test.mos"));
729 // `NodeId(9999)` was never allocated by `doc`; the call must
730 // abort instead of leaking a detached node.
731 doc.alloc_child(
732 NodeId(9999),
733 Node {
734 id: NodeId::default(),
735 kind: NodeKind::Text,
736 span: SourceSpan::placeholder(PathBuf::from("test.mos")),
737 content_hash: ContentHash::default(),
738 style_id: StyleId::default(),
739 children: Vec::new(),
740 attributes: AttrMap::new(),
741 },
742 );
743 }
744
745 #[test]
746 fn document_alloc_and_traverse() {
747 let mut doc = Document::new(PathBuf::from("test.mos"));
748 let para = doc.alloc_child(
749 doc.root,
750 Node {
751 id: NodeId::default(),
752 kind: NodeKind::Paragraph,
753 span: SourceSpan::placeholder(PathBuf::from("test.mos")),
754 content_hash: ContentHash::default(),
755 style_id: StyleId::default(),
756 children: Vec::new(),
757 attributes: AttrMap::new(),
758 },
759 );
760 doc.alloc_child(
761 para,
762 Node {
763 id: NodeId::default(),
764 kind: NodeKind::Text,
765 span: SourceSpan::placeholder(PathBuf::from("test.mos")),
766 content_hash: ContentHash::default(),
767 style_id: StyleId::default(),
768 children: Vec::new(),
769 attributes: AttrMap::new(),
770 },
771 );
772 assert_eq!(doc.len(), 3);
773 assert_eq!(doc.get(doc.root).unwrap().children.len(), 1);
774 assert_eq!(doc.get(para).unwrap().children.len(), 1);
775 }
776}