Skip to main content

panache_parser/parser/
core.rs

1use crate::options::ParserOptions;
2use crate::syntax::{SyntaxKind, SyntaxNode};
3use rowan::GreenNodeBuilder;
4
5use super::block_dispatcher::{
6    BlockContext, BlockDetectionResult, BlockEffect, BlockParserRegistry, BlockQuotePrepared,
7    PreparedBlockMatch,
8};
9use super::blocks::blockquotes;
10use super::blocks::code_blocks;
11use super::blocks::definition_lists;
12use super::blocks::fenced_divs;
13use super::blocks::headings::{emit_atx_heading, try_parse_atx_heading};
14use super::blocks::line_blocks;
15use super::blocks::lists;
16use super::blocks::paragraphs;
17use super::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
18use super::utils::container_stack;
19use super::utils::helpers::{split_lines_inclusive, strip_newline};
20use super::utils::inline_emission;
21use super::utils::marker_utils;
22use super::utils::text_buffer;
23
24use super::blocks::blockquotes::strip_n_blockquote_markers;
25use super::utils::continuation::ContinuationPolicy;
26use container_stack::{Container, ContainerStack, byte_index_at_column, leading_indent};
27use definition_lists::{emit_definition_marker, emit_term};
28use line_blocks::{parse_line_block, try_parse_line_block_start};
29use lists::{
30    ListItemEmissionInput, ListMarker, is_content_nested_bullet_marker, start_nested_list,
31    try_parse_list_marker,
32};
33use marker_utils::{count_blockquote_markers, parse_blockquote_marker_info};
34use text_buffer::TextBuffer;
35
36const GITHUB_ALERT_MARKERS: [&str; 5] = [
37    "[!TIP]",
38    "[!WARNING]",
39    "[!IMPORTANT]",
40    "[!CAUTION]",
41    "[!NOTE]",
42];
43
44pub struct Parser<'a> {
45    lines: Vec<&'a str>,
46    pos: usize,
47    builder: GreenNodeBuilder<'static>,
48    containers: ContainerStack,
49    config: &'a ParserOptions,
50    block_registry: BlockParserRegistry,
51    /// True when the previous block was a metadata block (YAML, Pandoc title, or MMD title).
52    /// The first line after a metadata block is treated as if it has a blank line before it,
53    /// matching Pandoc's behavior of allowing headings etc. directly after frontmatter.
54    after_metadata_block: bool,
55}
56
57impl<'a> Parser<'a> {
58    pub fn new(input: &'a str, config: &'a ParserOptions) -> Self {
59        // Use split_lines_inclusive to preserve line endings (both LF and CRLF)
60        let lines = split_lines_inclusive(input);
61        Self {
62            lines,
63            pos: 0,
64            builder: GreenNodeBuilder::new(),
65            containers: ContainerStack::new(),
66            config,
67            block_registry: BlockParserRegistry::new(),
68            after_metadata_block: false,
69        }
70    }
71
72    pub fn parse(mut self) -> SyntaxNode {
73        self.parse_document_stack();
74
75        SyntaxNode::new_root(self.builder.finish())
76    }
77
78    /// Emit buffered PLAIN content if Definition container has open PLAIN.
79    /// Close containers down to `keep`, emitting buffered content first.
80    fn close_containers_to(&mut self, keep: usize) {
81        // Emit buffered PARAGRAPH/PLAIN content before closing
82        while self.containers.depth() > keep {
83            match self.containers.stack.last() {
84                // Handle ListItem with buffering
85                Some(Container::ListItem { buffer, .. }) if !buffer.is_empty() => {
86                    // Clone buffer to avoid borrow issues
87                    let buffer_clone = buffer.clone();
88
89                    log::debug!(
90                        "Closing ListItem with buffer (is_empty={}, segment_count={})",
91                        buffer_clone.is_empty(),
92                        buffer_clone.segment_count()
93                    );
94
95                    // Determine if this should be Plain or PARAGRAPH:
96                    // 1. Check if parent LIST has blank lines between items (list-level loose)
97                    // 2. OR check if this item has blank lines within its content (item-level loose)
98                    let parent_list_is_loose = self
99                        .containers
100                        .stack
101                        .iter()
102                        .rev()
103                        .find_map(|c| match c {
104                            Container::List {
105                                has_blank_between_items,
106                                ..
107                            } => Some(*has_blank_between_items),
108                            _ => None,
109                        })
110                        .unwrap_or(false);
111
112                    let use_paragraph =
113                        parent_list_is_loose || buffer_clone.has_blank_lines_between_content();
114
115                    log::debug!(
116                        "Emitting ListItem buffer: use_paragraph={} (parent_list_is_loose={}, item_has_blanks={})",
117                        use_paragraph,
118                        parent_list_is_loose,
119                        buffer_clone.has_blank_lines_between_content()
120                    );
121
122                    // Pop container first
123                    self.containers.stack.pop();
124                    // Emit buffered content as Plain or PARAGRAPH
125                    buffer_clone.emit_as_block(&mut self.builder, use_paragraph, self.config);
126                    self.builder.finish_node(); // Close LIST_ITEM
127                }
128                // Handle ListItem without content
129                Some(Container::ListItem { .. }) => {
130                    log::debug!("Closing empty ListItem (no buffer content)");
131                    // Just close normally (empty list item)
132                    self.containers.stack.pop();
133                    self.builder.finish_node();
134                }
135                // Handle Paragraph with buffering
136                Some(Container::Paragraph { buffer, .. }) if !buffer.is_empty() => {
137                    // Clone buffer to avoid borrow issues
138                    let buffer_clone = buffer.clone();
139                    // Pop container first
140                    self.containers.stack.pop();
141                    // Emit buffered content with inline parsing (handles markers)
142                    buffer_clone.emit_with_inlines(&mut self.builder, self.config);
143                    self.builder.finish_node();
144                }
145                // Handle Paragraph without content
146                Some(Container::Paragraph { .. }) => {
147                    // Just close normally
148                    self.containers.stack.pop();
149                    self.builder.finish_node();
150                }
151                // Handle Definition with buffered PLAIN
152                Some(Container::Definition {
153                    plain_open: true,
154                    plain_buffer,
155                    ..
156                }) if !plain_buffer.is_empty() => {
157                    let text = plain_buffer.get_accumulated_text();
158                    let line_without_newline = text
159                        .strip_suffix("\r\n")
160                        .or_else(|| text.strip_suffix('\n'));
161                    if let Some(line) = line_without_newline
162                        && !line.contains('\n')
163                        && !line.contains('\r')
164                        && let Some(level) = try_parse_atx_heading(line)
165                    {
166                        emit_atx_heading(&mut self.builder, &text, level, self.config);
167                    } else {
168                        // Emit PLAIN node with buffered inline-parsed content
169                        self.builder.start_node(SyntaxKind::PLAIN.into());
170                        inline_emission::emit_inlines(&mut self.builder, &text, self.config);
171                        self.builder.finish_node();
172                    }
173
174                    // Mark PLAIN as closed and clear buffer
175                    if let Some(Container::Definition {
176                        plain_open,
177                        plain_buffer,
178                        ..
179                    }) = self.containers.stack.last_mut()
180                    {
181                        plain_buffer.clear();
182                        *plain_open = false;
183                    }
184
185                    // Pop container and finish node
186                    self.containers.stack.pop();
187                    self.builder.finish_node();
188                }
189                // Handle Definition with PLAIN open but empty buffer
190                Some(Container::Definition {
191                    plain_open: true, ..
192                }) => {
193                    // Mark PLAIN as closed
194                    if let Some(Container::Definition {
195                        plain_open,
196                        plain_buffer,
197                        ..
198                    }) = self.containers.stack.last_mut()
199                    {
200                        plain_buffer.clear();
201                        *plain_open = false;
202                    }
203
204                    // Pop container and finish node
205                    self.containers.stack.pop();
206                    self.builder.finish_node();
207                }
208                // All other containers
209                _ => {
210                    self.containers.stack.pop();
211                    self.builder.finish_node();
212                }
213            }
214        }
215    }
216
217    /// Emit buffered PLAIN content if there's an open PLAIN in a Definition.
218    /// This is used when we need to close PLAIN but keep the Definition container open.
219    fn emit_buffered_plain_if_needed(&mut self) {
220        // Check if we have an open PLAIN with buffered content
221        if let Some(Container::Definition {
222            plain_open: true,
223            plain_buffer,
224            ..
225        }) = self.containers.stack.last()
226            && !plain_buffer.is_empty()
227        {
228            let text = plain_buffer.get_accumulated_text();
229            let line_without_newline = text
230                .strip_suffix("\r\n")
231                .or_else(|| text.strip_suffix('\n'));
232            if let Some(line) = line_without_newline
233                && !line.contains('\n')
234                && !line.contains('\r')
235                && let Some(level) = try_parse_atx_heading(line)
236            {
237                emit_atx_heading(&mut self.builder, &text, level, self.config);
238            } else {
239                // Emit PLAIN node with buffered inline-parsed content
240                self.builder.start_node(SyntaxKind::PLAIN.into());
241                inline_emission::emit_inlines(&mut self.builder, &text, self.config);
242                self.builder.finish_node();
243            }
244        }
245
246        // Mark PLAIN as closed and clear buffer
247        if let Some(Container::Definition {
248            plain_open,
249            plain_buffer,
250            ..
251        }) = self.containers.stack.last_mut()
252            && *plain_open
253        {
254            plain_buffer.clear();
255            *plain_open = false;
256        }
257    }
258
259    /// Close blockquotes down to a target depth.
260    ///
261    /// Must use `Parser::close_containers_to` (not `ContainerStack::close_to`) so list/paragraph
262    /// buffers are emitted for losslessness.
263    fn close_blockquotes_to_depth(&mut self, target_depth: usize) {
264        let mut current = self.current_blockquote_depth();
265        while current > target_depth {
266            while !matches!(self.containers.last(), Some(Container::BlockQuote { .. })) {
267                if self.containers.depth() == 0 {
268                    break;
269                }
270                self.close_containers_to(self.containers.depth() - 1);
271            }
272            if matches!(self.containers.last(), Some(Container::BlockQuote { .. })) {
273                self.close_containers_to(self.containers.depth() - 1);
274                current -= 1;
275            } else {
276                break;
277            }
278        }
279    }
280
281    fn active_alert_blockquote_depth(&self) -> Option<usize> {
282        self.containers.stack.iter().rev().find_map(|c| match c {
283            Container::Alert { blockquote_depth } => Some(*blockquote_depth),
284            _ => None,
285        })
286    }
287
288    fn in_active_alert(&self) -> bool {
289        self.active_alert_blockquote_depth().is_some()
290    }
291
292    fn alert_marker_from_content(content: &str) -> Option<&'static str> {
293        let (without_newline, _) = strip_newline(content);
294        let trimmed = without_newline.trim();
295        GITHUB_ALERT_MARKERS
296            .into_iter()
297            .find(|marker| *marker == trimmed)
298    }
299
300    /// Emit buffered list item content if we're in a ListItem and it has content.
301    /// This is used before starting block-level elements inside list items.
302    fn emit_list_item_buffer_if_needed(&mut self) {
303        if let Some(Container::ListItem { buffer, .. }) = self.containers.stack.last_mut()
304            && !buffer.is_empty()
305        {
306            let buffer_clone = buffer.clone();
307            buffer.clear();
308            let use_paragraph = buffer_clone.has_blank_lines_between_content();
309            buffer_clone.emit_as_block(&mut self.builder, use_paragraph, self.config);
310        }
311    }
312
313    /// Check if a paragraph is currently open.
314    fn is_paragraph_open(&self) -> bool {
315        matches!(self.containers.last(), Some(Container::Paragraph { .. }))
316    }
317
318    /// Close paragraph if one is currently open.
319    fn close_paragraph_if_open(&mut self) {
320        if self.is_paragraph_open() {
321            self.close_containers_to(self.containers.depth() - 1);
322        }
323    }
324
325    /// Prepare for a block-level element by flushing buffers and closing paragraphs.
326    /// This is a common pattern before starting tables, code blocks, divs, etc.
327    fn prepare_for_block_element(&mut self) {
328        self.emit_list_item_buffer_if_needed();
329        self.close_paragraph_if_open();
330    }
331
332    fn handle_footnote_open_effect(
333        &mut self,
334        block_match: &super::block_dispatcher::PreparedBlockMatch,
335        content: &str,
336    ) {
337        let content_start = block_match
338            .payload
339            .as_ref()
340            .and_then(|p| p.downcast_ref::<super::block_dispatcher::FootnoteDefinitionPrepared>())
341            .map(|p| p.content_start)
342            .unwrap_or(0);
343
344        while matches!(
345            self.containers.last(),
346            Some(Container::FootnoteDefinition { .. })
347        ) {
348            self.close_containers_to(self.containers.depth() - 1);
349        }
350
351        let content_col = 4;
352        self.containers
353            .push(Container::FootnoteDefinition { content_col });
354
355        if content_start > 0 {
356            let first_line_content = &content[content_start..];
357            if !first_line_content.trim().is_empty() {
358                paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
359                paragraphs::append_paragraph_line(
360                    &mut self.containers,
361                    &mut self.builder,
362                    first_line_content,
363                    self.config,
364                );
365            } else {
366                let (_, newline_str) = strip_newline(content);
367                if !newline_str.is_empty() {
368                    self.builder.token(SyntaxKind::NEWLINE.into(), newline_str);
369                }
370            }
371        }
372    }
373
374    fn handle_list_open_effect(
375        &mut self,
376        block_match: &super::block_dispatcher::PreparedBlockMatch,
377        content: &str,
378        indent_to_emit: Option<&str>,
379    ) {
380        use super::block_dispatcher::ListPrepared;
381
382        let prepared = block_match
383            .payload
384            .as_ref()
385            .and_then(|p| p.downcast_ref::<ListPrepared>());
386        let Some(prepared) = prepared else {
387            return;
388        };
389
390        if prepared.indent_cols >= 4 && !lists::in_list(&self.containers) {
391            paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
392            paragraphs::append_paragraph_line(
393                &mut self.containers,
394                &mut self.builder,
395                content,
396                self.config,
397            );
398            return;
399        }
400
401        if self.is_paragraph_open() {
402            if !block_match.detection.eq(&BlockDetectionResult::Yes) {
403                paragraphs::append_paragraph_line(
404                    &mut self.containers,
405                    &mut self.builder,
406                    content,
407                    self.config,
408                );
409                return;
410            }
411            self.close_containers_to(self.containers.depth() - 1);
412        }
413
414        if matches!(
415            self.containers.last(),
416            Some(Container::Definition {
417                plain_open: true,
418                ..
419            })
420        ) {
421            self.emit_buffered_plain_if_needed();
422        }
423
424        let matched_level = lists::find_matching_list_level(
425            &self.containers,
426            &prepared.marker,
427            prepared.indent_cols,
428        );
429        let list_item = ListItemEmissionInput {
430            content,
431            marker_len: prepared.marker_len,
432            spaces_after_cols: prepared.spaces_after_cols,
433            spaces_after_bytes: prepared.spaces_after,
434            indent_cols: prepared.indent_cols,
435            indent_bytes: prepared.indent_bytes,
436        };
437        let current_content_col = paragraphs::current_content_col(&self.containers);
438        let deep_ordered_matched_level = matched_level
439            .and_then(|level| self.containers.stack.get(level).map(|c| (level, c)))
440            .and_then(|(level, container)| match container {
441                Container::List {
442                    marker: list_marker,
443                    base_indent_cols,
444                    ..
445                } if matches!(
446                    (&prepared.marker, list_marker),
447                    (ListMarker::Ordered(_), ListMarker::Ordered(_))
448                ) && prepared.indent_cols >= 4
449                    && *base_indent_cols >= 4
450                    && prepared.indent_cols.abs_diff(*base_indent_cols) <= 3 =>
451                {
452                    Some(level)
453                }
454                _ => None,
455            });
456
457        if deep_ordered_matched_level.is_none()
458            && current_content_col > 0
459            && prepared.indent_cols >= current_content_col
460        {
461            if let Some(level) = matched_level
462                && let Some(Container::List {
463                    base_indent_cols, ..
464                }) = self.containers.stack.get(level)
465                && prepared.indent_cols == *base_indent_cols
466            {
467                let num_parent_lists = self.containers.stack[..level]
468                    .iter()
469                    .filter(|c| matches!(c, Container::List { .. }))
470                    .count();
471
472                if num_parent_lists > 0 {
473                    self.close_containers_to(level + 1);
474
475                    if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
476                        self.close_containers_to(self.containers.depth() - 1);
477                    }
478                    if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
479                        self.close_containers_to(self.containers.depth() - 1);
480                    }
481
482                    if let Some(indent_str) = indent_to_emit {
483                        self.builder
484                            .token(SyntaxKind::WHITESPACE.into(), indent_str);
485                    }
486
487                    if let Some(nested_marker) = prepared.nested_marker {
488                        lists::add_list_item_with_nested_empty_list(
489                            &mut self.containers,
490                            &mut self.builder,
491                            &list_item,
492                            nested_marker,
493                        );
494                    } else {
495                        lists::add_list_item(&mut self.containers, &mut self.builder, &list_item);
496                    }
497                    return;
498                }
499            }
500
501            self.emit_list_item_buffer_if_needed();
502
503            start_nested_list(
504                &mut self.containers,
505                &mut self.builder,
506                &prepared.marker,
507                &list_item,
508                indent_to_emit,
509            );
510            return;
511        }
512
513        if let Some(level) = matched_level {
514            self.close_containers_to(level + 1);
515
516            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
517                self.close_containers_to(self.containers.depth() - 1);
518            }
519            if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
520                self.close_containers_to(self.containers.depth() - 1);
521            }
522
523            if let Some(indent_str) = indent_to_emit {
524                self.builder
525                    .token(SyntaxKind::WHITESPACE.into(), indent_str);
526            }
527
528            if let Some(nested_marker) = prepared.nested_marker {
529                lists::add_list_item_with_nested_empty_list(
530                    &mut self.containers,
531                    &mut self.builder,
532                    &list_item,
533                    nested_marker,
534                );
535            } else {
536                lists::add_list_item(&mut self.containers, &mut self.builder, &list_item);
537            }
538            return;
539        }
540
541        if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
542            self.close_containers_to(self.containers.depth() - 1);
543        }
544        while matches!(self.containers.last(), Some(Container::ListItem { .. })) {
545            self.close_containers_to(self.containers.depth() - 1);
546        }
547        while matches!(self.containers.last(), Some(Container::List { .. })) {
548            self.close_containers_to(self.containers.depth() - 1);
549        }
550
551        self.builder.start_node(SyntaxKind::LIST.into());
552        if let Some(indent_str) = indent_to_emit {
553            self.builder
554                .token(SyntaxKind::WHITESPACE.into(), indent_str);
555        }
556        self.containers.push(Container::List {
557            marker: prepared.marker.clone(),
558            base_indent_cols: prepared.indent_cols,
559            has_blank_between_items: false,
560        });
561
562        if let Some(nested_marker) = prepared.nested_marker {
563            lists::add_list_item_with_nested_empty_list(
564                &mut self.containers,
565                &mut self.builder,
566                &list_item,
567                nested_marker,
568            );
569        } else {
570            lists::add_list_item(&mut self.containers, &mut self.builder, &list_item);
571        }
572    }
573
574    fn handle_definition_list_effect(
575        &mut self,
576        block_match: &super::block_dispatcher::PreparedBlockMatch,
577        content: &str,
578        indent_to_emit: Option<&str>,
579    ) {
580        use super::block_dispatcher::DefinitionPrepared;
581
582        let prepared = block_match
583            .payload
584            .as_ref()
585            .and_then(|p| p.downcast_ref::<DefinitionPrepared>());
586        let Some(prepared) = prepared else {
587            return;
588        };
589
590        match prepared {
591            DefinitionPrepared::Definition {
592                marker_char,
593                indent,
594                spaces_after,
595                spaces_after_cols,
596                has_content,
597            } => {
598                self.emit_buffered_plain_if_needed();
599
600                while matches!(self.containers.last(), Some(Container::ListItem { .. })) {
601                    self.close_containers_to(self.containers.depth() - 1);
602                }
603                while matches!(self.containers.last(), Some(Container::List { .. })) {
604                    self.close_containers_to(self.containers.depth() - 1);
605                }
606
607                if matches!(self.containers.last(), Some(Container::Definition { .. })) {
608                    self.close_containers_to(self.containers.depth() - 1);
609                }
610
611                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
612                    self.close_containers_to(self.containers.depth() - 1);
613                }
614
615                // A definition marker cannot start a new definition item without a term.
616                // If the preceding term/item was closed by a blank line but we are still
617                // inside the same definition list, reopen a definition item for continuation.
618                if definition_lists::in_definition_list(&self.containers)
619                    && !matches!(
620                        self.containers.last(),
621                        Some(Container::DefinitionItem { .. })
622                    )
623                {
624                    self.builder.start_node(SyntaxKind::DEFINITION_ITEM.into());
625                    self.containers.push(Container::DefinitionItem {});
626                }
627
628                if !definition_lists::in_definition_list(&self.containers) {
629                    self.builder.start_node(SyntaxKind::DEFINITION_LIST.into());
630                    self.containers.push(Container::DefinitionList {});
631                }
632
633                if !matches!(
634                    self.containers.last(),
635                    Some(Container::DefinitionItem { .. })
636                ) {
637                    self.builder.start_node(SyntaxKind::DEFINITION_ITEM.into());
638                    self.containers.push(Container::DefinitionItem {});
639                }
640
641                self.builder.start_node(SyntaxKind::DEFINITION.into());
642
643                if let Some(indent_str) = indent_to_emit {
644                    self.builder
645                        .token(SyntaxKind::WHITESPACE.into(), indent_str);
646                }
647
648                emit_definition_marker(&mut self.builder, *marker_char, *indent);
649                let indent_bytes = byte_index_at_column(content, *indent);
650                if *spaces_after > 0 {
651                    let space_start = indent_bytes + 1;
652                    let space_end = space_start + *spaces_after;
653                    if space_end <= content.len() {
654                        self.builder.token(
655                            SyntaxKind::WHITESPACE.into(),
656                            &content[space_start..space_end],
657                        );
658                    }
659                }
660
661                if !*has_content {
662                    let current_line = self.lines[self.pos];
663                    let (_, newline_str) = strip_newline(current_line);
664                    if !newline_str.is_empty() {
665                        self.builder.token(SyntaxKind::NEWLINE.into(), newline_str);
666                    }
667                }
668
669                let content_col = *indent + 1 + *spaces_after_cols;
670                let content_start_bytes = indent_bytes + 1 + *spaces_after;
671                let after_marker_and_spaces = content.get(content_start_bytes..).unwrap_or("");
672                let mut plain_buffer = TextBuffer::new();
673                let mut definition_pushed = false;
674
675                if *has_content {
676                    let current_line = self.lines[self.pos];
677                    let (trimmed_line, _) = strip_newline(current_line);
678
679                    let content_start = content_start_bytes.min(trimmed_line.len());
680                    let content_slice = &trimmed_line[content_start..];
681                    let content_line = &current_line[content_start_bytes.min(current_line.len())..];
682
683                    let (blockquote_depth, inner_blockquote_content) =
684                        count_blockquote_markers(content_line);
685
686                    let should_start_list_from_first_line = self
687                        .lines
688                        .get(self.pos + 1)
689                        .map(|next_line| {
690                            let (next_without_newline, _) = strip_newline(next_line);
691                            if next_without_newline.trim().is_empty() {
692                                return false;
693                            }
694
695                            let (next_indent_cols, _) = leading_indent(next_without_newline);
696                            next_indent_cols >= content_col
697                        })
698                        .unwrap_or(false);
699
700                    if blockquote_depth > 0 {
701                        self.containers.push(Container::Definition {
702                            content_col,
703                            plain_open: false,
704                            plain_buffer: TextBuffer::new(),
705                        });
706                        definition_pushed = true;
707
708                        let marker_info = parse_blockquote_marker_info(content_line);
709                        for level in 0..blockquote_depth {
710                            self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
711                            if let Some(info) = marker_info.get(level) {
712                                blockquotes::emit_one_blockquote_marker(
713                                    &mut self.builder,
714                                    info.leading_spaces,
715                                    info.has_trailing_space,
716                                );
717                            }
718                            self.containers.push(Container::BlockQuote {});
719                        }
720
721                        if !inner_blockquote_content.trim().is_empty() {
722                            paragraphs::start_paragraph_if_needed(
723                                &mut self.containers,
724                                &mut self.builder,
725                            );
726                            paragraphs::append_paragraph_line(
727                                &mut self.containers,
728                                &mut self.builder,
729                                inner_blockquote_content,
730                                self.config,
731                            );
732                        }
733                    } else if let Some(marker_match) =
734                        try_parse_list_marker(content_slice, self.config)
735                        && should_start_list_from_first_line
736                    {
737                        self.containers.push(Container::Definition {
738                            content_col,
739                            plain_open: false,
740                            plain_buffer: TextBuffer::new(),
741                        });
742                        definition_pushed = true;
743
744                        let (indent_cols, indent_bytes) = leading_indent(content_line);
745                        self.builder.start_node(SyntaxKind::LIST.into());
746                        self.containers.push(Container::List {
747                            marker: marker_match.marker.clone(),
748                            base_indent_cols: indent_cols,
749                            has_blank_between_items: false,
750                        });
751
752                        let list_item = ListItemEmissionInput {
753                            content: content_line,
754                            marker_len: marker_match.marker_len,
755                            spaces_after_cols: marker_match.spaces_after_cols,
756                            spaces_after_bytes: marker_match.spaces_after_bytes,
757                            indent_cols,
758                            indent_bytes,
759                        };
760
761                        if let Some(nested_marker) = is_content_nested_bullet_marker(
762                            content_line,
763                            marker_match.marker_len,
764                            marker_match.spaces_after_bytes,
765                        ) {
766                            lists::add_list_item_with_nested_empty_list(
767                                &mut self.containers,
768                                &mut self.builder,
769                                &list_item,
770                                nested_marker,
771                            );
772                        } else {
773                            lists::add_list_item(
774                                &mut self.containers,
775                                &mut self.builder,
776                                &list_item,
777                            );
778                        }
779                    } else if let Some(fence) = code_blocks::try_parse_fence_open(content_slice) {
780                        self.containers.push(Container::Definition {
781                            content_col,
782                            plain_open: false,
783                            plain_buffer: TextBuffer::new(),
784                        });
785                        definition_pushed = true;
786
787                        let bq_depth = self.current_blockquote_depth();
788                        if let Some(indent_str) = indent_to_emit {
789                            self.builder
790                                .token(SyntaxKind::WHITESPACE.into(), indent_str);
791                        }
792                        let fence_line = current_line[content_start..].to_string();
793                        let new_pos = if self.config.extensions.tex_math_gfm
794                            && code_blocks::is_gfm_math_fence(&fence)
795                        {
796                            code_blocks::parse_fenced_math_block(
797                                &mut self.builder,
798                                &self.lines,
799                                self.pos,
800                                fence,
801                                bq_depth,
802                                content_col,
803                                Some(&fence_line),
804                            )
805                        } else {
806                            code_blocks::parse_fenced_code_block(
807                                &mut self.builder,
808                                &self.lines,
809                                self.pos,
810                                fence,
811                                bq_depth,
812                                content_col,
813                                Some(&fence_line),
814                            )
815                        };
816                        self.pos = new_pos - 1;
817                    } else {
818                        let (_, newline_str) = strip_newline(current_line);
819                        let (content_without_newline, _) = strip_newline(after_marker_and_spaces);
820                        if content_without_newline.is_empty() {
821                            plain_buffer.push_line(newline_str);
822                        } else {
823                            let line_with_newline = if !newline_str.is_empty() {
824                                format!("{}{}", content_without_newline, newline_str)
825                            } else {
826                                content_without_newline.to_string()
827                            };
828                            plain_buffer.push_line(line_with_newline);
829                        }
830                    }
831                }
832
833                if !definition_pushed {
834                    self.containers.push(Container::Definition {
835                        content_col,
836                        plain_open: *has_content,
837                        plain_buffer,
838                    });
839                }
840            }
841            DefinitionPrepared::Term { blank_count } => {
842                self.emit_buffered_plain_if_needed();
843
844                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
845                    self.close_containers_to(self.containers.depth() - 1);
846                }
847
848                if !definition_lists::in_definition_list(&self.containers) {
849                    self.builder.start_node(SyntaxKind::DEFINITION_LIST.into());
850                    self.containers.push(Container::DefinitionList {});
851                }
852
853                while matches!(
854                    self.containers.last(),
855                    Some(Container::Definition { .. }) | Some(Container::DefinitionItem { .. })
856                ) {
857                    self.close_containers_to(self.containers.depth() - 1);
858                }
859
860                self.builder.start_node(SyntaxKind::DEFINITION_ITEM.into());
861                self.containers.push(Container::DefinitionItem {});
862
863                emit_term(&mut self.builder, content, self.config);
864
865                for i in 0..*blank_count {
866                    let blank_pos = self.pos + 1 + i;
867                    if blank_pos < self.lines.len() {
868                        let blank_line = self.lines[blank_pos];
869                        self.builder.start_node(SyntaxKind::BLANK_LINE.into());
870                        self.builder
871                            .token(SyntaxKind::BLANK_LINE.into(), blank_line);
872                        self.builder.finish_node();
873                    }
874                }
875                self.pos += *blank_count;
876            }
877        }
878    }
879
880    /// Get current blockquote depth from container stack.
881    fn blockquote_marker_info(
882        &self,
883        payload: Option<&BlockQuotePrepared>,
884        line: &str,
885    ) -> Vec<marker_utils::BlockQuoteMarkerInfo> {
886        payload
887            .map(|payload| payload.marker_info.clone())
888            .unwrap_or_else(|| parse_blockquote_marker_info(line))
889    }
890
891    fn emit_blockquote_markers(
892        &mut self,
893        marker_info: &[marker_utils::BlockQuoteMarkerInfo],
894        depth: usize,
895    ) {
896        for i in 0..depth {
897            if let Some(info) = marker_info.get(i) {
898                blockquotes::emit_one_blockquote_marker(
899                    &mut self.builder,
900                    info.leading_spaces,
901                    info.has_trailing_space,
902                );
903            }
904        }
905    }
906
907    fn current_blockquote_depth(&self) -> usize {
908        blockquotes::current_blockquote_depth(&self.containers)
909    }
910
911    /// Emit or buffer a blockquote marker depending on parser state.
912    ///
913    /// If a paragraph is open and we're using integrated parsing, buffer the marker.
914    /// Otherwise emit it directly to the builder.
915    fn emit_or_buffer_blockquote_marker(
916        &mut self,
917        leading_spaces: usize,
918        has_trailing_space: bool,
919    ) {
920        // If paragraph is open, buffer the marker (it will be emitted at correct position)
921        if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
922            // Buffer the marker in the paragraph
923            paragraphs::append_paragraph_marker(
924                &mut self.containers,
925                leading_spaces,
926                has_trailing_space,
927            );
928        } else {
929            // Emit directly
930            blockquotes::emit_one_blockquote_marker(
931                &mut self.builder,
932                leading_spaces,
933                has_trailing_space,
934            );
935        }
936    }
937
938    fn parse_document_stack(&mut self) {
939        self.builder.start_node(SyntaxKind::DOCUMENT.into());
940
941        log::debug!("Starting document parse");
942
943        // Pandoc title block is handled via the block dispatcher.
944
945        while self.pos < self.lines.len() {
946            let line = self.lines[self.pos];
947
948            log::debug!("Parsing line {}: {}", self.pos + 1, line);
949
950            if self.parse_line(line) {
951                continue;
952            }
953            self.pos += 1;
954        }
955
956        self.close_containers_to(0);
957        self.builder.finish_node(); // DOCUMENT
958    }
959
960    /// Returns true if the line was consumed.
961    fn parse_line(&mut self, line: &str) -> bool {
962        // Count blockquote markers on this line
963        let (bq_depth, inner_content) = count_blockquote_markers(line);
964        let current_bq_depth = self.current_blockquote_depth();
965
966        let has_blank_before = self.pos == 0 || self.lines[self.pos - 1].trim().is_empty();
967        let mut blockquote_match: Option<PreparedBlockMatch> = None;
968        let dispatcher_ctx = if current_bq_depth == 0 {
969            Some(BlockContext {
970                content: line,
971                has_blank_before,
972                has_blank_before_strict: has_blank_before,
973                at_document_start: self.pos == 0,
974                in_fenced_div: self.in_fenced_div(),
975                blockquote_depth: current_bq_depth,
976                config: self.config,
977                content_indent: 0,
978                indent_to_emit: None,
979                list_indent_info: None,
980                in_list: lists::in_list(&self.containers),
981                next_line: if self.pos + 1 < self.lines.len() {
982                    Some(self.lines[self.pos + 1])
983                } else {
984                    None
985                },
986            })
987        } else {
988            None
989        };
990
991        let blockquote_payload = if let Some(dispatcher_ctx) = dispatcher_ctx.as_ref() {
992            self.block_registry
993                .detect_prepared(dispatcher_ctx, &self.lines, self.pos)
994                .and_then(|prepared| {
995                    if matches!(prepared.effect, BlockEffect::OpenBlockQuote) {
996                        blockquote_match = Some(prepared);
997                        blockquote_match.as_ref().and_then(|prepared| {
998                            prepared
999                                .payload
1000                                .as_ref()
1001                                .and_then(|payload| payload.downcast_ref::<BlockQuotePrepared>())
1002                                .cloned()
1003                        })
1004                    } else {
1005                        None
1006                    }
1007                })
1008        } else {
1009            None
1010        };
1011
1012        log::debug!(
1013            "parse_line [{}]: bq_depth={}, current_bq={}, depth={}, line={:?}",
1014            self.pos,
1015            bq_depth,
1016            current_bq_depth,
1017            self.containers.depth(),
1018            line.trim_end()
1019        );
1020
1021        // Handle blank lines specially (including blank lines inside blockquotes)
1022        // A line like ">" with nothing after is a blank line inside a blockquote
1023        let is_blank = line.trim_end_matches('\n').trim().is_empty()
1024            || (bq_depth > 0 && inner_content.trim_end_matches('\n').trim().is_empty());
1025
1026        if is_blank {
1027            if self.is_paragraph_open()
1028                && paragraphs::has_open_inline_math_environment(&self.containers)
1029            {
1030                paragraphs::append_paragraph_line(
1031                    &mut self.containers,
1032                    &mut self.builder,
1033                    line,
1034                    self.config,
1035                );
1036                self.pos += 1;
1037                return true;
1038            }
1039
1040            // Close paragraph if open
1041            self.close_paragraph_if_open();
1042
1043            // Close Plain node in Definition if open
1044            // Blank lines should close Plain, allowing subsequent content to be siblings
1045            // Emit buffered PLAIN content before continuing
1046            self.emit_buffered_plain_if_needed();
1047
1048            // Note: Blank lines between terms and definitions are now preserved
1049            // and emitted as part of the term parsing logic
1050
1051            // For blank lines inside blockquotes, we need to handle them at the right depth
1052            // First, adjust blockquote depth if needed
1053            if bq_depth > current_bq_depth {
1054                // Open blockquotes
1055                for _ in current_bq_depth..bq_depth {
1056                    self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
1057                    self.containers.push(Container::BlockQuote {});
1058                }
1059            } else if bq_depth < current_bq_depth {
1060                // Close blockquotes down to bq_depth (must use Parser close to emit buffers)
1061                self.close_blockquotes_to_depth(bq_depth);
1062            }
1063
1064            // Peek ahead to determine what containers to keep open
1065            let mut peek = self.pos + 1;
1066            while peek < self.lines.len() && self.lines[peek].trim().is_empty() {
1067                peek += 1;
1068            }
1069
1070            // Determine what containers to keep open based on next line
1071            let levels_to_keep = if peek < self.lines.len() {
1072                ContinuationPolicy::new(self.config, &self.block_registry).compute_levels_to_keep(
1073                    self.current_blockquote_depth(),
1074                    &self.containers,
1075                    &self.lines,
1076                    peek,
1077                    self.lines[peek],
1078                )
1079            } else {
1080                0
1081            };
1082            log::trace!(
1083                "Blank line: depth={}, levels_to_keep={}, next='{}'",
1084                self.containers.depth(),
1085                levels_to_keep,
1086                if peek < self.lines.len() {
1087                    self.lines[peek]
1088                } else {
1089                    "<EOF>"
1090                }
1091            );
1092
1093            // Check if blank line should be buffered in a ListItem BEFORE closing containers
1094
1095            // Close containers down to the level we want to keep
1096            while self.containers.depth() > levels_to_keep {
1097                match self.containers.last() {
1098                    Some(Container::ListItem { .. }) => {
1099                        // levels_to_keep wants to close the ListItem - blank line is between items
1100                        log::debug!(
1101                            "Closing ListItem at blank line (levels_to_keep={} < depth={})",
1102                            levels_to_keep,
1103                            self.containers.depth()
1104                        );
1105                        self.close_containers_to(self.containers.depth() - 1);
1106                    }
1107                    Some(Container::List { .. })
1108                    | Some(Container::FootnoteDefinition { .. })
1109                    | Some(Container::Alert { .. })
1110                    | Some(Container::Paragraph { .. })
1111                    | Some(Container::Definition { .. })
1112                    | Some(Container::DefinitionItem { .. })
1113                    | Some(Container::DefinitionList { .. }) => {
1114                        log::debug!(
1115                            "Closing {:?} at blank line (depth {} > levels_to_keep {})",
1116                            self.containers.last(),
1117                            self.containers.depth(),
1118                            levels_to_keep
1119                        );
1120
1121                        self.close_containers_to(self.containers.depth() - 1);
1122                    }
1123                    _ => break,
1124                }
1125            }
1126
1127            // If we kept a list item open, its first-line text may still be buffered.
1128            // Flush it *before* emitting the blank line node (and its blockquote markers)
1129            // so byte order matches the source.
1130            if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
1131                self.emit_list_item_buffer_if_needed();
1132            }
1133
1134            // Emit blockquote markers for this blank line if inside blockquotes
1135            if bq_depth > 0 {
1136                let marker_info = self.blockquote_marker_info(blockquote_payload.as_ref(), line);
1137                self.emit_blockquote_markers(&marker_info, bq_depth);
1138            }
1139
1140            self.builder.start_node(SyntaxKind::BLANK_LINE.into());
1141            self.builder
1142                .token(SyntaxKind::BLANK_LINE.into(), inner_content);
1143            self.builder.finish_node();
1144
1145            self.pos += 1;
1146            return true;
1147        }
1148
1149        // Handle blockquote depth changes
1150        if bq_depth > current_bq_depth {
1151            // Need to open new blockquote(s)
1152            // But first check blank_before_blockquote requirement
1153            if self.config.extensions.blank_before_blockquote
1154                && current_bq_depth == 0
1155                && !blockquote_payload
1156                    .as_ref()
1157                    .map(|payload| payload.can_start)
1158                    .unwrap_or_else(|| blockquotes::can_start_blockquote(self.pos, &self.lines))
1159            {
1160                // Can't start blockquote without blank line - treat as paragraph
1161                paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
1162                paragraphs::append_paragraph_line(
1163                    &mut self.containers,
1164                    &mut self.builder,
1165                    line,
1166                    self.config,
1167                );
1168                self.pos += 1;
1169                return true;
1170            }
1171
1172            // For nested blockquotes, also need blank line before (blank_before_blockquote)
1173            // Check if previous line inside the blockquote was blank
1174            let can_nest = if current_bq_depth > 0 {
1175                if self.config.extensions.blank_before_blockquote {
1176                    // Check if we're right after a blank line or at start of blockquote
1177                    matches!(self.containers.last(), Some(Container::BlockQuote { .. }))
1178                        || (self.pos > 0 && {
1179                            let prev_line = self.lines[self.pos - 1];
1180                            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
1181                            prev_bq_depth >= current_bq_depth && prev_inner.trim().is_empty()
1182                        })
1183                } else {
1184                    true
1185                }
1186            } else {
1187                blockquote_payload
1188                    .as_ref()
1189                    .map(|payload| payload.can_nest)
1190                    .unwrap_or(true)
1191            };
1192
1193            if !can_nest {
1194                // Can't nest deeper - treat extra > as content
1195                // Only strip markers up to current depth
1196                let content_at_current_depth =
1197                    blockquotes::strip_n_blockquote_markers(line, current_bq_depth);
1198
1199                // Emit blockquote markers for current depth (for losslessness)
1200                let marker_info = self.blockquote_marker_info(blockquote_payload.as_ref(), line);
1201                for i in 0..current_bq_depth {
1202                    if let Some(info) = marker_info.get(i) {
1203                        self.emit_or_buffer_blockquote_marker(
1204                            info.leading_spaces,
1205                            info.has_trailing_space,
1206                        );
1207                    }
1208                }
1209
1210                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1211                    // Lazy continuation with the extra > as content
1212                    paragraphs::append_paragraph_line(
1213                        &mut self.containers,
1214                        &mut self.builder,
1215                        content_at_current_depth,
1216                        self.config,
1217                    );
1218                    self.pos += 1;
1219                    return true;
1220                } else {
1221                    // Start new paragraph with the extra > as content
1222                    paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
1223                    paragraphs::append_paragraph_line(
1224                        &mut self.containers,
1225                        &mut self.builder,
1226                        content_at_current_depth,
1227                        self.config,
1228                    );
1229                    self.pos += 1;
1230                    return true;
1231                }
1232            }
1233
1234            // Close paragraph before opening blockquote
1235            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1236                self.close_containers_to(self.containers.depth() - 1);
1237            }
1238
1239            // Parse marker information for all levels
1240            let marker_info = self.blockquote_marker_info(blockquote_payload.as_ref(), line);
1241
1242            if let (Some(dispatcher_ctx), Some(prepared)) =
1243                (dispatcher_ctx.as_ref(), blockquote_match.as_ref())
1244            {
1245                let _ = self.block_registry.parse_prepared(
1246                    prepared,
1247                    dispatcher_ctx,
1248                    &mut self.builder,
1249                    &self.lines,
1250                    self.pos,
1251                );
1252                for _ in 0..bq_depth {
1253                    self.containers.push(Container::BlockQuote {});
1254                }
1255            } else {
1256                // First, emit markers for existing blockquote levels (before opening new ones)
1257                for level in 0..current_bq_depth {
1258                    if let Some(info) = marker_info.get(level) {
1259                        self.emit_or_buffer_blockquote_marker(
1260                            info.leading_spaces,
1261                            info.has_trailing_space,
1262                        );
1263                    }
1264                }
1265
1266                // Then open new blockquotes and emit their markers
1267                for level in current_bq_depth..bq_depth {
1268                    self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
1269
1270                    // Emit the marker for this new level
1271                    if let Some(info) = marker_info.get(level) {
1272                        blockquotes::emit_one_blockquote_marker(
1273                            &mut self.builder,
1274                            info.leading_spaces,
1275                            info.has_trailing_space,
1276                        );
1277                    }
1278
1279                    self.containers.push(Container::BlockQuote {});
1280                }
1281            }
1282
1283            // Now parse the inner content
1284            // Pass inner_content as line_to_append since markers are already stripped
1285            return self.parse_inner_content(inner_content, Some(inner_content));
1286        } else if bq_depth < current_bq_depth {
1287            // Need to close some blockquotes, but first check for lazy continuation
1288            // Lazy continuation: line without > continues content in a blockquote
1289            if bq_depth == 0 {
1290                // Check for lazy paragraph continuation
1291                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1292                    paragraphs::append_paragraph_line(
1293                        &mut self.containers,
1294                        &mut self.builder,
1295                        line,
1296                        self.config,
1297                    );
1298                    self.pos += 1;
1299                    return true;
1300                }
1301
1302                // Check for lazy list continuation - if we're in a list item and
1303                // this line looks like a list item with matching marker
1304                if lists::in_blockquote_list(&self.containers)
1305                    && let Some(marker_match) = try_parse_list_marker(line, self.config)
1306                {
1307                    let (indent_cols, indent_bytes) = leading_indent(line);
1308                    if let Some(level) = lists::find_matching_list_level(
1309                        &self.containers,
1310                        &marker_match.marker,
1311                        indent_cols,
1312                    ) {
1313                        // Continue the list inside the blockquote
1314                        // Close containers to the target level, emitting buffers properly
1315                        self.close_containers_to(level + 1);
1316
1317                        // Close any open paragraph or list item at this level
1318                        if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1319                            self.close_containers_to(self.containers.depth() - 1);
1320                        }
1321                        if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
1322                            self.close_containers_to(self.containers.depth() - 1);
1323                        }
1324
1325                        // Check if content is a nested bullet marker
1326                        if let Some(nested_marker) = is_content_nested_bullet_marker(
1327                            line,
1328                            marker_match.marker_len,
1329                            marker_match.spaces_after_bytes,
1330                        ) {
1331                            let list_item = ListItemEmissionInput {
1332                                content: line,
1333                                marker_len: marker_match.marker_len,
1334                                spaces_after_cols: marker_match.spaces_after_cols,
1335                                spaces_after_bytes: marker_match.spaces_after_bytes,
1336                                indent_cols,
1337                                indent_bytes,
1338                            };
1339                            lists::add_list_item_with_nested_empty_list(
1340                                &mut self.containers,
1341                                &mut self.builder,
1342                                &list_item,
1343                                nested_marker,
1344                            );
1345                        } else {
1346                            let list_item = ListItemEmissionInput {
1347                                content: line,
1348                                marker_len: marker_match.marker_len,
1349                                spaces_after_cols: marker_match.spaces_after_cols,
1350                                spaces_after_bytes: marker_match.spaces_after_bytes,
1351                                indent_cols,
1352                                indent_bytes,
1353                            };
1354                            lists::add_list_item(
1355                                &mut self.containers,
1356                                &mut self.builder,
1357                                &list_item,
1358                            );
1359                        }
1360                        self.pos += 1;
1361                        return true;
1362                    }
1363                }
1364            }
1365
1366            // Not lazy continuation - close paragraph if open
1367            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1368                self.close_containers_to(self.containers.depth() - 1);
1369            }
1370
1371            // Close blockquotes down to the new depth (must use Parser close to emit buffers)
1372            self.close_blockquotes_to_depth(bq_depth);
1373
1374            // Parse the inner content at the new depth
1375            if bq_depth > 0 {
1376                // Emit markers at current depth before parsing content
1377                let marker_info = parse_blockquote_marker_info(line);
1378                for i in 0..bq_depth {
1379                    if let Some(info) = marker_info.get(i) {
1380                        self.emit_or_buffer_blockquote_marker(
1381                            info.leading_spaces,
1382                            info.has_trailing_space,
1383                        );
1384                    }
1385                }
1386                // Content with markers stripped - use inner_content for paragraph appending
1387                return self.parse_inner_content(inner_content, Some(inner_content));
1388            } else {
1389                // Not inside blockquotes - use original line
1390                return self.parse_inner_content(line, None);
1391            }
1392        } else if bq_depth > 0 {
1393            // Same blockquote depth - emit markers and continue parsing inner content
1394            let mut list_item_continuation = false;
1395
1396            // Check if we should close the ListItem
1397            // ListItem should continue if the line is properly indented for continuation
1398            if matches!(
1399                self.containers.last(),
1400                Some(Container::ListItem { content_col: _, .. })
1401            ) {
1402                let (indent_cols, _) = leading_indent(inner_content);
1403                let content_indent = self.content_container_indent_to_strip();
1404                let effective_indent = indent_cols.saturating_sub(content_indent);
1405                let content_col = match self.containers.last() {
1406                    Some(Container::ListItem { content_col, .. }) => *content_col,
1407                    _ => 0,
1408                };
1409
1410                // Check if this line starts a new list item at outer level
1411                let is_new_item_at_outer_level =
1412                    if try_parse_list_marker(inner_content, self.config).is_some() {
1413                        effective_indent < content_col
1414                    } else {
1415                        false
1416                    };
1417
1418                // Close ListItem if:
1419                // 1. It's a new list item at an outer (or same) level, OR
1420                // 2. The line is not indented enough to continue the current item
1421                if is_new_item_at_outer_level || effective_indent < content_col {
1422                    log::debug!(
1423                        "Closing ListItem: is_new_item={}, effective_indent={} < content_col={}",
1424                        is_new_item_at_outer_level,
1425                        effective_indent,
1426                        content_col
1427                    );
1428                    self.close_containers_to(self.containers.depth() - 1);
1429                } else {
1430                    log::debug!(
1431                        "Keeping ListItem: effective_indent={} >= content_col={}",
1432                        effective_indent,
1433                        content_col
1434                    );
1435                    list_item_continuation = true;
1436                }
1437            }
1438
1439            // Fenced code blocks inside list items need marker emission in this branch.
1440            // If we keep continuation buffering for these lines, opening fence markers in
1441            // blockquote contexts can be dropped from CST text.
1442            if list_item_continuation && code_blocks::try_parse_fence_open(inner_content).is_some()
1443            {
1444                list_item_continuation = false;
1445            }
1446
1447            if !list_item_continuation {
1448                let marker_info = parse_blockquote_marker_info(line);
1449                for i in 0..bq_depth {
1450                    if let Some(info) = marker_info.get(i) {
1451                        self.emit_or_buffer_blockquote_marker(
1452                            info.leading_spaces,
1453                            info.has_trailing_space,
1454                        );
1455                    }
1456                }
1457            }
1458            // When continuing a list item inside a blockquote, keep original line bytes in the
1459            // list-item buffer and avoid emitting separate marker tokens here.
1460            let line_to_append = if list_item_continuation {
1461                Some(line)
1462            } else {
1463                Some(inner_content)
1464            };
1465            return self.parse_inner_content(inner_content, line_to_append);
1466        }
1467
1468        // No blockquote markers - parse as regular content
1469        // But check for lazy continuation first
1470        if current_bq_depth > 0 {
1471            // Check for lazy paragraph continuation
1472            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1473                paragraphs::append_paragraph_line(
1474                    &mut self.containers,
1475                    &mut self.builder,
1476                    line,
1477                    self.config,
1478                );
1479                self.pos += 1;
1480                return true;
1481            }
1482
1483            // Check for lazy list continuation
1484            if lists::in_blockquote_list(&self.containers)
1485                && let Some(marker_match) = try_parse_list_marker(line, self.config)
1486            {
1487                let (indent_cols, indent_bytes) = leading_indent(line);
1488                if let Some(level) = lists::find_matching_list_level(
1489                    &self.containers,
1490                    &marker_match.marker,
1491                    indent_cols,
1492                ) {
1493                    // Close containers to the target level, emitting buffers properly
1494                    self.close_containers_to(level + 1);
1495
1496                    // Close any open paragraph or list item at this level
1497                    if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1498                        self.close_containers_to(self.containers.depth() - 1);
1499                    }
1500                    if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
1501                        self.close_containers_to(self.containers.depth() - 1);
1502                    }
1503
1504                    // Check if content is a nested bullet marker
1505                    if let Some(nested_marker) = is_content_nested_bullet_marker(
1506                        line,
1507                        marker_match.marker_len,
1508                        marker_match.spaces_after_bytes,
1509                    ) {
1510                        let list_item = ListItemEmissionInput {
1511                            content: line,
1512                            marker_len: marker_match.marker_len,
1513                            spaces_after_cols: marker_match.spaces_after_cols,
1514                            spaces_after_bytes: marker_match.spaces_after_bytes,
1515                            indent_cols,
1516                            indent_bytes,
1517                        };
1518                        lists::add_list_item_with_nested_empty_list(
1519                            &mut self.containers,
1520                            &mut self.builder,
1521                            &list_item,
1522                            nested_marker,
1523                        );
1524                    } else {
1525                        let list_item = ListItemEmissionInput {
1526                            content: line,
1527                            marker_len: marker_match.marker_len,
1528                            spaces_after_cols: marker_match.spaces_after_cols,
1529                            spaces_after_bytes: marker_match.spaces_after_bytes,
1530                            indent_cols,
1531                            indent_bytes,
1532                        };
1533                        lists::add_list_item(&mut self.containers, &mut self.builder, &list_item);
1534                    }
1535                    self.pos += 1;
1536                    return true;
1537                }
1538            }
1539        }
1540
1541        // No blockquote markers - use original line
1542        self.parse_inner_content(line, None)
1543    }
1544
1545    /// Get the total indentation to strip from content containers (footnotes + definitions).
1546    fn content_container_indent_to_strip(&self) -> usize {
1547        self.containers
1548            .stack
1549            .iter()
1550            .filter_map(|c| match c {
1551                Container::FootnoteDefinition { content_col, .. } => Some(*content_col),
1552                Container::Definition { content_col, .. } => Some(*content_col),
1553                _ => None,
1554            })
1555            .sum()
1556    }
1557
1558    /// Parse content inside blockquotes (or at top level).
1559    ///
1560    /// `content` - The content to parse (may have indent/markers stripped)
1561    /// `line_to_append` - Optional line to use when appending to paragraphs.
1562    ///                    If None, uses self.lines[self.pos]
1563    fn parse_inner_content(&mut self, content: &str, line_to_append: Option<&str>) -> bool {
1564        log::debug!(
1565            "parse_inner_content [{}]: depth={}, last={:?}, content={:?}",
1566            self.pos,
1567            self.containers.depth(),
1568            self.containers.last(),
1569            content.trim_end()
1570        );
1571        // Calculate how much indentation should be stripped for content containers
1572        // (definitions, footnotes) FIRST, so we can check for block markers correctly
1573        let content_indent = self.content_container_indent_to_strip();
1574        let (stripped_content, indent_to_emit) = if content_indent > 0 {
1575            let (indent_cols, _) = leading_indent(content);
1576            if indent_cols >= content_indent {
1577                let idx = byte_index_at_column(content, content_indent);
1578                (&content[idx..], Some(&content[..idx]))
1579            } else {
1580                // Line has less indent than required - preserve leading whitespace
1581                let trimmed_start = content.trim_start();
1582                let ws_len = content.len() - trimmed_start.len();
1583                if ws_len > 0 {
1584                    (trimmed_start, Some(&content[..ws_len]))
1585                } else {
1586                    (content, None)
1587                }
1588            }
1589        } else {
1590            (content, None)
1591        };
1592
1593        if self.config.extensions.alerts
1594            && self.current_blockquote_depth() > 0
1595            && !self.in_active_alert()
1596            && !self.is_paragraph_open()
1597            && let Some(marker) = Self::alert_marker_from_content(stripped_content)
1598        {
1599            let (_, newline_str) = strip_newline(stripped_content);
1600            self.builder.start_node(SyntaxKind::ALERT.into());
1601            self.builder.token(SyntaxKind::ALERT_MARKER.into(), marker);
1602            if !newline_str.is_empty() {
1603                self.builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1604            }
1605            self.containers.push(Container::Alert {
1606                blockquote_depth: self.current_blockquote_depth(),
1607            });
1608            self.pos += 1;
1609            return true;
1610        }
1611
1612        // Check if we're in a Definition container (with or without an open PLAIN)
1613        // Continuation lines should be added to PLAIN, not treated as new blocks
1614        // BUT: Don't treat lines with block element markers as continuations
1615        if matches!(self.containers.last(), Some(Container::Definition { .. })) {
1616            let is_definition_marker =
1617                definition_lists::try_parse_definition_marker(stripped_content).is_some()
1618                    && !stripped_content.starts_with(':');
1619            if content_indent == 0 && is_definition_marker {
1620                // Definition markers at top-level should start a new definition.
1621            } else {
1622                let policy = ContinuationPolicy::new(self.config, &self.block_registry);
1623
1624                if policy.definition_plain_can_continue(
1625                    stripped_content,
1626                    content,
1627                    content_indent,
1628                    &BlockContext {
1629                        content: stripped_content,
1630                        has_blank_before: self.pos == 0
1631                            || self.lines[self.pos - 1].trim().is_empty(),
1632                        has_blank_before_strict: self.pos == 0
1633                            || self.lines[self.pos - 1].trim().is_empty(),
1634                        at_document_start: self.pos == 0 && self.current_blockquote_depth() == 0,
1635                        in_fenced_div: self.in_fenced_div(),
1636                        blockquote_depth: self.current_blockquote_depth(),
1637                        config: self.config,
1638                        content_indent,
1639                        indent_to_emit: None,
1640                        list_indent_info: None,
1641                        in_list: lists::in_list(&self.containers),
1642                        next_line: if self.pos + 1 < self.lines.len() {
1643                            Some(self.lines[self.pos + 1])
1644                        } else {
1645                            None
1646                        },
1647                    },
1648                    &self.lines,
1649                    self.pos,
1650                ) {
1651                    let content_line = stripped_content;
1652                    let (text_without_newline, newline_str) = strip_newline(content_line);
1653                    let indent_prefix = if !text_without_newline.trim().is_empty() {
1654                        indent_to_emit.unwrap_or("")
1655                    } else {
1656                        ""
1657                    };
1658                    let content_line = format!("{}{}", indent_prefix, text_without_newline);
1659
1660                    if let Some(Container::Definition {
1661                        plain_open,
1662                        plain_buffer,
1663                        ..
1664                    }) = self.containers.stack.last_mut()
1665                    {
1666                        let line_with_newline = if !newline_str.is_empty() {
1667                            format!("{}{}", content_line, newline_str)
1668                        } else {
1669                            content_line
1670                        };
1671                        plain_buffer.push_line(line_with_newline);
1672                        *plain_open = true;
1673                    }
1674
1675                    self.pos += 1;
1676                    return true;
1677                }
1678            }
1679        }
1680
1681        // Handle blockquotes that appear after stripping content-container indentation
1682        // (e.g. `    > quote` inside a definition list item).
1683        if content_indent > 0 {
1684            let (bq_depth, inner_content) = count_blockquote_markers(stripped_content);
1685            let current_bq_depth = self.current_blockquote_depth();
1686
1687            if bq_depth > 0 {
1688                // If definition/list plain text is buffered, flush it before opening nested
1689                // blockquotes so block order remains lossless and stable across reparse.
1690                self.emit_buffered_plain_if_needed();
1691                self.emit_list_item_buffer_if_needed();
1692
1693                // Blockquotes can nest inside content containers; preserve the stripped indentation
1694                // as WHITESPACE before the first marker for losslessness.
1695                self.close_paragraph_if_open();
1696
1697                if bq_depth > current_bq_depth {
1698                    let marker_info = parse_blockquote_marker_info(stripped_content);
1699
1700                    // Open new blockquotes and emit their markers.
1701                    for level in current_bq_depth..bq_depth {
1702                        self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
1703
1704                        if level == current_bq_depth
1705                            && let Some(indent_str) = indent_to_emit
1706                        {
1707                            self.builder
1708                                .token(SyntaxKind::WHITESPACE.into(), indent_str);
1709                        }
1710
1711                        if let Some(info) = marker_info.get(level) {
1712                            blockquotes::emit_one_blockquote_marker(
1713                                &mut self.builder,
1714                                info.leading_spaces,
1715                                info.has_trailing_space,
1716                            );
1717                        }
1718
1719                        self.containers.push(Container::BlockQuote {});
1720                    }
1721                } else if bq_depth < current_bq_depth {
1722                    self.close_blockquotes_to_depth(bq_depth);
1723                } else {
1724                    // Same depth: emit markers for losslessness.
1725                    let marker_info = parse_blockquote_marker_info(stripped_content);
1726                    self.emit_blockquote_markers(&marker_info, bq_depth);
1727                }
1728
1729                return self.parse_inner_content(inner_content, Some(inner_content));
1730            }
1731        }
1732
1733        // Store the stripped content for later use
1734        let content = stripped_content;
1735
1736        if self.is_paragraph_open()
1737            && paragraphs::has_open_inline_math_environment(&self.containers)
1738        {
1739            paragraphs::append_paragraph_line(
1740                &mut self.containers,
1741                &mut self.builder,
1742                line_to_append.unwrap_or(self.lines[self.pos]),
1743                self.config,
1744            );
1745            self.pos += 1;
1746            return true;
1747        }
1748
1749        // Precompute dispatcher match once per line (reused by multiple branches below).
1750        // This covers: blocks requiring blank lines, blocks that can interrupt paragraphs,
1751        // and blocks that can appear without blank lines (e.g. reference definitions).
1752        use super::blocks::lists;
1753        use super::blocks::paragraphs;
1754        let list_indent_info = if lists::in_list(&self.containers) {
1755            let content_col = paragraphs::current_content_col(&self.containers);
1756            if content_col > 0 {
1757                Some(super::block_dispatcher::ListIndentInfo { content_col })
1758            } else {
1759                None
1760            }
1761        } else {
1762            None
1763        };
1764
1765        let next_line = if self.pos + 1 < self.lines.len() {
1766            // For lookahead-based blocks (e.g. setext headings), the dispatcher expects
1767            // `ctx.next_line` to be in the same “inner content” form as `ctx.content`.
1768            Some(count_blockquote_markers(self.lines[self.pos + 1]).1)
1769        } else {
1770            None
1771        };
1772
1773        let current_bq_depth = self.current_blockquote_depth();
1774        if let Some(alert_bq_depth) = self.active_alert_blockquote_depth()
1775            && current_bq_depth < alert_bq_depth
1776        {
1777            while matches!(self.containers.last(), Some(Container::Alert { .. })) {
1778                self.close_containers_to(self.containers.depth() - 1);
1779            }
1780        }
1781
1782        let dispatcher_ctx = BlockContext {
1783            content,
1784            has_blank_before: false,        // filled in later
1785            has_blank_before_strict: false, // filled in later
1786            at_document_start: false,       // filled in later
1787            in_fenced_div: self.in_fenced_div(),
1788            blockquote_depth: current_bq_depth,
1789            config: self.config,
1790            content_indent,
1791            indent_to_emit,
1792            list_indent_info,
1793            in_list: lists::in_list(&self.containers),
1794            next_line,
1795        };
1796
1797        // We'll update these two fields shortly (after they are computed), but we can still
1798        // use this ctx shape to avoid rebuilding repeated context objects.
1799        let mut dispatcher_ctx = dispatcher_ctx;
1800
1801        // Initial detection (before blank/doc-start are computed). Note: this can
1802        // match reference definitions, but footnotes are handled explicitly later.
1803        let dispatcher_match =
1804            self.block_registry
1805                .detect_prepared(&dispatcher_ctx, &self.lines, self.pos);
1806
1807        // Check for heading (needs blank line before, or at start of container)
1808        // Note: for fenced div nesting, the line immediately after a div opening fence
1809        // should be treated like the start of a container (Pandoc allows nested fences
1810        // without an intervening blank line). Similarly, the first line after a metadata
1811        // block (YAML/Pandoc title/MMD title) is treated as having a blank before it.
1812        let after_metadata_block = std::mem::replace(&mut self.after_metadata_block, false);
1813        let has_blank_before = if self.pos == 0 || after_metadata_block {
1814            true
1815        } else {
1816            let prev_line = self.lines[self.pos - 1];
1817            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
1818            let (prev_inner_no_nl, _) = strip_newline(prev_inner);
1819            let prev_is_fenced_div_open = self.config.extensions.fenced_divs
1820                && fenced_divs::try_parse_div_fence_open(
1821                    strip_n_blockquote_markers(prev_inner_no_nl, prev_bq_depth).trim_start(),
1822                )
1823                .is_some();
1824
1825            prev_line.trim().is_empty()
1826                || prev_is_fenced_div_open
1827                || matches!(self.containers.last(), Some(Container::BlockQuote { .. }))
1828        };
1829
1830        // For indented code blocks, we need a stricter condition - only actual blank lines count
1831        // Being at document start (pos == 0) is OK only if we're not inside a blockquote
1832        let at_document_start = self.pos == 0 && current_bq_depth == 0;
1833
1834        let prev_line_blank = if self.pos > 0 {
1835            let prev_line = self.lines[self.pos - 1];
1836            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
1837            prev_line.trim().is_empty() || (prev_bq_depth > 0 && prev_inner.trim().is_empty())
1838        } else {
1839            false
1840        };
1841        let has_blank_before_strict = at_document_start || prev_line_blank;
1842
1843        dispatcher_ctx.has_blank_before = has_blank_before;
1844        dispatcher_ctx.has_blank_before_strict = has_blank_before_strict;
1845        dispatcher_ctx.at_document_start = at_document_start;
1846
1847        let dispatcher_match =
1848            if dispatcher_ctx.has_blank_before || dispatcher_ctx.at_document_start {
1849                // Recompute now that blank/doc-start conditions are known.
1850                self.block_registry
1851                    .detect_prepared(&dispatcher_ctx, &self.lines, self.pos)
1852            } else {
1853                dispatcher_match
1854            };
1855
1856        if has_blank_before {
1857            if let Some(env_name) = extract_environment_name(content)
1858                && is_inline_math_environment(&env_name)
1859            {
1860                if !self.is_paragraph_open() {
1861                    paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
1862                }
1863                paragraphs::append_paragraph_line(
1864                    &mut self.containers,
1865                    &mut self.builder,
1866                    line_to_append.unwrap_or(self.lines[self.pos]),
1867                    self.config,
1868                );
1869                self.pos += 1;
1870                return true;
1871            }
1872
1873            if let Some(block_match) = dispatcher_match.as_ref() {
1874                let detection = block_match.detection;
1875
1876                match detection {
1877                    BlockDetectionResult::YesCanInterrupt => {
1878                        self.emit_list_item_buffer_if_needed();
1879                        if self.is_paragraph_open() {
1880                            self.close_containers_to(self.containers.depth() - 1);
1881                        }
1882                    }
1883                    BlockDetectionResult::Yes => {
1884                        self.prepare_for_block_element();
1885                    }
1886                    BlockDetectionResult::No => unreachable!(),
1887                }
1888
1889                if matches!(block_match.effect, BlockEffect::CloseFencedDiv) {
1890                    self.close_containers_to_fenced_div();
1891                }
1892
1893                let lines_consumed = self.block_registry.parse_prepared(
1894                    block_match,
1895                    &dispatcher_ctx,
1896                    &mut self.builder,
1897                    &self.lines,
1898                    self.pos,
1899                );
1900
1901                if matches!(
1902                    self.block_registry.parser_name(block_match),
1903                    "yaml_metadata" | "pandoc_title_block" | "mmd_title_block"
1904                ) {
1905                    self.after_metadata_block = true;
1906                }
1907
1908                match block_match.effect {
1909                    BlockEffect::None => {}
1910                    BlockEffect::OpenFencedDiv => {
1911                        self.containers.push(Container::FencedDiv {});
1912                    }
1913                    BlockEffect::CloseFencedDiv => {
1914                        self.close_fenced_div();
1915                    }
1916                    BlockEffect::OpenFootnoteDefinition => {
1917                        self.handle_footnote_open_effect(block_match, content);
1918                    }
1919                    BlockEffect::OpenList => {
1920                        self.handle_list_open_effect(block_match, content, indent_to_emit);
1921                    }
1922                    BlockEffect::OpenDefinitionList => {
1923                        self.handle_definition_list_effect(block_match, content, indent_to_emit);
1924                    }
1925                    BlockEffect::OpenBlockQuote => {
1926                        // Detection only for now; keep core blockquote handling intact.
1927                    }
1928                }
1929
1930                if lines_consumed == 0 {
1931                    log::warn!(
1932                        "block parser made no progress at line {} (parser={})",
1933                        self.pos + 1,
1934                        self.block_registry.parser_name(block_match)
1935                    );
1936                    return false;
1937                }
1938
1939                self.pos += lines_consumed;
1940                return true;
1941            }
1942        } else if let Some(block_match) = dispatcher_match.as_ref() {
1943            // Without blank-before, only allow interrupting blocks OR blocks that are
1944            // explicitly allowed without blank lines (e.g. reference definitions).
1945            let parser_name = self.block_registry.parser_name(block_match);
1946            match block_match.detection {
1947                BlockDetectionResult::YesCanInterrupt => {
1948                    if matches!(block_match.effect, BlockEffect::OpenFencedDiv)
1949                        && self.is_paragraph_open()
1950                    {
1951                        // Fenced divs must not interrupt paragraphs without a blank line.
1952                        if !self.is_paragraph_open() {
1953                            paragraphs::start_paragraph_if_needed(
1954                                &mut self.containers,
1955                                &mut self.builder,
1956                            );
1957                        }
1958                        paragraphs::append_paragraph_line(
1959                            &mut self.containers,
1960                            &mut self.builder,
1961                            line_to_append.unwrap_or(self.lines[self.pos]),
1962                            self.config,
1963                        );
1964                        self.pos += 1;
1965                        return true;
1966                    }
1967
1968                    if matches!(block_match.effect, BlockEffect::OpenList)
1969                        && self.is_paragraph_open()
1970                        && !lists::in_list(&self.containers)
1971                        && self.content_container_indent_to_strip() == 0
1972                    {
1973                        // Do not let lists interrupt a paragraph without a blank line.
1974                        paragraphs::append_paragraph_line(
1975                            &mut self.containers,
1976                            &mut self.builder,
1977                            line_to_append.unwrap_or(self.lines[self.pos]),
1978                            self.config,
1979                        );
1980                        self.pos += 1;
1981                        return true;
1982                    }
1983
1984                    self.emit_list_item_buffer_if_needed();
1985                    if self.is_paragraph_open() {
1986                        self.close_containers_to(self.containers.depth() - 1);
1987                    }
1988                }
1989                BlockDetectionResult::Yes => {
1990                    // Keep ambiguous fenced-div openers from interrupting an
1991                    // active paragraph without a blank line.
1992                    if parser_name == "fenced_div_open" && self.is_paragraph_open() {
1993                        if !self.is_paragraph_open() {
1994                            paragraphs::start_paragraph_if_needed(
1995                                &mut self.containers,
1996                                &mut self.builder,
1997                            );
1998                        }
1999                        paragraphs::append_paragraph_line(
2000                            &mut self.containers,
2001                            &mut self.builder,
2002                            line_to_append.unwrap_or(self.lines[self.pos]),
2003                            self.config,
2004                        );
2005                        self.pos += 1;
2006                        return true;
2007                    }
2008                }
2009                BlockDetectionResult::No => unreachable!(),
2010            }
2011
2012            if !matches!(block_match.detection, BlockDetectionResult::No) {
2013                if matches!(block_match.effect, BlockEffect::CloseFencedDiv) {
2014                    self.close_containers_to_fenced_div();
2015                }
2016
2017                let lines_consumed = self.block_registry.parse_prepared(
2018                    block_match,
2019                    &dispatcher_ctx,
2020                    &mut self.builder,
2021                    &self.lines,
2022                    self.pos,
2023                );
2024
2025                match block_match.effect {
2026                    BlockEffect::None => {}
2027                    BlockEffect::OpenFencedDiv => {
2028                        self.containers.push(Container::FencedDiv {});
2029                    }
2030                    BlockEffect::CloseFencedDiv => {
2031                        self.close_fenced_div();
2032                    }
2033                    BlockEffect::OpenFootnoteDefinition => {
2034                        self.handle_footnote_open_effect(block_match, content);
2035                    }
2036                    BlockEffect::OpenList => {
2037                        self.handle_list_open_effect(block_match, content, indent_to_emit);
2038                    }
2039                    BlockEffect::OpenDefinitionList => {
2040                        self.handle_definition_list_effect(block_match, content, indent_to_emit);
2041                    }
2042                    BlockEffect::OpenBlockQuote => {
2043                        // Detection only for now; keep core blockquote handling intact.
2044                    }
2045                }
2046
2047                if lines_consumed == 0 {
2048                    log::warn!(
2049                        "block parser made no progress at line {} (parser={})",
2050                        self.pos + 1,
2051                        self.block_registry.parser_name(block_match)
2052                    );
2053                    return false;
2054                }
2055
2056                self.pos += lines_consumed;
2057                return true;
2058            }
2059        }
2060
2061        // Check for line block (if line_blocks extension is enabled)
2062        if self.config.extensions.line_blocks
2063            && (has_blank_before || self.pos == 0)
2064            && try_parse_line_block_start(content).is_some()
2065            // Guard against context-stripped content (e.g. inside blockquotes) that
2066            // looks like a line block while the raw source line does not. Calling
2067            // parse_line_block on raw lines in that state would consume 0 lines.
2068            && try_parse_line_block_start(self.lines[self.pos]).is_some()
2069        {
2070            log::debug!("Parsed line block at line {}", self.pos);
2071            // Close paragraph before opening line block
2072            self.close_paragraph_if_open();
2073
2074            let new_pos = parse_line_block(&self.lines, self.pos, &mut self.builder, self.config);
2075            if new_pos > self.pos {
2076                self.pos = new_pos;
2077                return true;
2078            }
2079        }
2080
2081        // Paragraph or list item continuation
2082        // Check if we're inside a ListItem - if so, buffer the content instead of emitting
2083        if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
2084            log::debug!(
2085                "Inside ListItem - buffering content: {:?}",
2086                line_to_append.unwrap_or(self.lines[self.pos]).trim_end()
2087            );
2088            // Inside list item - buffer content for later parsing
2089            let line = line_to_append.unwrap_or(self.lines[self.pos]);
2090
2091            // Add line to buffer in the ListItem container
2092            if let Some(Container::ListItem { buffer, .. }) = self.containers.stack.last_mut() {
2093                buffer.push_text(line);
2094            }
2095
2096            self.pos += 1;
2097            return true;
2098        }
2099
2100        log::debug!(
2101            "Not in ListItem - creating paragraph for: {:?}",
2102            line_to_append.unwrap_or(self.lines[self.pos]).trim_end()
2103        );
2104        // Not in list item - create paragraph as usual
2105        paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
2106        // For lossless parsing: use line_to_append if provided (e.g., for blockquotes
2107        // where markers have been stripped), otherwise use the original line
2108        let line = line_to_append.unwrap_or(self.lines[self.pos]);
2109        paragraphs::append_paragraph_line(
2110            &mut self.containers,
2111            &mut self.builder,
2112            line,
2113            self.config,
2114        );
2115        self.pos += 1;
2116        true
2117    }
2118
2119    fn fenced_div_container_index(&self) -> Option<usize> {
2120        self.containers
2121            .stack
2122            .iter()
2123            .rposition(|c| matches!(c, Container::FencedDiv { .. }))
2124    }
2125
2126    fn close_containers_to_fenced_div(&mut self) {
2127        if let Some(index) = self.fenced_div_container_index() {
2128            self.close_containers_to(index + 1);
2129        }
2130    }
2131
2132    fn close_fenced_div(&mut self) {
2133        if let Some(index) = self.fenced_div_container_index() {
2134            self.close_containers_to(index);
2135        }
2136    }
2137
2138    fn in_fenced_div(&self) -> bool {
2139        self.containers
2140            .stack
2141            .iter()
2142            .any(|c| matches!(c, Container::FencedDiv { .. }))
2143    }
2144}