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