Skip to main content

lex_core/lex/ast/elements/
paragraph.rs

1//! Paragraph element
2//!
3//! A paragraph is a block of one or more text lines. It represents any text,  except for a
4//! unascaped annotation string/.
5//!
6//! The above is an example of a single line paragraph.
7//! Whereas this is an example of a multi-line paragraph:
8//!
9//! Parsing Structure:
10//!
11//! | Element   | Prec. Blank | Head     | Tail                |
12//! |-----------|-------------|----------|---------------------|
13//! | Paragraph | Optional    | Any Line | BlankLine or Dedent |
14//!
15//! Special Case: Dialog - Lines starting with "-" can be formally specified as dialog
16//! (paragraphs) rather than list items.
17//!
18//! Learn More:
19//! - Paragraphs spec: specs/v1/elements/paragraph.lex
20//!
21//! Examples:
22//! - A single paragraph spans multiple lines until a blank line
23//! - Blank lines separate paragraphs; lists and sessions break flow
24
25use super::super::range::{Position, Range};
26use super::super::text_content::TextContent;
27use super::super::traits::{AstNode, Container, TextNode, Visitor, VisualStructure};
28use super::annotation::Annotation;
29use super::content_item::ContentItem;
30use std::fmt;
31
32/// A text line within a paragraph
33#[derive(Debug, Clone, PartialEq)]
34pub struct TextLine {
35    pub content: TextContent,
36    pub location: Range,
37}
38
39impl TextLine {
40    fn default_location() -> Range {
41        Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
42    }
43    pub fn new(content: TextContent) -> Self {
44        Self {
45            content,
46            location: Self::default_location(),
47        }
48    }
49
50    pub fn at(mut self, location: Range) -> Self {
51        self.location = location;
52        self
53    }
54
55    pub fn text(&self) -> &str {
56        self.content.as_string()
57    }
58}
59
60impl AstNode for TextLine {
61    fn node_type(&self) -> &'static str {
62        "TextLine"
63    }
64
65    fn display_label(&self) -> String {
66        let text = self.text();
67        if text.chars().count() > 50 {
68            format!("{}…", text.chars().take(50).collect::<String>())
69        } else {
70            text.to_string()
71        }
72    }
73
74    fn range(&self) -> &Range {
75        &self.location
76    }
77
78    fn accept(&self, visitor: &mut dyn Visitor) {
79        visitor.visit_text_line(self);
80        visitor.leave_text_line(self);
81    }
82}
83
84impl VisualStructure for TextLine {
85    fn is_source_line_node(&self) -> bool {
86        true
87    }
88}
89
90impl fmt::Display for TextLine {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "TextLine('{}')", self.text())
93    }
94}
95
96/// A paragraph represents a block of text lines
97#[derive(Debug, Clone, PartialEq)]
98pub struct Paragraph {
99    /// Lines stored as ContentItems (each a TextLine wrapping TextContent)
100    pub lines: Vec<ContentItem>,
101    pub annotations: Vec<Annotation>,
102    pub location: Range,
103}
104
105impl Paragraph {
106    fn default_location() -> Range {
107        Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
108    }
109    pub fn new(lines: Vec<ContentItem>) -> Self {
110        debug_assert!(
111            lines
112                .iter()
113                .all(|item| matches!(item, ContentItem::TextLine(_))),
114            "Paragraph lines must be TextLine items"
115        );
116        Self {
117            lines,
118            annotations: Vec::new(),
119            location: Self::default_location(),
120        }
121    }
122    pub fn from_line(line: String) -> Self {
123        Self {
124            lines: vec![ContentItem::TextLine(TextLine::new(
125                TextContent::from_string(line, None),
126            ))],
127            annotations: Vec::new(),
128            location: Self::default_location(),
129        }
130    }
131    /// Create a paragraph with a single line and attach a location
132    pub fn from_line_at(line: String, location: Range) -> Self {
133        let mut para = Self {
134            lines: vec![ContentItem::TextLine(TextLine::new(
135                TextContent::from_string(line, None),
136            ))],
137            annotations: Vec::new(),
138            location: Self::default_location(),
139        };
140        para = para.at(location);
141        para
142    }
143
144    /// Preferred builder
145    pub fn at(mut self, location: Range) -> Self {
146        self.location = location.clone();
147        // When a paragraph's location is set in tests, we should also update
148        // the location of the single child TextLine for consistency, as this
149        // is what the parser would do.
150        if self.lines.len() == 1 {
151            if let Some(super::content_item::ContentItem::TextLine(text_line)) =
152                self.lines.get_mut(0)
153            {
154                text_line.location = location;
155            }
156        }
157        self
158    }
159    pub fn text(&self) -> String {
160        self.lines
161            .iter()
162            .filter_map(|item| {
163                if let super::content_item::ContentItem::TextLine(tl) = item {
164                    Some(tl.text().to_string())
165                } else {
166                    None
167                }
168            })
169            .collect::<Vec<_>>()
170            .join("\n")
171    }
172
173    /// Annotations attached to this paragraph.
174    pub fn annotations(&self) -> &[Annotation] {
175        &self.annotations
176    }
177
178    /// Mutable access to paragraph annotations.
179    pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
180        &mut self.annotations
181    }
182
183    /// Iterate over annotation blocks in source order.
184    pub fn iter_annotations(&self) -> std::slice::Iter<'_, Annotation> {
185        self.annotations.iter()
186    }
187
188    /// Iterate over all content items nested inside attached annotations.
189    pub fn iter_annotation_contents(&self) -> impl Iterator<Item = &ContentItem> {
190        self.annotations
191            .iter()
192            .flat_map(|annotation| annotation.children())
193    }
194}
195
196impl AstNode for Paragraph {
197    fn node_type(&self) -> &'static str {
198        "Paragraph"
199    }
200    fn display_label(&self) -> String {
201        format!("{} line(s)", self.lines.len())
202    }
203    fn range(&self) -> &Range {
204        &self.location
205    }
206
207    fn accept(&self, visitor: &mut dyn Visitor) {
208        visitor.visit_paragraph(self);
209        // Visit child TextLines
210        super::super::traits::visit_children(visitor, &self.lines);
211        visitor.leave_paragraph(self);
212    }
213}
214
215impl TextNode for Paragraph {
216    fn text(&self) -> String {
217        self.lines
218            .iter()
219            .filter_map(|item| {
220                if let super::content_item::ContentItem::TextLine(tl) = item {
221                    Some(tl.text().to_string())
222                } else {
223                    None
224                }
225            })
226            .collect::<Vec<_>>()
227            .join("\n")
228    }
229    fn lines(&self) -> &[TextContent] {
230        // This is a compatibility method - we no longer store raw TextContent
231        // Return empty slice since we've moved to ContentItem::TextLine
232        &[]
233    }
234}
235
236impl VisualStructure for Paragraph {
237    fn collapses_with_children(&self) -> bool {
238        true
239    }
240}
241
242impl fmt::Display for Paragraph {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        write!(f, "Paragraph({} lines)", self.lines.len())
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::super::content_item::ContentItem;
251    use super::*;
252
253    #[test]
254    fn test_paragraph_creation() {
255        let para = Paragraph::new(vec![
256            ContentItem::TextLine(TextLine::new(TextContent::from_string(
257                "Hello".to_string(),
258                None,
259            ))),
260            ContentItem::TextLine(TextLine::new(TextContent::from_string(
261                "World".to_string(),
262                None,
263            ))),
264        ]);
265        assert_eq!(para.lines.len(), 2);
266        assert_eq!(para.text(), "Hello\nWorld");
267    }
268
269    #[test]
270    fn test_paragraph() {
271        let location = Range::new(
272            0..0,
273            super::super::super::range::Position::new(0, 0),
274            super::super::super::range::Position::new(0, 5),
275        );
276        let para = Paragraph::from_line("Hello".to_string()).at(location.clone());
277
278        assert_eq!(para.location, location);
279    }
280}