panache_parser/parser/utils/
list_item_buffer.rs1use crate::options::ParserOptions;
7use crate::parser::blocks::headings::{emit_atx_heading, try_parse_atx_heading};
8use crate::parser::blocks::horizontal_rules::{emit_horizontal_rule, try_parse_horizontal_rule};
9use crate::parser::utils::inline_emission;
10use crate::parser::utils::text_buffer::ParagraphBuffer;
11use crate::syntax::SyntaxKind;
12use rowan::GreenNodeBuilder;
13
14#[derive(Debug, Clone)]
16pub(crate) enum ListItemContent {
17 Text(String),
19 BlockquoteMarker {
21 leading_spaces: usize,
22 has_trailing_space: bool,
23 },
24}
25
26#[derive(Debug, Default, Clone)]
34pub(crate) struct ListItemBuffer {
35 segments: Vec<ListItemContent>,
37}
38
39impl ListItemBuffer {
40 pub(crate) fn new() -> Self {
42 Self {
43 segments: Vec::new(),
44 }
45 }
46
47 pub(crate) fn push_text(&mut self, text: impl Into<String>) {
49 let text = text.into();
50 if text.is_empty() {
51 return;
52 }
53 self.segments.push(ListItemContent::Text(text));
54 }
55
56 pub(crate) fn push_blockquote_marker(
57 &mut self,
58 leading_spaces: usize,
59 has_trailing_space: bool,
60 ) {
61 self.segments.push(ListItemContent::BlockquoteMarker {
62 leading_spaces,
63 has_trailing_space,
64 });
65 }
66
67 pub(crate) fn is_empty(&self) -> bool {
69 self.segments.is_empty()
70 }
71
72 pub(crate) fn segment_count(&self) -> usize {
74 self.segments.len()
75 }
76
77 pub(crate) fn first_text(&self) -> Option<&str> {
79 match self.segments.first()? {
80 ListItemContent::Text(t) => Some(t.as_str()),
81 ListItemContent::BlockquoteMarker { .. } => None,
82 }
83 }
84
85 pub(crate) fn has_blank_lines_between_content(&self) -> bool {
90 log::trace!(
91 "has_blank_lines_between_content: segments={} result=false",
92 self.segments.len()
93 );
94
95 false
96 }
97
98 fn get_text_for_parsing(&self) -> String {
100 let mut result = String::new();
101 for segment in &self.segments {
102 if let ListItemContent::Text(text) = segment {
103 result.push_str(text);
104 }
105 }
106 result
107 }
108
109 fn to_paragraph_buffer(&self) -> ParagraphBuffer {
110 let mut paragraph_buffer = ParagraphBuffer::new();
111 for segment in &self.segments {
112 match segment {
113 ListItemContent::Text(text) => paragraph_buffer.push_text(text),
114 ListItemContent::BlockquoteMarker {
115 leading_spaces,
116 has_trailing_space,
117 } => paragraph_buffer.push_marker(*leading_spaces, *has_trailing_space),
118 }
119 }
120 paragraph_buffer
121 }
122
123 pub(crate) fn emit_as_block(
128 &self,
129 builder: &mut GreenNodeBuilder<'static>,
130 use_paragraph: bool,
131 config: &ParserOptions,
132 ) {
133 if self.is_empty() {
134 return;
135 }
136
137 let text = self.get_text_for_parsing();
139
140 if !text.is_empty() {
141 let line_without_newline = text
142 .strip_suffix("\r\n")
143 .or_else(|| text.strip_suffix('\n'));
144 if let Some(line) = line_without_newline
145 && !line.contains('\n')
146 && !line.contains('\r')
147 {
148 if let Some(level) = try_parse_atx_heading(line) {
149 emit_atx_heading(builder, &text, level, config);
150 return;
151 }
152 if try_parse_horizontal_rule(line).is_some() {
153 emit_horizontal_rule(builder, &text);
154 return;
155 }
156 }
157
158 if self
163 .segments
164 .iter()
165 .all(|s| matches!(s, ListItemContent::Text(_)))
166 && let Some(first_nl) = text.find('\n')
167 {
168 let first_line = &text[..first_nl];
169 let after_first = &text[first_nl + 1..];
170 if !after_first.is_empty()
171 && let Some(level) = try_parse_atx_heading(first_line)
172 {
173 let heading_bytes = &text[..first_nl + 1];
174 emit_atx_heading(builder, heading_bytes, level, config);
175
176 let block_kind = if use_paragraph {
177 SyntaxKind::PARAGRAPH
178 } else {
179 SyntaxKind::PLAIN
180 };
181 builder.start_node(block_kind.into());
182 inline_emission::emit_inlines(builder, after_first, config);
183 builder.finish_node();
184 return;
185 }
186 }
187 }
188
189 let block_kind = if use_paragraph {
190 SyntaxKind::PARAGRAPH
191 } else {
192 SyntaxKind::PLAIN
193 };
194
195 builder.start_node(block_kind.into());
196
197 let paragraph_buffer = self.to_paragraph_buffer();
198 if !paragraph_buffer.is_empty() {
199 paragraph_buffer.emit_with_inlines(builder, config);
200 } else if !text.is_empty() {
201 inline_emission::emit_inlines(builder, &text, config);
202 }
203
204 builder.finish_node(); }
206
207 pub(crate) fn clear(&mut self) {
209 self.segments.clear();
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_new_buffer_is_empty() {
219 let buffer = ListItemBuffer::new();
220 assert!(buffer.is_empty());
221 assert!(!buffer.has_blank_lines_between_content());
222 }
223
224 #[test]
225 fn test_push_single_text() {
226 let mut buffer = ListItemBuffer::new();
227 buffer.push_text("Hello, world!");
228 assert!(!buffer.is_empty());
229 assert!(!buffer.has_blank_lines_between_content());
230 assert_eq!(buffer.get_text_for_parsing(), "Hello, world!");
231 }
232
233 #[test]
234 fn test_push_multiple_text_segments() {
235 let mut buffer = ListItemBuffer::new();
236 buffer.push_text("Line 1\n");
237 buffer.push_text("Line 2\n");
238 buffer.push_text("Line 3");
239 assert_eq!(buffer.get_text_for_parsing(), "Line 1\nLine 2\nLine 3");
240 }
241
242 #[test]
243 fn test_clear_buffer() {
244 let mut buffer = ListItemBuffer::new();
245 buffer.push_text("Some text");
246 assert!(!buffer.is_empty());
247
248 buffer.clear();
249 assert!(buffer.is_empty());
250 assert_eq!(buffer.get_text_for_parsing(), "");
251 }
252
253 #[test]
254 fn test_empty_text_ignored() {
255 let mut buffer = ListItemBuffer::new();
256 buffer.push_text("");
257 assert!(buffer.is_empty());
258 }
259}