Skip to main content

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}