mos_core/document.rs
1//! The lowered semantic document graph (manifest §5, §6 stage 2).
2//!
3//! [`Document`] owns every [`Node`] and hands them out through their stable
4//! [`NodeId`]. Each node carries a [`NodeKind`], a [`SourceSpan`], a
5//! [`ContentHash`], a [`StyleId`], and an [`AttrMap`] of [`AttrValue`]s.
6
7use std::collections::BTreeMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use crate::{ContentHash, SourceSpan};
12
13/// Stable identifier for a document node.
14///
15/// Per manifest §5.1, IDs should ideally be derived from
16/// `hash(file path + syntactic position + explicit label + local structure)`
17/// rather than parse order. The MVP 0 lowerer (`mos-eval`) hands out
18/// monotonic IDs through `Document::alloc`; the hash-based derivation is
19/// deferred to MVP 5 when stable IDs become observable through the cache.
20///
21/// # Examples
22///
23/// ```
24/// use mos_core::NodeId;
25///
26/// let root = NodeId(0);
27///
28/// assert_eq!(root.0, 0);
29/// ```
30#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
31pub struct NodeId(pub u64);
32
33/// Identifier for a resolved style bundle.
34///
35/// # Examples
36///
37/// ```
38/// use mos_core::StyleId;
39///
40/// let style = StyleId::default();
41///
42/// assert_eq!(style.0, 0);
43/// ```
44#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
45pub struct StyleId(pub u32);
46
47/// The kinds of nodes Mosaic recognises (manifest §5.1).
48///
49/// # Examples
50///
51/// ```
52/// use mos_core::NodeKind;
53///
54/// let kind = NodeKind::Paragraph;
55///
56/// assert_eq!(kind, NodeKind::Paragraph);
57/// ```
58#[derive(Copy, Clone, Eq, PartialEq, Debug)]
59pub enum NodeKind {
60 Document,
61 Section,
62 Paragraph,
63 Text,
64 Emphasis,
65 Strong,
66 BoldItalic,
67 Math,
68 Equation,
69 /// A captioned container: an image plus a caption paragraph, laid
70 /// out together with the caption beneath. Cross-references via
71 /// `@fig:foo` will target this kind once MVP 3 lands.
72 Figure,
73 /// A raster image (PNG / JPEG in MVP 1.5). The decoded pixel data
74 /// and natural dimensions live on the node's attributes; see the
75 /// `mos-eval` resolver for the exact attribute names.
76 Image,
77 Table,
78 Citation,
79 Reference,
80 /// A `@page(label)` reference to the printed page number of a labelled
81 /// target. Distinct from [`Reference`](Self::Reference) (which resolves to
82 /// a section/figure number): a page reference resolves to where the target
83 /// lands, which is only known after layout, via the resolve↔layout fixpoint
84 /// (issue #72). Carries a `label` attribute and placeholder `text`; layout
85 /// renders the `text` attribute like any inline run.
86 PageReference,
87 Theorem,
88 Footnote,
89 Bibliography,
90 Raw,
91 /// A bullet or numbered list. The `ordered` attribute distinguishes
92 /// the two kinds and child nodes are [`NodeKind::ListItem`]s.
93 List,
94 /// One entry inside a [`NodeKind::List`]. Paragraph children carry item
95 /// text; nested [`NodeKind::List`] children describe deeper levels.
96 ListItem,
97 /// `\\`: a forced line break inside a paragraph. Carries no
98 /// attributes; layout consumes it as a `WordItem::HardBreak`
99 /// sentinel in the inline word stream. A blank-line paragraph
100 /// break is **not** the same node: it ends the paragraph and
101 /// triggers paragraph-spacing leading, whereas `HardBreak` keeps
102 /// the same paragraph and applies normal inter-line leading.
103 HardBreak,
104}
105
106/// A semantic document node (manifest §5.1).
107///
108/// Nodes are allocated only by [`Document::alloc`] / [`Document::alloc_child`]
109/// from a [`NodeSpec`]: the arena assigns the [`NodeId`] and owns the
110/// `content_hash`/`style_id` placeholders. Those two fields are `pub(crate)`,
111/// which makes the struct literal unconstructible outside this crate, so no
112/// caller can fabricate a node with a fake id or a hand-set hash.
113///
114/// # Examples
115///
116/// ```
117/// use std::path::PathBuf;
118///
119/// use mos_core::{Document, NodeKind, NodeSpec, SourceSpan};
120///
121/// let file = PathBuf::from("main.mos");
122/// let mut doc = Document::new(file.clone());
123/// let id = doc.alloc(NodeSpec::new(NodeKind::Paragraph, SourceSpan::placeholder(file)));
124///
125/// assert_eq!(doc.get(id).map(|node| &node.kind), Some(&NodeKind::Paragraph));
126/// ```
127#[derive(Clone, Debug)]
128pub struct Node {
129 pub id: NodeId,
130 pub kind: NodeKind,
131 pub span: SourceSpan,
132 pub children: Vec<NodeId>,
133 pub attributes: AttrMap,
134 /// Hash-derived identity placeholder (manifest §5.1); set by the arena,
135 /// always default until the MVP 5 cache work. `pub(crate)` to seal
136 /// external construction.
137 pub(crate) content_hash: ContentHash,
138 /// Resolved style slot placeholder; set by the arena, always default
139 /// until styling lands. `pub(crate)` to seal external construction.
140 pub(crate) style_id: StyleId,
141}
142
143impl Node {
144 /// The node's content hash: a hash-derived identity placeholder
145 /// (manifest §5.1), default until the MVP 5 cache work. Read-only: the
146 /// arena owns this field.
147 #[must_use]
148 pub const fn content_hash(&self) -> ContentHash {
149 self.content_hash
150 }
151
152 /// The node's resolved style slot: a placeholder, default until styling
153 /// lands. Read-only: the arena owns this field.
154 #[must_use]
155 pub const fn style_id(&self) -> StyleId {
156 self.style_id
157 }
158}
159
160/// Blueprint for allocating a node in the document arena.
161///
162/// Carries only caller-chosen fields: `kind`, `span`, and `attributes`.
163/// The arena supplies the `id`, empty `children`, and identity/style
164/// placeholders.
165#[derive(Clone, Debug)]
166pub struct NodeSpec {
167 pub kind: NodeKind,
168 pub span: SourceSpan,
169 pub attributes: AttrMap,
170}
171
172impl NodeSpec {
173 /// A spec for a node of `kind` spanning `span`, with no attributes.
174 #[must_use]
175 pub const fn new(kind: NodeKind, span: SourceSpan) -> Self {
176 Self {
177 kind,
178 span,
179 attributes: AttrMap::new(),
180 }
181 }
182
183 /// Attach `attributes` to this spec.
184 #[must_use]
185 pub fn with_attributes(mut self, attributes: AttrMap) -> Self {
186 self.attributes = attributes;
187 self
188 }
189}
190
191/// Attribute map carried on each node. Keys are interned strings in a
192/// later iteration; for now plain `String` keys are fine for the stub.
193pub type AttrMap = BTreeMap<String, AttrValue>;
194
195/// Attribute value carried on a semantic [`Node`].
196///
197/// # Examples
198///
199/// ```
200/// use mos_core::AttrValue;
201///
202/// let value = AttrValue::Str("intro".to_owned());
203///
204/// assert_eq!(value, AttrValue::Str("intro".to_owned()));
205/// ```
206#[derive(Clone, Debug, PartialEq)]
207pub enum AttrValue {
208 Bool(bool),
209 Int(i64),
210 Float(f64),
211 Str(String),
212 List(Vec<Self>),
213 /// A length already resolved to PDF points. The parser carries
214 /// unit-tagged literals (`mm`, `pt`, `em`); the lowerer converts
215 /// them to a single canonical scalar so layout never has to know
216 /// about units.
217 Length(f64),
218 /// Opaque binary payload; currently used to carry decoded raster
219 /// image pixels (RGB8) onto an [`NodeKind::Image`] node so the PDF
220 /// backend can emit them as an Image `XObject` without re-reading the
221 /// source file.
222 ///
223 /// Stored as `Arc<[u8]>` so a node carrying decoded pixels is cheap
224 /// to clone (e.g. across cache boundaries or when the same image
225 /// would otherwise be duplicated through the document graph). The
226 /// layout engine still dedups by resolved path, so most documents
227 /// hold one buffer per image regardless; the `Arc` is insurance
228 /// against accidental copies on the eval → layout boundary.
229 Bytes(Arc<[u8]>),
230}
231
232/// The lowered semantic document graph (manifest §5, §6 stage 2).
233///
234/// Owns every [`Node`] and exposes them through their stable [`NodeId`].
235/// MVP 0 stores nodes in insertion order; the manifest §5.1 hash-derived
236/// IDs land alongside the cache work in MVP 5.
237///
238/// # Examples
239///
240/// ```
241/// use std::path::PathBuf;
242///
243/// use mos_core::{Document, NodeId};
244///
245/// let doc = Document::new(PathBuf::from("main.mos"));
246///
247/// assert_eq!(doc.root, NodeId(0));
248/// ```
249#[derive(Debug)]
250pub struct Document {
251 pub root: NodeId,
252 pub file: PathBuf,
253 nodes: BTreeMap<NodeId, Node>,
254 next_id: u64,
255}
256
257impl Document {
258 /// Create an empty document rooted at `file`. Allocates the
259 /// `Document` root node (`NodeId(0)`) eagerly so callers can append
260 /// children to it immediately.
261 ///
262 /// # Examples
263 ///
264 /// ```
265 /// use std::path::PathBuf;
266 ///
267 /// use mos_core::Document;
268 ///
269 /// let doc = Document::new(PathBuf::from("main.mos"));
270 ///
271 /// assert_eq!(doc.len(), 1);
272 /// ```
273 #[must_use]
274 pub fn new(file: PathBuf) -> Self {
275 let root_id = NodeId(0);
276 let root_node = Node {
277 id: root_id,
278 kind: NodeKind::Document,
279 span: SourceSpan::placeholder(file.clone()),
280 content_hash: ContentHash::default(),
281 style_id: StyleId::default(),
282 children: Vec::new(),
283 attributes: AttrMap::new(),
284 };
285 let mut nodes = BTreeMap::new();
286 nodes.insert(root_id, root_node);
287 Self {
288 root: root_id,
289 file,
290 nodes,
291 next_id: 1,
292 }
293 }
294
295 /// Allocate a node from `spec` in the arena and return its assigned
296 /// [`NodeId`]. The arena fills in the id, an empty `children` list, and
297 /// the default `content_hash`/`style_id` placeholders.
298 ///
299 /// # Examples
300 ///
301 /// ```
302 /// use std::path::PathBuf;
303 ///
304 /// use mos_core::{Document, NodeId, NodeKind, NodeSpec, SourceSpan};
305 ///
306 /// let file = PathBuf::from("main.mos");
307 /// let mut doc = Document::new(file.clone());
308 /// let id = doc.alloc(NodeSpec::new(NodeKind::Paragraph, SourceSpan::placeholder(file)));
309 ///
310 /// assert_eq!(id, NodeId(1));
311 /// ```
312 pub fn alloc(&mut self, spec: NodeSpec) -> NodeId {
313 let id = NodeId(self.next_id);
314 self.next_id += 1;
315 self.nodes.insert(id, Self::node_from_spec(id, spec));
316 id
317 }
318
319 /// Build the arena-owned [`Node`] for `id` from a caller's [`NodeSpec`],
320 /// supplying the fields the caller does not control.
321 fn node_from_spec(id: NodeId, spec: NodeSpec) -> Node {
322 Node {
323 id,
324 kind: spec.kind,
325 span: spec.span,
326 children: Vec::new(),
327 attributes: spec.attributes,
328 content_hash: ContentHash::default(),
329 style_id: StyleId::default(),
330 }
331 }
332
333 /// Allocate a node from `spec` as a child of `parent` and return its
334 /// [`NodeId`].
335 ///
336 /// # Panics
337 ///
338 /// Panics if `parent` is not a node already allocated by this
339 /// `Document`. Silently producing detached nodes would hide lowerer
340 /// bugs in release builds, so this is intentionally a release-time
341 /// assertion rather than a `debug_assert!`.
342 ///
343 /// # Examples
344 ///
345 /// ```
346 /// use std::path::PathBuf;
347 ///
348 /// use mos_core::{Document, NodeKind, NodeSpec, SourceSpan};
349 ///
350 /// let file = PathBuf::from("main.mos");
351 /// let mut doc = Document::new(file.clone());
352 /// let child = doc.alloc_child(doc.root, NodeSpec::new(NodeKind::Paragraph, SourceSpan::placeholder(file)));
353 ///
354 /// assert_eq!(doc.get(doc.root).map(|node| node.children.as_slice()), Some(&[child][..]));
355 /// ```
356 pub fn alloc_child(&mut self, parent: NodeId, spec: NodeSpec) -> NodeId {
357 assert!(
358 self.nodes.contains_key(&parent),
359 "Document::alloc_child: unknown parent {parent:?}"
360 );
361 let child_id = self.alloc(spec);
362 // Safe to index: we just verified the key exists, and `alloc`
363 // doesn't remove existing entries.
364 if let Some(parent_node) = self.nodes.get_mut(&parent) {
365 parent_node.children.push(child_id);
366 }
367 child_id
368 }
369
370 /// Get a node by id.
371 ///
372 /// # Examples
373 ///
374 /// ```
375 /// use std::path::PathBuf;
376 ///
377 /// use mos_core::{Document, NodeKind};
378 ///
379 /// let doc = Document::new(PathBuf::from("main.mos"));
380 ///
381 /// assert_eq!(doc.get(doc.root).map(|node| node.kind), Some(NodeKind::Document));
382 /// ```
383 #[must_use]
384 pub fn get(&self, id: NodeId) -> Option<&Node> {
385 self.nodes.get(&id)
386 }
387
388 /// Mutable accessor for a single node. Used by the resolver
389 /// (manifest §6 stage 3) to back-patch attributes like `number`
390 /// onto sections and `text` onto `@label` references.
391 ///
392 /// # Examples
393 ///
394 /// ```
395 /// use std::path::PathBuf;
396 ///
397 /// use mos_core::{AttrValue, Document};
398 ///
399 /// let mut doc = Document::new(PathBuf::from("main.mos"));
400 /// if let Some(root) = doc.get_mut(doc.root) {
401 /// root.attributes.insert("title".to_owned(), AttrValue::Str("Demo".to_owned()));
402 /// }
403 ///
404 /// assert!(doc.get(doc.root).is_some_and(|node| node.attributes.contains_key("title")));
405 /// ```
406 #[must_use]
407 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> {
408 self.nodes.get_mut(&id)
409 }
410
411 /// Iterate over every node in the arena in insertion order.
412 ///
413 /// # Examples
414 ///
415 /// ```
416 /// use std::path::PathBuf;
417 ///
418 /// use mos_core::{Document, NodeKind};
419 ///
420 /// let doc = Document::new(PathBuf::from("main.mos"));
421 /// let kinds: Vec<NodeKind> = doc.nodes().map(|node| node.kind).collect();
422 ///
423 /// assert_eq!(kinds, vec![NodeKind::Document]);
424 /// ```
425 pub fn nodes(&self) -> impl Iterator<Item = &Node> {
426 self.nodes.values()
427 }
428
429 /// Total number of nodes including the document root.
430 ///
431 /// # Examples
432 ///
433 /// ```
434 /// use std::path::PathBuf;
435 ///
436 /// use mos_core::Document;
437 ///
438 /// let doc = Document::new(PathBuf::from("main.mos"));
439 ///
440 /// assert_eq!(doc.len(), 1);
441 /// ```
442 #[must_use]
443 pub fn len(&self) -> usize {
444 self.nodes.len()
445 }
446
447 /// Return whether the document has no semantic content beyond the root.
448 ///
449 /// # Examples
450 ///
451 /// ```
452 /// use std::path::PathBuf;
453 ///
454 /// use mos_core::Document;
455 ///
456 /// let doc = Document::new(PathBuf::from("main.mos"));
457 ///
458 /// assert!(doc.is_empty());
459 /// ```
460 #[must_use]
461 pub fn is_empty(&self) -> bool {
462 // The root always exists, so `Document` is never truly empty;
463 // expose the conventional method anyway for clippy compliance.
464 self.len() <= 1
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 #[should_panic(expected = "unknown parent")]
474 fn alloc_child_panics_on_unknown_parent() {
475 let mut doc = Document::new(PathBuf::from("test.mos"));
476 // `NodeId(9999)` was never allocated by `doc`; the call must
477 // abort instead of leaking a detached node.
478 doc.alloc_child(
479 NodeId(9999),
480 NodeSpec::new(
481 NodeKind::Text,
482 SourceSpan::placeholder(PathBuf::from("test.mos")),
483 ),
484 );
485 }
486
487 #[test]
488 fn document_alloc_and_traverse() {
489 let mut doc = Document::new(PathBuf::from("test.mos"));
490 let para = doc.alloc_child(
491 doc.root,
492 NodeSpec::new(
493 NodeKind::Paragraph,
494 SourceSpan::placeholder(PathBuf::from("test.mos")),
495 ),
496 );
497 doc.alloc_child(
498 para,
499 NodeSpec::new(
500 NodeKind::Text,
501 SourceSpan::placeholder(PathBuf::from("test.mos")),
502 ),
503 );
504 assert_eq!(doc.len(), 3);
505 assert_eq!(doc.get(doc.root).unwrap().children.len(), 1);
506 assert_eq!(doc.get(para).unwrap().children.len(), 1);
507 }
508}