Skip to main content

panache_parser/parser/utils/
list_item_buffer.rs

1//! Buffer for accumulating list item content before emission.
2//!
3//! This module provides infrastructure for buffering list item content during parsing,
4//! allowing us to determine tight vs loose lists and parse inline elements correctly.
5
6use crate::options::ParserOptions;
7use crate::parser::blocks::headings::{emit_atx_heading, try_parse_atx_heading};
8use crate::parser::utils::inline_emission;
9use crate::parser::utils::text_buffer::ParagraphBuffer;
10use crate::syntax::SyntaxKind;
11use rowan::GreenNodeBuilder;
12
13/// A segment in the list item buffer - either text content or a blank line.
14#[derive(Debug, Clone)]
15pub(crate) enum ListItemContent {
16    /// Text content (includes newlines for losslessness)
17    Text(String),
18    /// Structural blockquote marker emitted inside buffered list-item text.
19    BlockquoteMarker {
20        leading_spaces: usize,
21        has_trailing_space: bool,
22    },
23}
24
25/// Buffer for accumulating list item content before emission.
26///
27/// Collects text, blank lines, and structural elements as we parse list item
28/// continuation lines. When the list item closes, we can:
29/// 1. Determine if it's tight (Plain) or loose (PARAGRAPH)
30/// 2. Parse inline elements correctly across continuation lines
31/// 3. Emit the complete structure
32#[derive(Debug, Default, Clone)]
33pub(crate) struct ListItemBuffer {
34    /// Segments of content in order
35    segments: Vec<ListItemContent>,
36}
37
38impl ListItemBuffer {
39    /// Create a new empty list item buffer.
40    pub(crate) fn new() -> Self {
41        Self {
42            segments: Vec::new(),
43        }
44    }
45
46    /// Push text content to the buffer.
47    pub(crate) fn push_text(&mut self, text: impl Into<String>) {
48        let text = text.into();
49        if text.is_empty() {
50            return;
51        }
52        self.segments.push(ListItemContent::Text(text));
53    }
54
55    pub(crate) fn push_blockquote_marker(
56        &mut self,
57        leading_spaces: usize,
58        has_trailing_space: bool,
59    ) {
60        self.segments.push(ListItemContent::BlockquoteMarker {
61            leading_spaces,
62            has_trailing_space,
63        });
64    }
65
66    /// Check if buffer is empty.
67    pub(crate) fn is_empty(&self) -> bool {
68        self.segments.is_empty()
69    }
70
71    /// Get the number of segments in the buffer (for debugging).
72    pub(crate) fn segment_count(&self) -> usize {
73        self.segments.len()
74    }
75
76    /// Determine if this list item has blank lines between content.
77    ///
78    /// Used to decide between Plain (tight) and PARAGRAPH (loose).
79    /// Returns true if there's a blank line followed by more content.
80    pub(crate) fn has_blank_lines_between_content(&self) -> bool {
81        log::trace!(
82            "has_blank_lines_between_content: segments={} result=false",
83            self.segments.len()
84        );
85
86        false
87    }
88
89    /// Get concatenated text for inline parsing (excludes blank lines).
90    fn get_text_for_parsing(&self) -> String {
91        let mut result = String::new();
92        for segment in &self.segments {
93            if let ListItemContent::Text(text) = segment {
94                result.push_str(text);
95            }
96        }
97        result
98    }
99
100    fn to_paragraph_buffer(&self) -> ParagraphBuffer {
101        let mut paragraph_buffer = ParagraphBuffer::new();
102        for segment in &self.segments {
103            match segment {
104                ListItemContent::Text(text) => paragraph_buffer.push_text(text),
105                ListItemContent::BlockquoteMarker {
106                    leading_spaces,
107                    has_trailing_space,
108                } => paragraph_buffer.push_marker(*leading_spaces, *has_trailing_space),
109            }
110        }
111        paragraph_buffer
112    }
113
114    /// Emit the buffered content as a Plain or PARAGRAPH block.
115    ///
116    /// If `use_paragraph` is true, wraps in PARAGRAPH (loose list).
117    /// If false, wraps in PLAIN (tight list).
118    pub(crate) fn emit_as_block(
119        &self,
120        builder: &mut GreenNodeBuilder<'static>,
121        use_paragraph: bool,
122        config: &ParserOptions,
123    ) {
124        if self.is_empty() {
125            return;
126        }
127
128        // Get text and parse inline elements
129        let text = self.get_text_for_parsing();
130
131        if !text.is_empty() {
132            let line_without_newline = text
133                .strip_suffix("\r\n")
134                .or_else(|| text.strip_suffix('\n'));
135            if let Some(line) = line_without_newline
136                && !line.contains('\n')
137                && !line.contains('\r')
138                && let Some(level) = try_parse_atx_heading(line)
139            {
140                emit_atx_heading(builder, &text, level, config);
141                return;
142            }
143        }
144
145        let block_kind = if use_paragraph {
146            SyntaxKind::PARAGRAPH
147        } else {
148            SyntaxKind::PLAIN
149        };
150
151        builder.start_node(block_kind.into());
152
153        let paragraph_buffer = self.to_paragraph_buffer();
154        if !paragraph_buffer.is_empty() {
155            paragraph_buffer.emit_with_inlines(builder, config);
156        } else if !text.is_empty() {
157            inline_emission::emit_inlines(builder, &text, config);
158        }
159
160        builder.finish_node(); // Close PLAIN or PARAGRAPH
161    }
162
163    /// Clear the buffer for reuse.
164    pub(crate) fn clear(&mut self) {
165        self.segments.clear();
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_new_buffer_is_empty() {
175        let buffer = ListItemBuffer::new();
176        assert!(buffer.is_empty());
177        assert!(!buffer.has_blank_lines_between_content());
178    }
179
180    #[test]
181    fn test_push_single_text() {
182        let mut buffer = ListItemBuffer::new();
183        buffer.push_text("Hello, world!");
184        assert!(!buffer.is_empty());
185        assert!(!buffer.has_blank_lines_between_content());
186        assert_eq!(buffer.get_text_for_parsing(), "Hello, world!");
187    }
188
189    #[test]
190    fn test_push_multiple_text_segments() {
191        let mut buffer = ListItemBuffer::new();
192        buffer.push_text("Line 1\n");
193        buffer.push_text("Line 2\n");
194        buffer.push_text("Line 3");
195        assert_eq!(buffer.get_text_for_parsing(), "Line 1\nLine 2\nLine 3");
196    }
197
198    #[test]
199    fn test_clear_buffer() {
200        let mut buffer = ListItemBuffer::new();
201        buffer.push_text("Some text");
202        assert!(!buffer.is_empty());
203
204        buffer.clear();
205        assert!(buffer.is_empty());
206        assert_eq!(buffer.get_text_for_parsing(), "");
207    }
208
209    #[test]
210    fn test_empty_text_ignored() {
211        let mut buffer = ListItemBuffer::new();
212        buffer.push_text("");
213        assert!(buffer.is_empty());
214    }
215}