panache_parser/parser/utils/
list_item_buffer.rs1use crate::options::{Dialect, ParserOptions};
7use crate::parser::blocks::container_prefix::{
8 ContainerPrefixLine, ContainerPrefixState, emit_container_prefix_tokens,
9};
10use crate::parser::blocks::headings::{emit_atx_heading, try_parse_atx_heading};
11use crate::parser::blocks::horizontal_rules::{emit_horizontal_rule, try_parse_horizontal_rule};
12use crate::parser::blocks::html_blocks::{
13 HtmlBlockType, count_tag_balance, is_pandoc_matched_pair_tag, try_parse_html_block_start,
14};
15use crate::parser::utils::inline_emission;
16use crate::parser::utils::text_buffer::ParagraphBuffer;
17use crate::syntax::{SyntaxKind, SyntaxNode};
18use rowan::{GreenNodeBuilder, TextSize};
19
20#[derive(Debug, Clone)]
22pub(crate) enum ListItemContent {
23 Text(String),
25 BlockquoteMarker {
27 leading_spaces: usize,
28 has_trailing_space: bool,
29 },
30}
31
32#[derive(Debug, Default, Clone)]
40pub(crate) struct ListItemBuffer {
41 segments: Vec<ListItemContent>,
43}
44
45impl ListItemBuffer {
46 pub(crate) fn new() -> Self {
48 Self {
49 segments: Vec::new(),
50 }
51 }
52
53 pub(crate) fn push_text(&mut self, text: impl Into<String>) {
55 let text = text.into();
56 if text.is_empty() {
57 return;
58 }
59 self.segments.push(ListItemContent::Text(text));
60 }
61
62 pub(crate) fn push_blockquote_marker(
63 &mut self,
64 leading_spaces: usize,
65 has_trailing_space: bool,
66 ) {
67 self.segments.push(ListItemContent::BlockquoteMarker {
68 leading_spaces,
69 has_trailing_space,
70 });
71 }
72
73 pub(crate) fn is_empty(&self) -> bool {
75 self.segments.is_empty()
76 }
77
78 pub(crate) fn segment_count(&self) -> usize {
80 self.segments.len()
81 }
82
83 pub(crate) fn first_text(&self) -> Option<&str> {
85 match self.segments.first()? {
86 ListItemContent::Text(t) => Some(t.as_str()),
87 ListItemContent::BlockquoteMarker { .. } => None,
88 }
89 }
90
91 pub(crate) fn unclosed_pandoc_matched_pair_tag(
103 &self,
104 config: &ParserOptions,
105 ) -> Option<String> {
106 if config.dialect != Dialect::Pandoc {
107 return None;
108 }
109 let first = self.first_text()?;
110 let first_line_with_nl = first.split_inclusive('\n').next()?;
111 let first_line_no_nl = first_line_with_nl
112 .strip_suffix("\r\n")
113 .or_else(|| first_line_with_nl.strip_suffix('\n'))
114 .unwrap_or(first_line_with_nl);
115 let HtmlBlockType::BlockTag {
116 tag_name,
117 is_closing: false,
118 ..
119 } = try_parse_html_block_start(first_line_no_nl, false)?
120 else {
121 return None;
122 };
123 if !is_pandoc_matched_pair_tag(&tag_name) {
124 return None;
125 }
126 let mut opens = 0usize;
127 let mut closes = 0usize;
128 for segment in &self.segments {
129 if let ListItemContent::Text(t) = segment {
130 let (o, c) = count_tag_balance(t, &tag_name);
131 opens += o;
132 closes += c;
133 }
134 }
135 if opens > closes { Some(tag_name) } else { None }
136 }
137
138 pub(crate) fn has_blank_lines_between_content(&self) -> bool {
143 log::trace!(
144 "has_blank_lines_between_content: segments={} result=false",
145 self.segments.len()
146 );
147
148 false
149 }
150
151 fn get_text_for_parsing(&self) -> String {
153 let mut result = String::new();
154 for segment in &self.segments {
155 if let ListItemContent::Text(text) = segment {
156 result.push_str(text);
157 }
158 }
159 result
160 }
161
162 fn to_paragraph_buffer(&self) -> ParagraphBuffer {
163 let mut paragraph_buffer = ParagraphBuffer::new();
164 for segment in &self.segments {
165 match segment {
166 ListItemContent::Text(text) => paragraph_buffer.push_text(text),
167 ListItemContent::BlockquoteMarker {
168 leading_spaces,
169 has_trailing_space,
170 } => paragraph_buffer.push_marker(*leading_spaces, *has_trailing_space),
171 }
172 }
173 paragraph_buffer
174 }
175
176 pub(crate) fn emit_as_block(
192 &self,
193 builder: &mut GreenNodeBuilder<'static>,
194 use_paragraph: bool,
195 config: &ParserOptions,
196 content_col: usize,
197 suppress_footnote_refs: bool,
198 ) {
199 if self.is_empty() {
200 return;
201 }
202
203 let text = self.get_text_for_parsing();
205
206 if !text.is_empty() {
207 let line_without_newline = text
208 .strip_suffix("\r\n")
209 .or_else(|| text.strip_suffix('\n'));
210 if let Some(line) = line_without_newline
211 && !line.contains('\n')
212 && !line.contains('\r')
213 {
214 let detect_line = &line[item_indent_prefix_len(line, content_col)..];
221 if let Some(level) = try_parse_atx_heading(detect_line) {
222 emit_atx_heading(builder, &text, level, config);
223 return;
224 }
225 if try_parse_horizontal_rule(detect_line).is_some() {
226 emit_horizontal_rule(builder, &text);
227 return;
228 }
229 }
230
231 if self
236 .segments
237 .iter()
238 .all(|s| matches!(s, ListItemContent::Text(_)))
239 && let Some(first_nl) = text.find('\n')
240 {
241 let first_line = &text[..first_nl];
242 let after_first = &text[first_nl + 1..];
243 let detect_first = &first_line[item_indent_prefix_len(first_line, content_col)..];
244 if !after_first.is_empty()
245 && let Some(level) = try_parse_atx_heading(detect_first)
246 {
247 let heading_bytes = &text[..first_nl + 1];
248 emit_atx_heading(builder, heading_bytes, level, config);
249
250 let block_kind = if use_paragraph {
251 SyntaxKind::PARAGRAPH
252 } else {
253 SyntaxKind::PLAIN
254 };
255 builder.start_node(block_kind.into());
256 inline_emission::emit_inlines(
257 builder,
258 after_first,
259 config,
260 suppress_footnote_refs,
261 );
262 builder.finish_node();
263 return;
264 }
265 }
266
267 if config.dialect == Dialect::Pandoc
282 && self
283 .segments
284 .iter()
285 .all(|s| matches!(s, ListItemContent::Text(_)))
286 && try_emit_html_block_lift(builder, &text, config, content_col, use_paragraph, "")
287 {
288 return;
289 }
290
291 if self
300 .segments
301 .iter()
302 .all(|s| matches!(s, ListItemContent::Text(_)))
303 && try_emit_table_or_div_lift(builder, &text, config, content_col)
304 {
305 return;
306 }
307 }
308
309 let block_kind = if use_paragraph {
310 SyntaxKind::PARAGRAPH
311 } else {
312 SyntaxKind::PLAIN
313 };
314
315 builder.start_node(block_kind.into());
316
317 let paragraph_buffer = self.to_paragraph_buffer();
318 if !paragraph_buffer.is_empty() {
319 paragraph_buffer.emit_with_inlines(builder, config, suppress_footnote_refs);
320 } else if !text.is_empty() {
321 inline_emission::emit_inlines(builder, &text, config, suppress_footnote_refs);
322 }
323
324 builder.finish_node(); }
326
327 pub(crate) fn clear(&mut self) {
329 self.segments.clear();
330 }
331}
332
333pub(crate) fn try_emit_html_block_lift(
362 builder: &mut GreenNodeBuilder<'static>,
363 text: &str,
364 config: &ParserOptions,
365 content_col: usize,
366 use_paragraph: bool,
367 line0_prefix: &str,
368) -> bool {
369 let first_line = text.split_inclusive('\n').next().unwrap_or(text);
370 let first_line_no_nl = first_line
371 .strip_suffix("\r\n")
372 .or_else(|| first_line.strip_suffix('\n'))
373 .unwrap_or(first_line);
374 if try_parse_html_block_start(first_line_no_nl, false).is_none() {
375 return false;
376 }
377
378 let (parse_text, mut prefixes) = if content_col > 0 {
379 strip_list_item_indent(text, content_col)
380 } else {
381 (text.to_string(), Vec::new())
382 };
383 if !line0_prefix.is_empty() {
384 if prefixes.is_empty() {
385 prefixes.push(line0_prefix.to_string());
386 } else {
387 prefixes[0] = line0_prefix.to_string();
388 }
389 }
390
391 let refdefs = config.refdef_labels.clone().unwrap_or_default();
392 let inner_root = crate::parser::parse_with_refdefs(&parse_text, Some(config.clone()), refdefs);
393
394 let children: Vec<SyntaxNode> = inner_root.children().collect();
395 if children.is_empty() {
396 return false;
397 }
398 let first = &children[0];
399 if !matches!(
400 first.kind(),
401 SyntaxKind::HTML_BLOCK | SyntaxKind::HTML_BLOCK_RAW | SyntaxKind::HTML_BLOCK_DIV
402 ) {
403 return false;
404 }
405 let total_end = children.last().unwrap().text_range().end();
406 if total_end != TextSize::of(parse_text.as_str()) {
407 return false;
408 }
409
410 let multi_child_trailing = if children.len() == 1 {
427 false
428 } else if children.len() == 2
429 && matches!(
430 first.kind(),
431 SyntaxKind::HTML_BLOCK | SyntaxKind::HTML_BLOCK_RAW | SyntaxKind::HTML_BLOCK_DIV
432 )
433 && children[1].kind() == SyntaxKind::PARAGRAPH
434 {
435 true
436 } else {
437 return false;
438 };
439
440 if first.kind() == SyntaxKind::HTML_BLOCK_DIV {
441 let html_block_tag_count = first
442 .children()
443 .filter(|c| c.kind() == SyntaxKind::HTML_BLOCK_TAG)
444 .count();
445 if html_block_tag_count < 2 {
446 return false;
447 }
448 }
449
450 let prefix_lines: Vec<ContainerPrefixLine> = prefixes
451 .into_iter()
452 .map(ContainerPrefixLine::list_only)
453 .collect();
454 let mut prefix_state = ContainerPrefixState::new(prefix_lines);
455 if multi_child_trailing {
456 graft_node(builder, first, &mut prefix_state);
457 let trailing_kind = if use_paragraph {
458 SyntaxKind::PARAGRAPH
459 } else {
460 SyntaxKind::PLAIN
461 };
462 graft_node_retag_root(builder, &children[1], &mut prefix_state, trailing_kind);
463 } else {
464 graft_node(builder, first, &mut prefix_state);
465 }
466 true
467}
468
469fn try_emit_table_or_div_lift(
477 builder: &mut GreenNodeBuilder<'static>,
478 text: &str,
479 config: &ParserOptions,
480 content_col: usize,
481) -> bool {
482 let first_line = text.split_inclusive('\n').next().unwrap_or(text);
483 let first_line_no_nl = first_line
484 .strip_suffix("\r\n")
485 .or_else(|| first_line.strip_suffix('\n'))
486 .unwrap_or(first_line);
487 let trimmed = first_line_no_nl.trim_start();
488 let first_byte = trimmed.as_bytes().first().copied();
489 if !matches!(first_byte, Some(b'|') | Some(b'+') | Some(b':')) {
490 return false;
491 }
492
493 let (parse_text, prefixes) = if content_col > 0 {
494 strip_list_item_indent(text, content_col)
495 } else {
496 (text.to_string(), Vec::new())
497 };
498
499 let refdefs = config.refdef_labels.clone().unwrap_or_default();
500 let inner_root = crate::parser::parse_with_refdefs(&parse_text, Some(config.clone()), refdefs);
501
502 let children: Vec<SyntaxNode> = inner_root.children().collect();
503 if children.len() != 1 {
504 return false;
505 }
506 let first = &children[0];
507 if !matches!(
508 first.kind(),
509 SyntaxKind::PIPE_TABLE | SyntaxKind::GRID_TABLE | SyntaxKind::FENCED_DIV
510 ) {
511 return false;
512 }
513 if first.text_range().end() != TextSize::of(parse_text.as_str()) {
514 return false;
515 }
516
517 let prefix_lines: Vec<ContainerPrefixLine> = prefixes
518 .into_iter()
519 .map(ContainerPrefixLine::list_only)
520 .collect();
521 let mut prefix_state = ContainerPrefixState::new(prefix_lines);
522 graft_node(builder, first, &mut prefix_state);
523 true
524}
525
526fn graft_node_retag_root(
527 builder: &mut GreenNodeBuilder<'static>,
528 node: &SyntaxNode,
529 prefix: &mut Option<ContainerPrefixState>,
530 new_kind: SyntaxKind,
531) {
532 builder.start_node(new_kind.into());
533 for child in node.children_with_tokens() {
534 match child {
535 rowan::NodeOrToken::Node(n) => graft_node(builder, &n, prefix),
536 rowan::NodeOrToken::Token(t) => {
537 emit_grafted_token(builder, t.kind(), t.text(), prefix);
538 }
539 }
540 }
541 builder.finish_node();
542}
543
544fn item_indent_prefix_len(line: &str, content_col: usize) -> usize {
552 let mut consumed = 0usize;
553 let mut col = 0usize;
554 for &b in line.as_bytes() {
555 if col >= content_col {
556 break;
557 }
558 match b {
559 b' ' => {
560 col += 1;
561 consumed += 1;
562 }
563 b'\t' => {
564 let next = (col / 4 + 1) * 4;
565 if next > content_col {
566 break;
567 }
568 col = next;
569 consumed += 1;
570 }
571 _ => break,
572 }
573 }
574 consumed
575}
576
577fn strip_list_item_indent(text: &str, content_col: usize) -> (String, Vec<String>) {
578 let mut stripped = String::with_capacity(text.len());
579 let mut prefixes: Vec<String> = Vec::new();
580 for (i, line) in text.split_inclusive('\n').enumerate() {
581 if i == 0 {
582 prefixes.push(String::new());
583 stripped.push_str(line);
584 continue;
585 }
586 let consumed = item_indent_prefix_len(line, content_col);
587 prefixes.push(line[..consumed].to_string());
588 stripped.push_str(&line[consumed..]);
589 }
590 (stripped, prefixes)
591}
592
593fn graft_node(
594 builder: &mut GreenNodeBuilder<'static>,
595 node: &SyntaxNode,
596 prefix: &mut Option<ContainerPrefixState>,
597) {
598 builder.start_node(node.kind().into());
599 for child in node.children_with_tokens() {
600 match child {
601 rowan::NodeOrToken::Node(n) => graft_node(builder, &n, prefix),
602 rowan::NodeOrToken::Token(t) => {
603 emit_grafted_token(builder, t.kind(), t.text(), prefix);
604 }
605 }
606 }
607 builder.finish_node();
608}
609
610fn emit_grafted_token(
611 builder: &mut GreenNodeBuilder<'static>,
612 kind: SyntaxKind,
613 text: &str,
614 prefix: &mut Option<ContainerPrefixState>,
615) {
616 if let Some(state) = prefix.as_mut() {
617 if state.at_line_start {
618 if let Some(line_prefix) = state.prefixes.get(state.line_idx) {
619 emit_container_prefix_tokens(builder, line_prefix);
620 }
621 state.at_line_start = false;
622 }
623 builder.token(kind.into(), text);
624 if kind == SyntaxKind::NEWLINE || kind == SyntaxKind::BLANK_LINE {
625 state.line_idx += 1;
626 state.at_line_start = true;
627 }
628 } else {
629 builder.token(kind.into(), text);
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
638 fn test_new_buffer_is_empty() {
639 let buffer = ListItemBuffer::new();
640 assert!(buffer.is_empty());
641 assert!(!buffer.has_blank_lines_between_content());
642 }
643
644 #[test]
645 fn test_push_single_text() {
646 let mut buffer = ListItemBuffer::new();
647 buffer.push_text("Hello, world!");
648 assert!(!buffer.is_empty());
649 assert!(!buffer.has_blank_lines_between_content());
650 assert_eq!(buffer.get_text_for_parsing(), "Hello, world!");
651 }
652
653 #[test]
654 fn test_push_multiple_text_segments() {
655 let mut buffer = ListItemBuffer::new();
656 buffer.push_text("Line 1\n");
657 buffer.push_text("Line 2\n");
658 buffer.push_text("Line 3");
659 assert_eq!(buffer.get_text_for_parsing(), "Line 1\nLine 2\nLine 3");
660 }
661
662 #[test]
663 fn test_clear_buffer() {
664 let mut buffer = ListItemBuffer::new();
665 buffer.push_text("Some text");
666 assert!(!buffer.is_empty());
667
668 buffer.clear();
669 assert!(buffer.is_empty());
670 assert_eq!(buffer.get_text_for_parsing(), "");
671 }
672
673 #[test]
674 fn test_empty_text_ignored() {
675 let mut buffer = ListItemBuffer::new();
676 buffer.push_text("");
677 assert!(buffer.is_empty());
678 }
679}