Skip to main content

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}