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, 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 VisualStructure for Paragraph {
216    fn collapses_with_children(&self) -> bool {
217        true
218    }
219}
220
221impl fmt::Display for Paragraph {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "Paragraph({} lines)", self.lines.len())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::super::content_item::ContentItem;
230    use super::*;
231
232    #[test]
233    fn test_paragraph_creation() {
234        let para = Paragraph::new(vec![
235            ContentItem::TextLine(TextLine::new(TextContent::from_string(
236                "Hello".to_string(),
237                None,
238            ))),
239            ContentItem::TextLine(TextLine::new(TextContent::from_string(
240                "World".to_string(),
241                None,
242            ))),
243        ]);
244        assert_eq!(para.lines.len(), 2);
245        assert_eq!(para.text(), "Hello\nWorld");
246    }
247
248    #[test]
249    fn test_paragraph() {
250        let location = Range::new(
251            0..0,
252            super::super::super::range::Position::new(0, 0),
253            super::super::super::range::Position::new(0, 5),
254        );
255        let para = Paragraph::from_line("Hello".to_string()).at(location.clone());
256
257        assert_eq!(para.location, location);
258    }
259}