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 a first-class element, represented as a dedicated `DocumentTitle`
19//!     AST node owned directly by the `Document`. It is parsed from a single unindented line
20//!     at the start of the document, followed by blank lines, where no indented content follows
21//!     (distinguishing it from a session title). See specs/elements/document.lex.
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//!     For more details on document structure and sessions, see the [ast](crate::lex::ast) module.
29//!
30//! Learn More:
31//! - Paragraphs: specs/v1/elements/paragraph.lex
32//! - Lists: specs/v1/elements/list.lex
33//! - Sessions: specs/v1/elements/session.lex
34//! - Annotations: specs/v1/elements/annotation.lex
35//! - Definitions: specs/v1/elements/definition.lex
36//! - Verbatim blocks: specs/v1/elements/verbatim.lex
37//!
38//! Examples:
39//! - Document-level metadata via annotations
40//! - All body content accessible via document.root.children
41
42use super::super::range::{Position, Range};
43use super::super::text_content::TextContent;
44use super::super::traits::{AstNode, Container, Visitor};
45use super::annotation::Annotation;
46use super::content_item::ContentItem;
47use super::session::Session;
48use super::typed_content;
49use std::fmt;
50
51/// A first-class document title element.
52///
53/// Represents the title of a Lex document — a single unindented line at the start
54/// of the document, followed by blank lines, with no indented content after.
55/// This is distinct from session titles.
56///
57/// An optional subtitle is supported: when the title line ends with a colon and a
58/// second non-blank, non-indented line follows before the blank separator, the
59/// second line is parsed as a subtitle. The trailing colon is structural (stripped
60/// from the title content).
61#[derive(Debug, Clone, PartialEq)]
62pub struct DocumentTitle {
63    pub content: TextContent,
64    pub subtitle: Option<TextContent>,
65    pub location: Range,
66}
67
68impl DocumentTitle {
69    pub fn new(content: TextContent, location: Range) -> Self {
70        Self {
71            content,
72            subtitle: None,
73            location,
74        }
75    }
76
77    pub fn with_subtitle(content: TextContent, subtitle: TextContent, location: Range) -> Self {
78        Self {
79            content,
80            subtitle: Some(subtitle),
81            location,
82        }
83    }
84
85    pub fn from_string(text: String, location: Range) -> Self {
86        Self {
87            content: TextContent::from_string(text, Some(location.clone())),
88            subtitle: None,
89            location,
90        }
91    }
92
93    pub fn as_str(&self) -> &str {
94        self.content.as_string()
95    }
96
97    pub fn subtitle_str(&self) -> Option<&str> {
98        self.subtitle.as_ref().map(|s| s.as_string())
99    }
100}
101
102impl AstNode for DocumentTitle {
103    fn node_type(&self) -> &'static str {
104        "DocumentTitle"
105    }
106
107    fn display_label(&self) -> String {
108        match &self.subtitle {
109            Some(sub) => format!(
110                "DocumentTitle(\"{}\", subtitle: \"{}\")",
111                self.as_str(),
112                sub.as_string()
113            ),
114            None => format!("DocumentTitle(\"{}\")", self.as_str()),
115        }
116    }
117
118    fn range(&self) -> &Range {
119        &self.location
120    }
121
122    fn accept(&self, _visitor: &mut dyn Visitor) {}
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub struct Document {
127    pub annotations: Vec<Annotation>,
128    pub title: Option<DocumentTitle>,
129    // all content is attached to the root node
130    pub root: Session,
131}
132
133impl Document {
134    pub fn new() -> Self {
135        Self {
136            annotations: Vec::new(),
137            title: None,
138            root: Session::with_title(String::new()),
139        }
140    }
141
142    pub fn with_content(content: Vec<ContentItem>) -> Self {
143        let mut root = Session::with_title(String::new());
144        let session_content = typed_content::into_session_contents(content);
145        root.children = super::container::SessionContainer::from_typed(session_content);
146        Self {
147            annotations: Vec::new(),
148            title: None,
149            root,
150        }
151    }
152
153    /// Construct a document from an existing root session.
154    pub fn from_root(root: Session) -> Self {
155        Self {
156            annotations: Vec::new(),
157            title: None,
158            root,
159        }
160    }
161
162    /// Construct a document from a title and root session.
163    pub fn from_title_and_root(title: Option<DocumentTitle>, root: Session) -> Self {
164        Self {
165            annotations: Vec::new(),
166            title,
167            root,
168        }
169    }
170
171    pub fn with_annotations_and_content(
172        annotations: Vec<Annotation>,
173        content: Vec<ContentItem>,
174    ) -> Self {
175        let mut root = Session::with_title(String::new());
176        let session_content = typed_content::into_session_contents(content);
177        root.children = super::container::SessionContainer::from_typed(session_content);
178        Self {
179            annotations,
180            title: None,
181            root,
182        }
183    }
184
185    pub fn with_root_location(mut self, location: Range) -> Self {
186        self.root.location = location;
187        self
188    }
189
190    pub fn root_session(&self) -> &Session {
191        &self.root
192    }
193
194    pub fn root_session_mut(&mut self) -> &mut Session {
195        &mut self.root
196    }
197
198    pub fn into_root(self) -> Session {
199        self.root
200    }
201
202    /// Get the document title text.
203    ///
204    /// Returns the title string if a DocumentTitle is present, empty string otherwise.
205    pub fn title(&self) -> &str {
206        match &self.title {
207            Some(dt) => dt.as_str(),
208            None => "",
209        }
210    }
211
212    /// Set the document title.
213    pub fn set_title(&mut self, title: String) {
214        if title.is_empty() {
215            self.title = None;
216        } else {
217            let location = Range::default();
218            self.title = Some(DocumentTitle::from_string(title, location));
219        }
220    }
221
222    /// Returns the path of nodes at the given position, starting from the document
223    pub fn node_path_at_position(&self, pos: Position) -> Vec<&dyn AstNode> {
224        let path = self.root.node_path_at_position(pos);
225        if !path.is_empty() {
226            let mut nodes: Vec<&dyn AstNode> = Vec::with_capacity(path.len() + 1);
227            nodes.push(self);
228            nodes.extend(path);
229            nodes
230        } else {
231            Vec::new()
232        }
233    }
234
235    /// Returns the deepest (most nested) element that contains the position
236    pub fn element_at(&self, pos: Position) -> Option<&ContentItem> {
237        self.root.element_at(pos)
238    }
239
240    /// Returns the visual line element at the given position
241    pub fn visual_line_at(&self, pos: Position) -> Option<&ContentItem> {
242        self.root.visual_line_at(pos)
243    }
244
245    /// Returns the block element at the given position
246    pub fn block_element_at(&self, pos: Position) -> Option<&ContentItem> {
247        self.root.block_element_at(pos)
248    }
249
250    /// All annotations attached directly to the document (document-level metadata).
251    pub fn annotations(&self) -> &[Annotation] {
252        &self.annotations
253    }
254
255    /// Mutable access to document-level annotations.
256    pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
257        &mut self.annotations
258    }
259
260    /// Iterate over document-level annotation blocks in source order.
261    pub fn iter_annotations(&self) -> std::slice::Iter<'_, Annotation> {
262        self.annotations.iter()
263    }
264
265    /// Iterate over all content items nested inside document-level annotations.
266    pub fn iter_annotation_contents(&self) -> impl Iterator<Item = &ContentItem> {
267        self.annotations
268            .iter()
269            .flat_map(|annotation| annotation.children())
270    }
271
272    // ========================================================================
273    // REFERENCE RESOLUTION APIs (Issue #291)
274    // Delegates to the root session
275    // ========================================================================
276
277    /// Find the first annotation with a matching label.
278    ///
279    /// This searches recursively through all annotations in the document,
280    /// including both document-level annotations and annotations in the content tree.
281    ///
282    /// # Arguments
283    /// * `label` - The label string to search for
284    ///
285    /// # Returns
286    /// The first annotation whose label matches exactly, or None if not found.
287    ///
288    /// # Example
289    /// ```rust,ignore
290    /// // Find annotation with label "42" for reference [42]
291    /// if let Some(annotation) = document.find_annotation_by_label("42") {
292    ///     // Jump to this annotation in go-to-definition
293    /// }
294    /// ```
295    pub fn find_annotation_by_label(&self, label: &str) -> Option<&Annotation> {
296        // First check document-level annotations
297        self.annotations
298            .iter()
299            .find(|ann| ann.data.label.value == label)
300            .or_else(|| self.root.find_annotation_by_label(label))
301    }
302
303    /// Find all annotations with a matching label.
304    ///
305    /// This searches recursively through all annotations in the document,
306    /// including both document-level annotations and annotations in the content tree.
307    ///
308    /// # Arguments
309    /// * `label` - The label string to search for
310    ///
311    /// # Returns
312    /// A vector of all annotations whose labels match exactly.
313    ///
314    /// # Example
315    /// ```rust,ignore
316    /// // Find all annotations labeled "note"
317    /// let notes = document.find_annotations_by_label("note");
318    /// for note in notes {
319    ///     // Process each note annotation
320    /// }
321    /// ```
322    pub fn find_annotations_by_label(&self, label: &str) -> Vec<&Annotation> {
323        let mut results: Vec<&Annotation> = self
324            .annotations
325            .iter()
326            .filter(|ann| ann.data.label.value == label)
327            .collect();
328
329        results.extend(self.root.find_annotations_by_label(label));
330        results
331    }
332
333    /// Iterate all inline references at any depth.
334    ///
335    /// This method recursively walks the document tree, parses inline content,
336    /// and yields all reference inline nodes (e.g., \[42\], \[@citation\], \[^note\]).
337    ///
338    /// # Returns
339    /// An iterator of references to ReferenceInline nodes
340    ///
341    /// # Example
342    /// ```rust,ignore
343    /// for reference in document.iter_all_references() {
344    ///     match &reference.reference_type {
345    ///         ReferenceType::FootnoteNumber { number } => {
346    ///             // Find annotation with this number
347    ///         }
348    ///         ReferenceType::Citation(data) => {
349    ///             // Process citation
350    ///         }
351    ///         _ => {}
352    ///     }
353    /// }
354    /// ```
355    pub fn iter_all_references(
356        &self,
357    ) -> Box<dyn Iterator<Item = crate::lex::inlines::ReferenceInline> + '_> {
358        let title_refs = self
359            .title
360            .iter()
361            .flat_map(|t| {
362                let title_inlines = t.content.inline_items();
363                let subtitle_inlines = t
364                    .subtitle
365                    .iter()
366                    .flat_map(|s| s.inline_items())
367                    .collect::<Vec<_>>();
368                title_inlines.into_iter().chain(subtitle_inlines)
369            })
370            .filter_map(|node| {
371                if let crate::lex::inlines::InlineNode::Reference { data, .. } = node {
372                    Some(data)
373                } else {
374                    None
375                }
376            });
377        Box::new(title_refs.chain(self.root.iter_all_references()))
378    }
379
380    /// Find all references to a specific target label.
381    ///
382    /// This method searches for inline references that point to the given target.
383    /// For example, find all `[42]` references when looking for footnote "42".
384    ///
385    /// # Arguments
386    /// * `target` - The target label to search for
387    ///
388    /// # Returns
389    /// A vector of references to ReferenceInline nodes that match the target
390    ///
391    /// # Example
392    /// ```rust,ignore
393    /// // Find all references to footnote "42"
394    /// let refs = document.find_references_to("42");
395    /// println!("Found {} references to footnote 42", refs.len());
396    /// ```
397    pub fn find_references_to(&self, target: &str) -> Vec<crate::lex::inlines::ReferenceInline> {
398        self.root.find_references_to(target)
399    }
400}
401
402impl AstNode for Document {
403    fn node_type(&self) -> &'static str {
404        "Document"
405    }
406
407    fn display_label(&self) -> String {
408        format!(
409            "Document ({} annotations, {} items)",
410            self.annotations.len(),
411            self.root.children.len()
412        )
413    }
414
415    fn range(&self) -> &Range {
416        &self.root.location
417    }
418
419    fn accept(&self, visitor: &mut dyn Visitor) {
420        for annotation in &self.annotations {
421            annotation.accept(visitor);
422        }
423        if let Some(title) = &self.title {
424            title.accept(visitor);
425        }
426        self.root.accept(visitor);
427    }
428}
429
430impl Default for Document {
431    fn default() -> Self {
432        Self::new()
433    }
434}
435
436impl fmt::Display for Document {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        write!(
439            f,
440            "Document({} annotations, {} items)",
441            self.annotations.len(),
442            self.root.children.len()
443        )
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::super::super::range::Position;
450    use super::super::paragraph::{Paragraph, TextLine};
451    use super::super::session::Session;
452    use super::*;
453    use crate::lex::ast::text_content::TextContent;
454    use crate::lex::ast::traits::AstNode;
455
456    #[test]
457    fn test_document_creation() {
458        let doc = Document::with_content(vec![
459            ContentItem::Paragraph(Paragraph::from_line("Para 1".to_string())),
460            ContentItem::Session(Session::with_title("Section 1".to_string())),
461        ]);
462        assert_eq!(doc.annotations.len(), 0);
463        assert_eq!(doc.root.children.len(), 2);
464    }
465
466    #[test]
467    fn test_document_element_at() {
468        let text_line1 = TextLine::new(TextContent::from_string("First".to_string(), None))
469            .at(Range::new(0..0, Position::new(0, 0), Position::new(0, 5)));
470        let para1 = Paragraph::new(vec![ContentItem::TextLine(text_line1)]).at(Range::new(
471            0..0,
472            Position::new(0, 0),
473            Position::new(0, 5),
474        ));
475
476        let text_line2 = TextLine::new(TextContent::from_string("Second".to_string(), None))
477            .at(Range::new(0..0, Position::new(1, 0), Position::new(1, 6)));
478        let para2 = Paragraph::new(vec![ContentItem::TextLine(text_line2)]).at(Range::new(
479            0..0,
480            Position::new(1, 0),
481            Position::new(1, 6),
482        ));
483
484        let doc = Document::with_content(vec![
485            ContentItem::Paragraph(para1),
486            ContentItem::Paragraph(para2),
487        ]);
488
489        let result = doc.root.element_at(Position::new(1, 3));
490        assert!(result.is_some(), "Expected to find element at position");
491        assert!(result.unwrap().is_text_line());
492    }
493
494    #[test]
495    fn test_document_traits() {
496        let doc = Document::with_content(vec![ContentItem::Paragraph(Paragraph::from_line(
497            "Line".to_string(),
498        ))]);
499
500        assert_eq!(doc.node_type(), "Document");
501        assert_eq!(doc.display_label(), "Document (0 annotations, 1 items)");
502        assert_eq!(doc.root.children.len(), 1);
503    }
504
505    #[test]
506    fn test_root_session_accessors() {
507        let doc = Document::with_content(vec![ContentItem::Session(Session::with_title(
508            "Section".to_string(),
509        ))]);
510
511        assert_eq!(doc.root_session().children.len(), 1);
512
513        let mut doc = doc;
514        doc.root_session_mut().title = TextContent::from_string("Updated".to_string(), None);
515        assert_eq!(doc.root_session().title.as_string(), "Updated");
516
517        let root = doc.into_root();
518        assert_eq!(root.title.as_string(), "Updated");
519    }
520
521    #[test]
522    fn test_document_title_field() {
523        let mut doc = Document::new();
524        assert!(doc.title.is_none());
525        assert_eq!(doc.title(), "");
526
527        doc.set_title("My Title".to_string());
528        assert!(doc.title.is_some());
529        assert_eq!(doc.title(), "My Title");
530
531        doc.set_title(String::new());
532        assert!(doc.title.is_none());
533        assert_eq!(doc.title(), "");
534    }
535
536    #[test]
537    fn test_from_title_and_root() {
538        let title = DocumentTitle::from_string("Test Title".to_string(), Range::default());
539        let root = Session::with_title(String::new());
540        let doc = Document::from_title_and_root(Some(title), root);
541        assert_eq!(doc.title(), "Test Title");
542    }
543}