lex_core/lex/ast/elements/document.rs
1//! Document element
2//!
3//! The document node serves two purposes:
4//! - Contains the document tree.
5//! - Contains document-level annotations, including non-content metadata (like file name,
6//! parser version, etc).
7//!
8//! Lex documents are plain text, utf-8 encoded files with the file extension .lex. Line width
9//! is not limited, and is considered a presentation detail. Best practice dictates only
10//! limiting line length when publishing, not while authoring.
11//!
12//! The document node holds the document metadata and the content's root node, which is a
13//! session node. The structure of the document then is a tree of sessions, which can be nested
14//! arbitrarily. This creates powerful addressing capabilities as one can target any sub-session
15//! from an index.
16//!
17//! Document Title:
18//! The document title is determined during the AST assembly phase (not by the grammar).
19//! If the first element of the document content (after any document-level annotations) is a
20//! single paragraph followed by blank lines, it is promoted to be the document title.
21//! This title is stored in the root session's title field.
22//!
23//! Document Start:
24//! A synthetic `DocumentStart` token is used to mark the boundary between document-level
25//! annotations (metadata) and the actual document content. This allows the parser and
26//! assembly logic to correctly identify where the body begins.
27//!
28//! This structure makes the entire AST homogeneous - the document's content is accessed through
29//! the standard Session interface, making traversal and transformation logic consistent
30//! throughout the tree.
31//!
32//! For more details on document structure and sessions, see the [ast](crate::lex::ast) module.
33//!
34//! Learn More:
35//! - Paragraphs: specs/v1/elements/paragraph.lex
36//! - Lists: specs/v1/elements/list.lex
37//! - Sessions: specs/v1/elements/session.lex
38//! - Annotations: specs/v1/elements/annotation.lex
39//! - Definitions: specs/v1/elements/definition.lex
40//! - Verbatim blocks: specs/v1/elements/verbatim.lex
41//!
42//! Examples:
43//! - Document-level metadata via annotations
44//! - All body content accessible via document.root.children
45
46use super::super::range::{Position, Range};
47use super::super::traits::{AstNode, Container, Visitor};
48use super::annotation::Annotation;
49use super::content_item::ContentItem;
50use super::session::Session;
51use super::typed_content;
52use std::fmt;
53
54#[derive(Debug, Clone, PartialEq)]
55pub struct Document {
56 pub annotations: Vec<Annotation>,
57 // all content is attached to the root node
58 pub root: Session,
59}
60
61impl Document {
62 pub fn new() -> Self {
63 Self {
64 annotations: Vec::new(),
65 root: Session::with_title(String::new()),
66 }
67 }
68
69 pub fn with_content(content: Vec<ContentItem>) -> Self {
70 let mut root = Session::with_title(String::new());
71 let session_content = typed_content::into_session_contents(content);
72 root.children = super::container::SessionContainer::from_typed(session_content);
73 Self {
74 annotations: Vec::new(),
75 root,
76 }
77 }
78
79 /// Construct a document from an existing root session.
80 pub fn from_root(root: Session) -> Self {
81 Self {
82 annotations: Vec::new(),
83 root,
84 }
85 }
86
87 pub fn with_annotations_and_content(
88 annotations: Vec<Annotation>,
89 content: Vec<ContentItem>,
90 ) -> Self {
91 let mut root = Session::with_title(String::new());
92 let session_content = typed_content::into_session_contents(content);
93 root.children = super::container::SessionContainer::from_typed(session_content);
94 Self { annotations, root }
95 }
96
97 pub fn with_root_location(mut self, location: Range) -> Self {
98 self.root.location = location;
99 self
100 }
101
102 pub fn root_session(&self) -> &Session {
103 &self.root
104 }
105
106 pub fn root_session_mut(&mut self) -> &mut Session {
107 &mut self.root
108 }
109
110 pub fn into_root(self) -> Session {
111 self.root
112 }
113
114 /// Get the document title.
115 ///
116 /// This delegates to the root session's title.
117 pub fn title(&self) -> &str {
118 self.root.title.as_string()
119 }
120
121 /// Set the document title.
122 ///
123 /// This updates the root session's title.
124 pub fn set_title(&mut self, title: String) {
125 self.root.title = crate::lex::ast::text_content::TextContent::from_string(title, None);
126 }
127
128 /// Returns the path of nodes at the given position, starting from the document
129 pub fn node_path_at_position(&self, pos: Position) -> Vec<&dyn AstNode> {
130 let path = self.root.node_path_at_position(pos);
131 if !path.is_empty() {
132 let mut nodes: Vec<&dyn AstNode> = Vec::with_capacity(path.len() + 1);
133 nodes.push(self);
134 nodes.extend(path);
135 nodes
136 } else {
137 Vec::new()
138 }
139 }
140
141 /// Returns the deepest (most nested) element that contains the position
142 pub fn element_at(&self, pos: Position) -> Option<&ContentItem> {
143 self.root.element_at(pos)
144 }
145
146 /// Returns the visual line element at the given position
147 pub fn visual_line_at(&self, pos: Position) -> Option<&ContentItem> {
148 self.root.visual_line_at(pos)
149 }
150
151 /// Returns the block element at the given position
152 pub fn block_element_at(&self, pos: Position) -> Option<&ContentItem> {
153 self.root.block_element_at(pos)
154 }
155
156 /// All annotations attached directly to the document (document-level metadata).
157 pub fn annotations(&self) -> &[Annotation] {
158 &self.annotations
159 }
160
161 /// Mutable access to document-level annotations.
162 pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
163 &mut self.annotations
164 }
165
166 /// Iterate over document-level annotation blocks in source order.
167 pub fn iter_annotations(&self) -> std::slice::Iter<'_, Annotation> {
168 self.annotations.iter()
169 }
170
171 /// Iterate over all content items nested inside document-level annotations.
172 pub fn iter_annotation_contents(&self) -> impl Iterator<Item = &ContentItem> {
173 self.annotations
174 .iter()
175 .flat_map(|annotation| annotation.children())
176 }
177
178 // ========================================================================
179 // REFERENCE RESOLUTION APIs (Issue #291)
180 // Delegates to the root session
181 // ========================================================================
182
183 /// Find the first annotation with a matching label.
184 ///
185 /// This searches recursively through all annotations in the document,
186 /// including both document-level annotations and annotations in the content tree.
187 ///
188 /// # Arguments
189 /// * `label` - The label string to search for
190 ///
191 /// # Returns
192 /// The first annotation whose label matches exactly, or None if not found.
193 ///
194 /// # Example
195 /// ```rust,ignore
196 /// // Find annotation with label "42" for reference [42]
197 /// if let Some(annotation) = document.find_annotation_by_label("42") {
198 /// // Jump to this annotation in go-to-definition
199 /// }
200 /// ```
201 pub fn find_annotation_by_label(&self, label: &str) -> Option<&Annotation> {
202 // First check document-level annotations
203 self.annotations
204 .iter()
205 .find(|ann| ann.data.label.value == label)
206 .or_else(|| self.root.find_annotation_by_label(label))
207 }
208
209 /// Find all annotations with a matching label.
210 ///
211 /// This searches recursively through all annotations in the document,
212 /// including both document-level annotations and annotations in the content tree.
213 ///
214 /// # Arguments
215 /// * `label` - The label string to search for
216 ///
217 /// # Returns
218 /// A vector of all annotations whose labels match exactly.
219 ///
220 /// # Example
221 /// ```rust,ignore
222 /// // Find all annotations labeled "note"
223 /// let notes = document.find_annotations_by_label("note");
224 /// for note in notes {
225 /// // Process each note annotation
226 /// }
227 /// ```
228 pub fn find_annotations_by_label(&self, label: &str) -> Vec<&Annotation> {
229 let mut results: Vec<&Annotation> = self
230 .annotations
231 .iter()
232 .filter(|ann| ann.data.label.value == label)
233 .collect();
234
235 results.extend(self.root.find_annotations_by_label(label));
236 results
237 }
238
239 /// Iterate all inline references at any depth.
240 ///
241 /// This method recursively walks the document tree, parses inline content,
242 /// and yields all reference inline nodes (e.g., \[42\], \[@citation\], \[^note\]).
243 ///
244 /// # Returns
245 /// An iterator of references to ReferenceInline nodes
246 ///
247 /// # Example
248 /// ```rust,ignore
249 /// for reference in document.iter_all_references() {
250 /// match &reference.reference_type {
251 /// ReferenceType::FootnoteNumber { number } => {
252 /// // Find annotation with this number
253 /// }
254 /// ReferenceType::Citation(data) => {
255 /// // Process citation
256 /// }
257 /// _ => {}
258 /// }
259 /// }
260 /// ```
261 pub fn iter_all_references(
262 &self,
263 ) -> Box<dyn Iterator<Item = crate::lex::inlines::ReferenceInline> + '_> {
264 self.root.iter_all_references()
265 }
266
267 /// Find all references to a specific target label.
268 ///
269 /// This method searches for inline references that point to the given target.
270 /// For example, find all `[42]` references when looking for footnote "42".
271 ///
272 /// # Arguments
273 /// * `target` - The target label to search for
274 ///
275 /// # Returns
276 /// A vector of references to ReferenceInline nodes that match the target
277 ///
278 /// # Example
279 /// ```rust,ignore
280 /// // Find all references to footnote "42"
281 /// let refs = document.find_references_to("42");
282 /// println!("Found {} references to footnote 42", refs.len());
283 /// ```
284 pub fn find_references_to(&self, target: &str) -> Vec<crate::lex::inlines::ReferenceInline> {
285 self.root.find_references_to(target)
286 }
287}
288
289impl AstNode for Document {
290 fn node_type(&self) -> &'static str {
291 "Document"
292 }
293
294 fn display_label(&self) -> String {
295 format!(
296 "Document ({} annotations, {} items)",
297 self.annotations.len(),
298 self.root.children.len()
299 )
300 }
301
302 fn range(&self) -> &Range {
303 &self.root.location
304 }
305
306 fn accept(&self, visitor: &mut dyn Visitor) {
307 for annotation in &self.annotations {
308 annotation.accept(visitor);
309 }
310 self.root.accept(visitor);
311 }
312}
313
314impl Default for Document {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320impl fmt::Display for Document {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 write!(
323 f,
324 "Document({} annotations, {} items)",
325 self.annotations.len(),
326 self.root.children.len()
327 )
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::super::super::range::Position;
334 use super::super::paragraph::{Paragraph, TextLine};
335 use super::super::session::Session;
336 use super::*;
337 use crate::lex::ast::text_content::TextContent;
338 use crate::lex::ast::traits::AstNode;
339
340 #[test]
341 fn test_document_creation() {
342 let doc = Document::with_content(vec![
343 ContentItem::Paragraph(Paragraph::from_line("Para 1".to_string())),
344 ContentItem::Session(Session::with_title("Section 1".to_string())),
345 ]);
346 assert_eq!(doc.annotations.len(), 0);
347 assert_eq!(doc.root.children.len(), 2);
348 }
349
350 #[test]
351 fn test_document_element_at() {
352 let text_line1 = TextLine::new(TextContent::from_string("First".to_string(), None))
353 .at(Range::new(0..0, Position::new(0, 0), Position::new(0, 5)));
354 let para1 = Paragraph::new(vec![ContentItem::TextLine(text_line1)]).at(Range::new(
355 0..0,
356 Position::new(0, 0),
357 Position::new(0, 5),
358 ));
359
360 let text_line2 = TextLine::new(TextContent::from_string("Second".to_string(), None))
361 .at(Range::new(0..0, Position::new(1, 0), Position::new(1, 6)));
362 let para2 = Paragraph::new(vec![ContentItem::TextLine(text_line2)]).at(Range::new(
363 0..0,
364 Position::new(1, 0),
365 Position::new(1, 6),
366 ));
367
368 let doc = Document::with_content(vec![
369 ContentItem::Paragraph(para1),
370 ContentItem::Paragraph(para2),
371 ]);
372
373 let result = doc.root.element_at(Position::new(1, 3));
374 assert!(result.is_some(), "Expected to find element at position");
375 assert!(result.unwrap().is_text_line());
376 }
377
378 #[test]
379 fn test_document_traits() {
380 let doc = Document::with_content(vec![ContentItem::Paragraph(Paragraph::from_line(
381 "Line".to_string(),
382 ))]);
383
384 assert_eq!(doc.node_type(), "Document");
385 assert_eq!(doc.display_label(), "Document (0 annotations, 1 items)");
386 assert_eq!(doc.root.children.len(), 1);
387 }
388
389 #[test]
390 fn test_root_session_accessors() {
391 let doc = Document::with_content(vec![ContentItem::Session(Session::with_title(
392 "Section".to_string(),
393 ))]);
394
395 assert_eq!(doc.root_session().children.len(), 1);
396
397 let mut doc = doc;
398 doc.root_session_mut().title = TextContent::from_string("Updated".to_string(), None);
399 assert_eq!(doc.root_session().title.as_string(), "Updated");
400
401 let root = doc.into_root();
402 assert_eq!(root.title.as_string(), "Updated");
403 }
404}