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