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::{
14    emit_atx_heading, emit_setext_heading, emit_setext_heading_body, try_parse_atx_heading,
15    try_parse_setext_heading,
16};
17use super::blocks::horizontal_rules::try_parse_horizontal_rule;
18use super::blocks::line_blocks;
19use super::blocks::lists;
20use super::blocks::paragraphs;
21use super::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
22use super::utils::container_stack;
23use super::utils::helpers::{is_blank_line, split_lines_inclusive, strip_newline};
24use super::utils::inline_emission;
25use super::utils::marker_utils;
26use super::utils::text_buffer;
27
28use super::blocks::blockquotes::strip_n_blockquote_markers;
29use super::utils::continuation::ContinuationPolicy;
30use container_stack::{Container, ContainerStack, byte_index_at_column, leading_indent};
31use definition_lists::{emit_definition_marker, emit_term};
32use line_blocks::{parse_line_block, try_parse_line_block_start};
33use lists::{
34    ListItemEmissionInput, ListMarker, is_content_nested_bullet_marker, start_nested_list,
35    try_parse_list_marker,
36};
37use marker_utils::{count_blockquote_markers, parse_blockquote_marker_info};
38use text_buffer::TextBuffer;
39
40const GITHUB_ALERT_MARKERS: [&str; 5] = [
41    "[!TIP]",
42    "[!WARNING]",
43    "[!IMPORTANT]",
44    "[!CAUTION]",
45    "[!NOTE]",
46];
47
48pub struct Parser<'a> {
49    lines: Vec<&'a str>,
50    pos: usize,
51    builder: GreenNodeBuilder<'static>,
52    containers: ContainerStack,
53    config: &'a ParserOptions,
54    block_registry: BlockParserRegistry,
55    /// True when the previous block was a metadata block (YAML, Pandoc title, or MMD title).
56    /// The first line after a metadata block is treated as if it has a blank line before it,
57    /// matching Pandoc's behavior of allowing headings etc. directly after frontmatter.
58    after_metadata_block: bool,
59}
60
61impl<'a> Parser<'a> {
62    pub fn new(input: &'a str, config: &'a ParserOptions) -> Self {
63        // Use split_lines_inclusive to preserve line endings (both LF and CRLF)
64        let lines = split_lines_inclusive(input);
65        Self {
66            lines,
67            pos: 0,
68            builder: GreenNodeBuilder::new(),
69            containers: ContainerStack::new(),
70            config,
71            block_registry: BlockParserRegistry::new(),
72            after_metadata_block: false,
73        }
74    }
75
76    pub fn parse(mut self) -> SyntaxNode {
77        self.parse_document_stack();
78
79        SyntaxNode::new_root(self.builder.finish())
80    }
81
82    /// Close enclosing list items (and their containing list) whose
83    /// `content_col` exceeds the given indent. Used by CommonMark when an
84    /// interrupting block (HR, ATX heading, fenced code, ...) appears at a
85    /// column shallower than the current list-item content column — per
86    /// §5.2 the line cannot continue the item, so the item and the
87    /// surrounding list close before the new block is emitted at the
88    /// outer level. Pandoc-markdown reaches this branch only when the
89    /// dispatcher already required a blank line before the interrupter,
90    /// at which point the blank-line handler has already closed the list;
91    /// gating on dialect at the call site keeps Pandoc unaffected.
92    fn close_lists_above_indent(&mut self, indent_cols: usize) {
93        while let Some(Container::ListItem { content_col, .. }) = self.containers.last() {
94            if indent_cols >= *content_col {
95                break;
96            }
97            self.close_containers_to(self.containers.depth() - 1);
98            if matches!(self.containers.last(), Some(Container::List { .. })) {
99                self.close_containers_to(self.containers.depth() - 1);
100            }
101        }
102    }
103
104    /// Emit buffered PLAIN content if Definition container has open PLAIN.
105    /// Close containers down to `keep`, emitting buffered content first.
106    fn close_containers_to(&mut self, keep: usize) {
107        // Emit buffered PARAGRAPH/PLAIN content before closing
108        while self.containers.depth() > keep {
109            match self.containers.stack.last() {
110                // Handle ListItem with buffering
111                Some(Container::ListItem { buffer, .. }) if !buffer.is_empty() => {
112                    // Clone buffer to avoid borrow issues
113                    let buffer_clone = buffer.clone();
114
115                    log::trace!(
116                        "Closing ListItem with buffer (is_empty={}, segment_count={})",
117                        buffer_clone.is_empty(),
118                        buffer_clone.segment_count()
119                    );
120
121                    // Determine if this should be Plain or PARAGRAPH:
122                    // 1. Check if parent LIST has blank lines between items (list-level loose)
123                    // 2. OR check if this item has blank lines within its content (item-level loose)
124                    let parent_list_is_loose = self
125                        .containers
126                        .stack
127                        .iter()
128                        .rev()
129                        .find_map(|c| match c {
130                            Container::List {
131                                has_blank_between_items,
132                                ..
133                            } => Some(*has_blank_between_items),
134                            _ => None,
135                        })
136                        .unwrap_or(false);
137
138                    let use_paragraph =
139                        parent_list_is_loose || buffer_clone.has_blank_lines_between_content();
140
141                    log::trace!(
142                        "Emitting ListItem buffer: use_paragraph={} (parent_list_is_loose={}, item_has_blanks={})",
143                        use_paragraph,
144                        parent_list_is_loose,
145                        buffer_clone.has_blank_lines_between_content()
146                    );
147
148                    // Pop container first
149                    self.containers.stack.pop();
150                    // Emit buffered content as Plain or PARAGRAPH
151                    buffer_clone.emit_as_block(&mut self.builder, use_paragraph, self.config);
152                    self.builder.finish_node(); // Close LIST_ITEM
153                }
154                // Handle ListItem without content
155                Some(Container::ListItem { .. }) => {
156                    log::trace!("Closing empty ListItem (no buffer content)");
157                    // Just close normally (empty list item)
158                    self.containers.stack.pop();
159                    self.builder.finish_node();
160                }
161                // Handle Paragraph with buffering
162                Some(Container::Paragraph {
163                    buffer,
164                    start_checkpoint,
165                    ..
166                }) if !buffer.is_empty() => {
167                    // Clone buffer to avoid borrow issues
168                    let buffer_clone = buffer.clone();
169                    let checkpoint = *start_checkpoint;
170                    // Pop container first
171                    self.containers.stack.pop();
172                    // Retroactively wrap as PARAGRAPH and emit buffered content
173                    self.builder
174                        .start_node_at(checkpoint, SyntaxKind::PARAGRAPH.into());
175                    buffer_clone.emit_with_inlines(&mut self.builder, self.config);
176                    self.builder.finish_node();
177                }
178                // Handle Paragraph without content
179                Some(Container::Paragraph {
180                    start_checkpoint, ..
181                }) => {
182                    let checkpoint = *start_checkpoint;
183                    // Just close normally — emit empty PARAGRAPH wrapper
184                    self.containers.stack.pop();
185                    self.builder
186                        .start_node_at(checkpoint, SyntaxKind::PARAGRAPH.into());
187                    self.builder.finish_node();
188                }
189                // Handle Definition with buffered PLAIN
190                Some(Container::Definition {
191                    plain_open: true,
192                    plain_buffer,
193                    ..
194                }) if !plain_buffer.is_empty() => {
195                    let text = plain_buffer.get_accumulated_text();
196                    let line_without_newline = text
197                        .strip_suffix("\r\n")
198                        .or_else(|| text.strip_suffix('\n'));
199                    if let Some(line) = line_without_newline
200                        && !line.contains('\n')
201                        && !line.contains('\r')
202                        && let Some(level) = try_parse_atx_heading(line)
203                    {
204                        emit_atx_heading(&mut self.builder, &text, level, self.config);
205                    } else {
206                        // Emit PLAIN node with buffered inline-parsed content
207                        self.builder.start_node(SyntaxKind::PLAIN.into());
208                        inline_emission::emit_inlines(&mut self.builder, &text, self.config);
209                        self.builder.finish_node();
210                    }
211
212                    // Mark PLAIN as closed and clear buffer
213                    if let Some(Container::Definition {
214                        plain_open,
215                        plain_buffer,
216                        ..
217                    }) = self.containers.stack.last_mut()
218                    {
219                        plain_buffer.clear();
220                        *plain_open = false;
221                    }
222
223                    // Pop container and finish node
224                    self.containers.stack.pop();
225                    self.builder.finish_node();
226                }
227                // Handle Definition with PLAIN open but empty buffer
228                Some(Container::Definition {
229                    plain_open: true, ..
230                }) => {
231                    // Mark PLAIN as closed
232                    if let Some(Container::Definition {
233                        plain_open,
234                        plain_buffer,
235                        ..
236                    }) = self.containers.stack.last_mut()
237                    {
238                        plain_buffer.clear();
239                        *plain_open = false;
240                    }
241
242                    // Pop container and finish node
243                    self.containers.stack.pop();
244                    self.builder.finish_node();
245                }
246                // All other containers
247                _ => {
248                    self.containers.stack.pop();
249                    self.builder.finish_node();
250                }
251            }
252        }
253    }
254
255    /// Emit buffered PLAIN content if there's an open PLAIN in a Definition.
256    /// This is used when we need to close PLAIN but keep the Definition container open.
257    fn emit_buffered_plain_if_needed(&mut self) {
258        // Check if we have an open PLAIN with buffered content
259        if let Some(Container::Definition {
260            plain_open: true,
261            plain_buffer,
262            ..
263        }) = self.containers.stack.last()
264            && !plain_buffer.is_empty()
265        {
266            let text = plain_buffer.get_accumulated_text();
267            let line_without_newline = text
268                .strip_suffix("\r\n")
269                .or_else(|| text.strip_suffix('\n'));
270            if let Some(line) = line_without_newline
271                && !line.contains('\n')
272                && !line.contains('\r')
273                && let Some(level) = try_parse_atx_heading(line)
274            {
275                emit_atx_heading(&mut self.builder, &text, level, self.config);
276            } else {
277                // Emit PLAIN node with buffered inline-parsed content
278                self.builder.start_node(SyntaxKind::PLAIN.into());
279                inline_emission::emit_inlines(&mut self.builder, &text, self.config);
280                self.builder.finish_node();
281            }
282        }
283
284        // Mark PLAIN as closed and clear buffer
285        if let Some(Container::Definition {
286            plain_open,
287            plain_buffer,
288            ..
289        }) = self.containers.stack.last_mut()
290            && *plain_open
291        {
292            plain_buffer.clear();
293            *plain_open = false;
294        }
295    }
296
297    /// Close blockquotes down to a target depth.
298    ///
299    /// Must use `Parser::close_containers_to` (not `ContainerStack::close_to`) so list/paragraph
300    /// buffers are emitted for losslessness.
301    fn close_blockquotes_to_depth(&mut self, target_depth: usize) {
302        let mut current = self.current_blockquote_depth();
303        while current > target_depth {
304            while !matches!(self.containers.last(), Some(Container::BlockQuote { .. })) {
305                if self.containers.depth() == 0 {
306                    break;
307                }
308                self.close_containers_to(self.containers.depth() - 1);
309            }
310            if matches!(self.containers.last(), Some(Container::BlockQuote { .. })) {
311                self.close_containers_to(self.containers.depth() - 1);
312                current -= 1;
313            } else {
314                break;
315            }
316        }
317    }
318
319    fn active_alert_blockquote_depth(&self) -> Option<usize> {
320        self.containers.stack.iter().rev().find_map(|c| match c {
321            Container::Alert { blockquote_depth } => Some(*blockquote_depth),
322            _ => None,
323        })
324    }
325
326    fn in_active_alert(&self) -> bool {
327        self.active_alert_blockquote_depth().is_some()
328    }
329
330    fn previous_block_requires_blank_before_heading(&self) -> bool {
331        matches!(
332            self.containers.last(),
333            Some(Container::Paragraph { .. })
334                | Some(Container::ListItem { .. })
335                | Some(Container::Definition { .. })
336                | Some(Container::DefinitionItem { .. })
337                | Some(Container::FootnoteDefinition { .. })
338        )
339    }
340
341    fn alert_marker_from_content(content: &str) -> Option<&'static str> {
342        let (without_newline, _) = strip_newline(content);
343        let trimmed = without_newline.trim();
344        GITHUB_ALERT_MARKERS
345            .into_iter()
346            .find(|marker| *marker == trimmed)
347    }
348
349    /// Emit buffered list item content if we're in a ListItem and it has content.
350    /// This is used before starting block-level elements inside list items.
351    fn emit_list_item_buffer_if_needed(&mut self) {
352        if let Some(Container::ListItem { buffer, .. }) = self.containers.stack.last_mut()
353            && !buffer.is_empty()
354        {
355            let buffer_clone = buffer.clone();
356            buffer.clear();
357            let use_paragraph = buffer_clone.has_blank_lines_between_content();
358            buffer_clone.emit_as_block(&mut self.builder, use_paragraph, self.config);
359        }
360    }
361
362    /// CommonMark §5.2: when a list item's first line (after the marker) is a
363    /// fenced code block opener, the content of the item *is* the code block —
364    /// not buffered text. The list-item open path normally pushes the
365    /// post-marker text into the item's buffer; this helper detects an opening
366    /// fence in that buffered first line and converts it into a CODE_BLOCK
367    /// inside the LIST_ITEM, consuming subsequent lines until the closing
368    /// fence (or end of document under CommonMark dialect, per §4.5).
369    ///
370    /// Pandoc-markdown also reaches this path: a bare fence still requires a
371    /// matching closer to register as a code block, matching
372    /// `FencedCodeBlockParser::detect_prepared` (`bare_fence_in_list_with_closer`).
373    fn maybe_open_fenced_code_in_new_list_item(&mut self) {
374        let Some(Container::ListItem {
375            content_col,
376            buffer,
377            ..
378        }) = self.containers.stack.last()
379        else {
380            return;
381        };
382        let content_col = *content_col;
383        let Some(text) = buffer.first_text() else {
384            return;
385        };
386        if buffer.segment_count() != 1 {
387            return;
388        }
389        let text_owned = text.to_string();
390        let Some(fence) = code_blocks::try_parse_fence_open(&text_owned) else {
391            return;
392        };
393        let common_mark_dialect = self.config.dialect == crate::options::Dialect::CommonMark;
394        let has_info = !fence.info_string.trim().is_empty();
395        let bq_depth = self.current_blockquote_depth();
396        let has_matching_closer = self.has_matching_fence_closer(&fence, bq_depth, content_col);
397        if !(has_info || has_matching_closer || common_mark_dialect) {
398            return;
399        }
400        // Gate fences by extension flags, mirroring the dispatcher.
401        if (fence.fence_char == '`' && !self.config.extensions.backtick_code_blocks)
402            || (fence.fence_char == '~' && !self.config.extensions.fenced_code_blocks)
403        {
404            return;
405        }
406        if let Some(Container::ListItem { buffer, .. }) = self.containers.stack.last_mut() {
407            buffer.clear();
408        }
409        let new_pos = code_blocks::parse_fenced_code_block(
410            &mut self.builder,
411            &self.lines,
412            self.pos,
413            fence,
414            bq_depth,
415            content_col,
416            Some(&text_owned),
417        );
418        // The dispatcher caller will advance pos by lines_consumed (= 1 from
419        // the list parser) after we return. Compensate so the final pos lands
420        // on `new_pos`.
421        self.pos = new_pos.saturating_sub(1);
422    }
423
424    /// CommonMark §5.2 rule #2: when a list marker is followed by ≥ 5 columns
425    /// of whitespace and non-empty content, the content begins as an indented
426    /// code block on the marker line. The marker parser collapses the post-
427    /// marker whitespace to "marker + 1 (possibly virtual) space" and leaves
428    /// the surplus in the post-marker text. This helper detects such a single-
429    /// line indented-code first-line and converts the buffered text into a
430    /// CODE_BLOCK > CODE_CONTENT inside the LIST_ITEM.
431    ///
432    /// Multi-line accumulation (subsequent indented-code lines on continuation
433    /// lines) is handled by the regular block-detection path.
434    fn maybe_open_indented_code_in_new_list_item(&mut self) {
435        let Some(Container::ListItem {
436            content_col,
437            buffer,
438            marker_only,
439            virtual_marker_space,
440        }) = self.containers.stack.last()
441        else {
442            return;
443        };
444        if *marker_only {
445            return;
446        }
447        if buffer.segment_count() != 1 {
448            return;
449        }
450        let Some(text) = buffer.first_text() else {
451            return;
452        };
453        let content_col = *content_col;
454        let virtual_marker_space = *virtual_marker_space;
455        let text_owned = text.to_string();
456
457        // Single-line content only for now.
458        let mut iter = text_owned.split_inclusive('\n');
459        let line_with_nl = iter.next().unwrap_or("").to_string();
460        if iter.next().is_some() {
461            return;
462        }
463
464        let line_no_nl = line_with_nl
465            .strip_suffix("\r\n")
466            .or_else(|| line_with_nl.strip_suffix('\n'))
467            .unwrap_or(&line_with_nl);
468        let nl_suffix = &line_with_nl[line_no_nl.len()..];
469
470        let buffer_start_col = if virtual_marker_space {
471            content_col.saturating_sub(1)
472        } else {
473            content_col
474        };
475
476        let target = content_col + 4;
477        let (cols_walked, ws_bytes) =
478            super::utils::container_stack::leading_indent_from(line_no_nl, buffer_start_col);
479
480        if buffer_start_col + cols_walked < target {
481            return;
482        }
483        if ws_bytes >= line_no_nl.len() {
484            return;
485        }
486
487        if let Some(Container::ListItem { buffer, .. }) = self.containers.stack.last_mut() {
488            buffer.clear();
489        }
490
491        self.builder.start_node(SyntaxKind::CODE_BLOCK.into());
492        self.builder.start_node(SyntaxKind::CODE_CONTENT.into());
493        if ws_bytes > 0 {
494            self.builder
495                .token(SyntaxKind::WHITESPACE.into(), &line_no_nl[..ws_bytes]);
496        }
497        let rest = &line_no_nl[ws_bytes..];
498        if !rest.is_empty() {
499            self.builder.token(SyntaxKind::TEXT.into(), rest);
500        }
501        if !nl_suffix.is_empty() {
502            self.builder.token(SyntaxKind::NEWLINE.into(), nl_suffix);
503        }
504        self.builder.finish_node();
505        self.builder.finish_node();
506    }
507
508    fn has_matching_fence_closer(
509        &self,
510        fence: &code_blocks::FenceInfo,
511        bq_depth: usize,
512        content_col: usize,
513    ) -> bool {
514        for raw_line in self.lines.iter().skip(self.pos + 1) {
515            let (line_bq_depth, inner) = count_blockquote_markers(raw_line);
516            if line_bq_depth < bq_depth {
517                break;
518            }
519            let candidate = if content_col > 0 && !inner.is_empty() {
520                let idx = byte_index_at_column(inner, content_col);
521                if idx <= inner.len() {
522                    &inner[idx..]
523                } else {
524                    inner
525                }
526            } else {
527                inner
528            };
529            if code_blocks::is_closing_fence(candidate, fence) {
530                return true;
531            }
532        }
533        false
534    }
535
536    /// Check if a paragraph is currently open.
537    fn is_paragraph_open(&self) -> bool {
538        matches!(self.containers.last(), Some(Container::Paragraph { .. }))
539    }
540
541    /// Fold an open paragraph's buffered content into a setext heading and emit it.
542    ///
543    /// Used for CommonMark multi-line setext: when a setext underline is matched
544    /// and a paragraph is already open with buffered text, the entire paragraph
545    /// (buffer + current text line) becomes the heading content. The HEADING node
546    /// is wrapped retroactively from the paragraph's start checkpoint so the
547    /// emitted bytes appear in source order.
548    fn emit_setext_heading_folding_paragraph(
549        &mut self,
550        text_line: &str,
551        underline_line: &str,
552        level: usize,
553    ) {
554        let (buffered_text, checkpoint) = match self.containers.stack.last() {
555            Some(Container::Paragraph {
556                buffer,
557                start_checkpoint,
558                ..
559            }) => (buffer.get_text_for_parsing(), Some(*start_checkpoint)),
560            _ => (String::new(), None),
561        };
562
563        if checkpoint.is_some() {
564            self.containers.stack.pop();
565        }
566
567        let combined_text = if buffered_text.is_empty() {
568            text_line.to_string()
569        } else {
570            format!("{}{}", buffered_text, text_line)
571        };
572
573        let cp = checkpoint.expect(
574            "emit_setext_heading_folding_paragraph requires an open paragraph; \
575             single-line setext should go through the regular dispatcher path",
576        );
577        self.builder.start_node_at(cp, SyntaxKind::HEADING.into());
578        emit_setext_heading_body(
579            &mut self.builder,
580            &combined_text,
581            underline_line,
582            level,
583            self.config,
584        );
585        self.builder.finish_node();
586    }
587
588    /// Try to fold a list item's buffered first-line text and the current line
589    /// into a setext HEADING node, returning true on success.
590    ///
591    /// CommonMark §4.3 / Pandoc-markdown both treat the marker line of a list
592    /// item as a fresh start for setext detection — i.e. `- Bar\n  ---\n` is a
593    /// setext h2 inside the list item. The dispatcher path can't see this
594    /// because the list parser consumes the marker line and buffers the
595    /// post-marker text; by the time `  ---` reaches the dispatcher, the
596    /// candidate text line is already inside the buffer rather than the line
597    /// stream. This helper bridges that gap: when the innermost container is a
598    /// `ListItem` with a single buffered text segment and the current
599    /// (list-item-content-stripped) line is a setext underline, emit the
600    /// folded heading directly and clear the buffer.
601    ///
602    /// Multi-line setext (multiple buffered text segments) is *not* handled
603    /// here because Pandoc-markdown disagrees with CommonMark on whether
604    /// `- Foo\n  Bar\n  ---\n` forms a setext heading.
605    fn try_fold_list_item_buffer_into_setext(&mut self, content: &str) -> bool {
606        let Some(Container::ListItem {
607            buffer,
608            content_col,
609            ..
610        }) = self.containers.stack.last()
611        else {
612            return false;
613        };
614        if buffer.segment_count() != 1 {
615            return false;
616        }
617        let Some(text_line) = buffer.first_text() else {
618            return false;
619        };
620
621        // CommonMark §5.2: the underline must be indented to at least the
622        // list item's content column. A bare `---` at column 0 escapes the
623        // item and becomes a thematic break (CMark spec example #94/#99); a
624        // bare `-` at column 0 is a sibling list marker (#281/#282).
625        let content_col = *content_col;
626        let (underline_indent_cols, _) = leading_indent(content);
627        if underline_indent_cols < content_col {
628            return false;
629        }
630
631        let lines = [text_line, content];
632        let Some((level, _)) = try_parse_setext_heading(&lines, 0) else {
633            return false;
634        };
635
636        let (text_no_newline, _) = strip_newline(text_line);
637        if text_no_newline.trim().is_empty() {
638            return false;
639        }
640        if try_parse_horizontal_rule(text_no_newline).is_some() {
641            return false;
642        }
643
644        let text_owned = text_line.to_string();
645        if let Some(Container::ListItem { buffer, .. }) = self.containers.stack.last_mut() {
646            buffer.clear();
647        }
648        emit_setext_heading(&mut self.builder, &text_owned, content, level, self.config);
649        self.pos += 1;
650        true
651    }
652
653    /// Close paragraph if one is currently open.
654    fn close_paragraph_if_open(&mut self) {
655        if self.is_paragraph_open() {
656            self.close_containers_to(self.containers.depth() - 1);
657        }
658    }
659
660    /// Prepare for a block-level element by flushing buffers and closing paragraphs.
661    /// This is a common pattern before starting tables, code blocks, divs, etc.
662    fn prepare_for_block_element(&mut self) {
663        self.emit_list_item_buffer_if_needed();
664        self.close_paragraph_if_open();
665    }
666
667    /// Close any open `FootnoteDefinition` container before a new footnote definition
668    /// is emitted into the green tree. Without this, a back-to-back `[^a]:`/`[^b]:`
669    /// pair would nest the second `FOOTNOTE_DEFINITION` node inside the first.
670    fn close_open_footnote_definition(&mut self) {
671        while matches!(
672            self.containers.last(),
673            Some(Container::FootnoteDefinition { .. })
674        ) {
675            self.close_containers_to(self.containers.depth() - 1);
676        }
677    }
678
679    fn handle_footnote_open_effect(
680        &mut self,
681        block_match: &super::block_dispatcher::PreparedBlockMatch,
682        content: &str,
683    ) {
684        let content_start = block_match
685            .payload
686            .as_ref()
687            .and_then(|p| p.downcast_ref::<super::block_dispatcher::FootnoteDefinitionPrepared>())
688            .map(|p| p.content_start)
689            .unwrap_or(0);
690
691        let content_col = 4;
692        self.containers
693            .push(Container::FootnoteDefinition { content_col });
694
695        if content_start > 0 {
696            let first_line_content = &content[content_start..];
697            if !first_line_content.trim().is_empty() {
698                paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
699                paragraphs::append_paragraph_line(
700                    &mut self.containers,
701                    &mut self.builder,
702                    first_line_content,
703                    self.config,
704                );
705            } else {
706                let (_, newline_str) = strip_newline(content);
707                if !newline_str.is_empty() {
708                    self.builder.token(SyntaxKind::NEWLINE.into(), newline_str);
709                }
710            }
711        }
712    }
713
714    /// CommonMark spec example #312: handle a detected list marker that's
715    /// actually lazy continuation rather than a new list item. Returns true
716    /// when the line was consumed as continuation (caller should advance pos
717    /// without calling `handle_list_open_effect`).
718    ///
719    /// A marker line whose leading indent is ≥ 4 columns isn't a real list
720    /// marker when (a) the indent doesn't reach the deepest open list item's
721    /// content column (so it can't open a child list), and (b) no open list
722    /// level matches the indent (so it can't be a sibling). In that case the
723    /// content is just text that lazily extends the deepest open paragraph
724    /// or list item.
725    fn try_lazy_list_continuation(
726        &mut self,
727        block_match: &super::block_dispatcher::PreparedBlockMatch,
728        content: &str,
729    ) -> bool {
730        use super::block_dispatcher::ListPrepared;
731
732        let Some(prepared) = block_match
733            .payload
734            .as_ref()
735            .and_then(|p| p.downcast_ref::<ListPrepared>())
736        else {
737            return false;
738        };
739
740        if prepared.indent_cols < 4 || !lists::in_list(&self.containers) {
741            return false;
742        }
743
744        let current_content_col = paragraphs::current_content_col(&self.containers);
745        if prepared.indent_cols >= current_content_col {
746            return false;
747        }
748
749        if lists::find_matching_list_level(
750            &self.containers,
751            &prepared.marker,
752            prepared.indent_cols,
753            self.config.dialect,
754        )
755        .is_some()
756        {
757            return false;
758        }
759
760        match self.containers.last() {
761            Some(Container::Paragraph { .. }) => {
762                paragraphs::append_paragraph_line(
763                    &mut self.containers,
764                    &mut self.builder,
765                    content,
766                    self.config,
767                );
768                true
769            }
770            Some(Container::ListItem { .. }) => {
771                if let Some(Container::ListItem {
772                    buffer,
773                    marker_only,
774                    ..
775                }) = self.containers.stack.last_mut()
776                {
777                    buffer.push_text(content);
778                    if !content.trim().is_empty() {
779                        *marker_only = false;
780                    }
781                }
782                true
783            }
784            _ => false,
785        }
786    }
787
788    fn handle_list_open_effect(
789        &mut self,
790        block_match: &super::block_dispatcher::PreparedBlockMatch,
791        content: &str,
792        indent_to_emit: Option<&str>,
793    ) {
794        use super::block_dispatcher::ListPrepared;
795
796        let prepared = block_match
797            .payload
798            .as_ref()
799            .and_then(|p| p.downcast_ref::<ListPrepared>());
800        let Some(prepared) = prepared else {
801            return;
802        };
803
804        if prepared.indent_cols >= 4 && !lists::in_list(&self.containers) {
805            paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
806            paragraphs::append_paragraph_line(
807                &mut self.containers,
808                &mut self.builder,
809                content,
810                self.config,
811            );
812            return;
813        }
814
815        if self.is_paragraph_open() {
816            if !block_match.detection.eq(&BlockDetectionResult::Yes) {
817                paragraphs::append_paragraph_line(
818                    &mut self.containers,
819                    &mut self.builder,
820                    content,
821                    self.config,
822                );
823                return;
824            }
825            self.close_containers_to(self.containers.depth() - 1);
826        }
827
828        if matches!(
829            self.containers.last(),
830            Some(Container::Definition {
831                plain_open: true,
832                ..
833            })
834        ) {
835            self.emit_buffered_plain_if_needed();
836        }
837
838        let matched_level = lists::find_matching_list_level(
839            &self.containers,
840            &prepared.marker,
841            prepared.indent_cols,
842            self.config.dialect,
843        );
844        let list_item = ListItemEmissionInput {
845            content,
846            marker_len: prepared.marker_len,
847            spaces_after_cols: prepared.spaces_after_cols,
848            spaces_after_bytes: prepared.spaces_after,
849            indent_cols: prepared.indent_cols,
850            indent_bytes: prepared.indent_bytes,
851            virtual_marker_space: prepared.virtual_marker_space,
852        };
853        let current_content_col = paragraphs::current_content_col(&self.containers);
854        let deep_ordered_matched_level = matched_level
855            .and_then(|level| self.containers.stack.get(level).map(|c| (level, c)))
856            .and_then(|(level, container)| match container {
857                Container::List {
858                    marker: list_marker,
859                    base_indent_cols,
860                    ..
861                } if matches!(
862                    (&prepared.marker, list_marker),
863                    (ListMarker::Ordered(_), ListMarker::Ordered(_))
864                ) && prepared.indent_cols >= 4
865                    && *base_indent_cols >= 4
866                    && prepared.indent_cols.abs_diff(*base_indent_cols) <= 3 =>
867                {
868                    Some(level)
869                }
870                _ => None,
871            });
872
873        if deep_ordered_matched_level.is_none()
874            && current_content_col > 0
875            && prepared.indent_cols >= current_content_col
876        {
877            if let Some(level) = matched_level
878                && let Some(Container::List {
879                    base_indent_cols, ..
880                }) = self.containers.stack.get(level)
881                && prepared.indent_cols == *base_indent_cols
882            {
883                let num_parent_lists = self.containers.stack[..level]
884                    .iter()
885                    .filter(|c| matches!(c, Container::List { .. }))
886                    .count();
887
888                if num_parent_lists > 0 {
889                    self.close_containers_to(level + 1);
890
891                    if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
892                        self.close_containers_to(self.containers.depth() - 1);
893                    }
894                    if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
895                        self.close_containers_to(self.containers.depth() - 1);
896                    }
897
898                    if let Some(indent_str) = indent_to_emit {
899                        self.builder
900                            .token(SyntaxKind::WHITESPACE.into(), indent_str);
901                    }
902
903                    if let Some(nested_marker) = prepared.nested_marker {
904                        lists::add_list_item_with_nested_empty_list(
905                            &mut self.containers,
906                            &mut self.builder,
907                            &list_item,
908                            nested_marker,
909                        );
910                    } else {
911                        lists::add_list_item(
912                            &mut self.containers,
913                            &mut self.builder,
914                            &list_item,
915                            self.config,
916                        );
917                    }
918                    self.maybe_open_fenced_code_in_new_list_item();
919                    self.maybe_open_indented_code_in_new_list_item();
920                    return;
921                }
922            }
923
924            self.emit_list_item_buffer_if_needed();
925
926            start_nested_list(
927                &mut self.containers,
928                &mut self.builder,
929                &prepared.marker,
930                &list_item,
931                indent_to_emit,
932                self.config,
933            );
934            self.maybe_open_fenced_code_in_new_list_item();
935            self.maybe_open_indented_code_in_new_list_item();
936            return;
937        }
938
939        if let Some(level) = matched_level {
940            self.close_containers_to(level + 1);
941
942            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
943                self.close_containers_to(self.containers.depth() - 1);
944            }
945            if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
946                self.close_containers_to(self.containers.depth() - 1);
947            }
948
949            if let Some(indent_str) = indent_to_emit {
950                self.builder
951                    .token(SyntaxKind::WHITESPACE.into(), indent_str);
952            }
953
954            if let Some(nested_marker) = prepared.nested_marker {
955                lists::add_list_item_with_nested_empty_list(
956                    &mut self.containers,
957                    &mut self.builder,
958                    &list_item,
959                    nested_marker,
960                );
961            } else {
962                lists::add_list_item(
963                    &mut self.containers,
964                    &mut self.builder,
965                    &list_item,
966                    self.config,
967                );
968            }
969            self.maybe_open_fenced_code_in_new_list_item();
970            self.maybe_open_indented_code_in_new_list_item();
971            return;
972        }
973
974        if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
975            self.close_containers_to(self.containers.depth() - 1);
976        }
977        while matches!(
978            self.containers.last(),
979            Some(Container::ListItem { .. } | Container::List { .. })
980        ) {
981            self.close_containers_to(self.containers.depth() - 1);
982        }
983
984        self.builder.start_node(SyntaxKind::LIST.into());
985        if let Some(indent_str) = indent_to_emit {
986            self.builder
987                .token(SyntaxKind::WHITESPACE.into(), indent_str);
988        }
989        self.containers.push(Container::List {
990            marker: prepared.marker.clone(),
991            base_indent_cols: prepared.indent_cols,
992            has_blank_between_items: false,
993        });
994
995        if let Some(nested_marker) = prepared.nested_marker {
996            lists::add_list_item_with_nested_empty_list(
997                &mut self.containers,
998                &mut self.builder,
999                &list_item,
1000                nested_marker,
1001            );
1002        } else {
1003            lists::add_list_item(
1004                &mut self.containers,
1005                &mut self.builder,
1006                &list_item,
1007                self.config,
1008            );
1009        }
1010        self.maybe_open_fenced_code_in_new_list_item();
1011        self.maybe_open_indented_code_in_new_list_item();
1012    }
1013
1014    fn handle_definition_list_effect(
1015        &mut self,
1016        block_match: &super::block_dispatcher::PreparedBlockMatch,
1017        content: &str,
1018        indent_to_emit: Option<&str>,
1019    ) {
1020        use super::block_dispatcher::DefinitionPrepared;
1021
1022        let prepared = block_match
1023            .payload
1024            .as_ref()
1025            .and_then(|p| p.downcast_ref::<DefinitionPrepared>());
1026        let Some(prepared) = prepared else {
1027            return;
1028        };
1029
1030        match prepared {
1031            DefinitionPrepared::Definition {
1032                marker_char,
1033                indent,
1034                spaces_after,
1035                spaces_after_cols,
1036                has_content,
1037            } => {
1038                self.emit_buffered_plain_if_needed();
1039
1040                while matches!(self.containers.last(), Some(Container::ListItem { .. })) {
1041                    self.close_containers_to(self.containers.depth() - 1);
1042                }
1043                while matches!(self.containers.last(), Some(Container::List { .. })) {
1044                    self.close_containers_to(self.containers.depth() - 1);
1045                }
1046
1047                if matches!(self.containers.last(), Some(Container::Definition { .. })) {
1048                    self.close_containers_to(self.containers.depth() - 1);
1049                }
1050
1051                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1052                    self.close_containers_to(self.containers.depth() - 1);
1053                }
1054
1055                // A definition marker cannot start a new definition item without a term.
1056                // If the preceding term/item was closed by a blank line but we are still
1057                // inside the same definition list, reopen a definition item for continuation.
1058                if definition_lists::in_definition_list(&self.containers)
1059                    && !matches!(
1060                        self.containers.last(),
1061                        Some(Container::DefinitionItem { .. })
1062                    )
1063                {
1064                    self.builder.start_node(SyntaxKind::DEFINITION_ITEM.into());
1065                    self.containers.push(Container::DefinitionItem {});
1066                }
1067
1068                if !definition_lists::in_definition_list(&self.containers) {
1069                    self.builder.start_node(SyntaxKind::DEFINITION_LIST.into());
1070                    self.containers.push(Container::DefinitionList {});
1071                }
1072
1073                if !matches!(
1074                    self.containers.last(),
1075                    Some(Container::DefinitionItem { .. })
1076                ) {
1077                    self.builder.start_node(SyntaxKind::DEFINITION_ITEM.into());
1078                    self.containers.push(Container::DefinitionItem {});
1079                }
1080
1081                self.builder.start_node(SyntaxKind::DEFINITION.into());
1082
1083                if let Some(indent_str) = indent_to_emit {
1084                    self.builder
1085                        .token(SyntaxKind::WHITESPACE.into(), indent_str);
1086                }
1087
1088                emit_definition_marker(&mut self.builder, *marker_char, *indent);
1089                let indent_bytes = byte_index_at_column(content, *indent);
1090                if *spaces_after > 0 {
1091                    let space_start = indent_bytes + 1;
1092                    let space_end = space_start + *spaces_after;
1093                    if space_end <= content.len() {
1094                        self.builder.token(
1095                            SyntaxKind::WHITESPACE.into(),
1096                            &content[space_start..space_end],
1097                        );
1098                    }
1099                }
1100
1101                if !*has_content {
1102                    let current_line = self.lines[self.pos];
1103                    let (_, newline_str) = strip_newline(current_line);
1104                    if !newline_str.is_empty() {
1105                        self.builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1106                    }
1107                }
1108
1109                let content_col = *indent + 1 + *spaces_after_cols;
1110                let content_start_bytes = indent_bytes + 1 + *spaces_after;
1111                let after_marker_and_spaces = content.get(content_start_bytes..).unwrap_or("");
1112                let mut plain_buffer = TextBuffer::new();
1113                let mut definition_pushed = false;
1114
1115                if *has_content {
1116                    let current_line = self.lines[self.pos];
1117                    let (trimmed_line, _) = strip_newline(current_line);
1118
1119                    let content_start = content_start_bytes.min(trimmed_line.len());
1120                    let content_slice = &trimmed_line[content_start..];
1121                    let content_line = &current_line[content_start_bytes.min(current_line.len())..];
1122
1123                    let (blockquote_depth, inner_blockquote_content) =
1124                        count_blockquote_markers(content_line);
1125
1126                    let should_start_list_from_first_line = self
1127                        .lines
1128                        .get(self.pos + 1)
1129                        .map(|next_line| {
1130                            let (next_without_newline, _) = strip_newline(next_line);
1131                            if next_without_newline.trim().is_empty() {
1132                                return false;
1133                            }
1134
1135                            let (next_indent_cols, _) = leading_indent(next_without_newline);
1136                            next_indent_cols >= content_col
1137                        })
1138                        .unwrap_or(false);
1139
1140                    if blockquote_depth > 0 {
1141                        self.containers.push(Container::Definition {
1142                            content_col,
1143                            plain_open: false,
1144                            plain_buffer: TextBuffer::new(),
1145                        });
1146                        definition_pushed = true;
1147
1148                        let marker_info = parse_blockquote_marker_info(content_line);
1149                        for level in 0..blockquote_depth {
1150                            self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
1151                            if let Some(info) = marker_info.get(level) {
1152                                blockquotes::emit_one_blockquote_marker(
1153                                    &mut self.builder,
1154                                    info.leading_spaces,
1155                                    info.has_trailing_space,
1156                                );
1157                            }
1158                            self.containers.push(Container::BlockQuote {});
1159                        }
1160
1161                        if !inner_blockquote_content.trim().is_empty() {
1162                            paragraphs::start_paragraph_if_needed(
1163                                &mut self.containers,
1164                                &mut self.builder,
1165                            );
1166                            paragraphs::append_paragraph_line(
1167                                &mut self.containers,
1168                                &mut self.builder,
1169                                inner_blockquote_content,
1170                                self.config,
1171                            );
1172                        }
1173                    } else if let Some(marker_match) =
1174                        try_parse_list_marker(content_slice, self.config)
1175                        && should_start_list_from_first_line
1176                    {
1177                        self.containers.push(Container::Definition {
1178                            content_col,
1179                            plain_open: false,
1180                            plain_buffer: TextBuffer::new(),
1181                        });
1182                        definition_pushed = true;
1183
1184                        let (indent_cols, indent_bytes) = leading_indent(content_line);
1185                        self.builder.start_node(SyntaxKind::LIST.into());
1186                        self.containers.push(Container::List {
1187                            marker: marker_match.marker.clone(),
1188                            base_indent_cols: indent_cols,
1189                            has_blank_between_items: false,
1190                        });
1191
1192                        let list_item = ListItemEmissionInput {
1193                            content: content_line,
1194                            marker_len: marker_match.marker_len,
1195                            spaces_after_cols: marker_match.spaces_after_cols,
1196                            spaces_after_bytes: marker_match.spaces_after_bytes,
1197                            indent_cols,
1198                            indent_bytes,
1199                            virtual_marker_space: marker_match.virtual_marker_space,
1200                        };
1201
1202                        if let Some(nested_marker) = is_content_nested_bullet_marker(
1203                            content_line,
1204                            marker_match.marker_len,
1205                            marker_match.spaces_after_bytes,
1206                        ) {
1207                            lists::add_list_item_with_nested_empty_list(
1208                                &mut self.containers,
1209                                &mut self.builder,
1210                                &list_item,
1211                                nested_marker,
1212                            );
1213                        } else {
1214                            lists::add_list_item(
1215                                &mut self.containers,
1216                                &mut self.builder,
1217                                &list_item,
1218                                self.config,
1219                            );
1220                        }
1221                    } else if let Some(fence) = code_blocks::try_parse_fence_open(content_slice) {
1222                        self.containers.push(Container::Definition {
1223                            content_col,
1224                            plain_open: false,
1225                            plain_buffer: TextBuffer::new(),
1226                        });
1227                        definition_pushed = true;
1228
1229                        let bq_depth = self.current_blockquote_depth();
1230                        if let Some(indent_str) = indent_to_emit {
1231                            self.builder
1232                                .token(SyntaxKind::WHITESPACE.into(), indent_str);
1233                        }
1234                        let fence_line = current_line[content_start..].to_string();
1235                        let new_pos = if self.config.extensions.tex_math_gfm
1236                            && code_blocks::is_gfm_math_fence(&fence)
1237                        {
1238                            code_blocks::parse_fenced_math_block(
1239                                &mut self.builder,
1240                                &self.lines,
1241                                self.pos,
1242                                fence,
1243                                bq_depth,
1244                                content_col,
1245                                Some(&fence_line),
1246                            )
1247                        } else {
1248                            code_blocks::parse_fenced_code_block(
1249                                &mut self.builder,
1250                                &self.lines,
1251                                self.pos,
1252                                fence,
1253                                bq_depth,
1254                                content_col,
1255                                Some(&fence_line),
1256                            )
1257                        };
1258                        self.pos = new_pos - 1;
1259                    } else {
1260                        let (_, newline_str) = strip_newline(current_line);
1261                        let (content_without_newline, _) = strip_newline(after_marker_and_spaces);
1262                        if content_without_newline.is_empty() {
1263                            plain_buffer.push_line(newline_str);
1264                        } else {
1265                            let line_with_newline = if !newline_str.is_empty() {
1266                                format!("{}{}", content_without_newline, newline_str)
1267                            } else {
1268                                content_without_newline.to_string()
1269                            };
1270                            plain_buffer.push_line(line_with_newline);
1271                        }
1272                    }
1273                }
1274
1275                if !definition_pushed {
1276                    self.containers.push(Container::Definition {
1277                        content_col,
1278                        plain_open: *has_content,
1279                        plain_buffer,
1280                    });
1281                }
1282            }
1283            DefinitionPrepared::Term { blank_count } => {
1284                self.emit_buffered_plain_if_needed();
1285
1286                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1287                    self.close_containers_to(self.containers.depth() - 1);
1288                }
1289
1290                if !definition_lists::in_definition_list(&self.containers) {
1291                    self.builder.start_node(SyntaxKind::DEFINITION_LIST.into());
1292                    self.containers.push(Container::DefinitionList {});
1293                }
1294
1295                while matches!(
1296                    self.containers.last(),
1297                    Some(Container::Definition { .. }) | Some(Container::DefinitionItem { .. })
1298                ) {
1299                    self.close_containers_to(self.containers.depth() - 1);
1300                }
1301
1302                self.builder.start_node(SyntaxKind::DEFINITION_ITEM.into());
1303                self.containers.push(Container::DefinitionItem {});
1304
1305                emit_term(&mut self.builder, content, self.config);
1306
1307                for i in 0..*blank_count {
1308                    let blank_pos = self.pos + 1 + i;
1309                    if blank_pos < self.lines.len() {
1310                        let blank_line = self.lines[blank_pos];
1311                        self.builder.start_node(SyntaxKind::BLANK_LINE.into());
1312                        self.builder
1313                            .token(SyntaxKind::BLANK_LINE.into(), blank_line);
1314                        self.builder.finish_node();
1315                    }
1316                }
1317                self.pos += *blank_count;
1318            }
1319        }
1320    }
1321
1322    /// Get current blockquote depth from container stack.
1323    fn blockquote_marker_info(
1324        &self,
1325        payload: Option<&BlockQuotePrepared>,
1326        line: &str,
1327    ) -> Vec<marker_utils::BlockQuoteMarkerInfo> {
1328        payload
1329            .map(|payload| payload.marker_info.clone())
1330            .unwrap_or_else(|| parse_blockquote_marker_info(line))
1331    }
1332
1333    /// Build blockquote marker metadata for the current source line.
1334    ///
1335    /// When a blockquote marker is detected at a shifted list content column
1336    /// (e.g. `    > ...` inside a list item), the prefix indentation must be
1337    /// folded into the first marker's leading spaces for lossless emission.
1338    fn marker_info_for_line(
1339        &self,
1340        payload: Option<&BlockQuotePrepared>,
1341        raw_line: &str,
1342        marker_line: &str,
1343        shifted_prefix: &str,
1344        used_shifted: bool,
1345    ) -> Vec<marker_utils::BlockQuoteMarkerInfo> {
1346        let mut marker_info = if used_shifted {
1347            parse_blockquote_marker_info(marker_line)
1348        } else {
1349            self.blockquote_marker_info(payload, raw_line)
1350        };
1351        if used_shifted && !shifted_prefix.is_empty() {
1352            let (prefix_cols, _) = leading_indent(shifted_prefix);
1353            if let Some(first) = marker_info.first_mut() {
1354                first.leading_spaces += prefix_cols;
1355            }
1356        }
1357        marker_info
1358    }
1359
1360    /// Detect blockquote markers that begin at list-content indentation instead
1361    /// of column 0 on the physical line.
1362    fn shifted_blockquote_from_list<'b>(
1363        &self,
1364        line: &'b str,
1365    ) -> Option<(usize, &'b str, &'b str, &'b str)> {
1366        if !lists::in_list(&self.containers) {
1367            return None;
1368        }
1369        let list_content_col = paragraphs::current_content_col(&self.containers);
1370        let content_container_indent = self.content_container_indent_to_strip();
1371        let marker_col = list_content_col.saturating_add(content_container_indent);
1372        if marker_col == 0 {
1373            return None;
1374        }
1375
1376        let (indent_cols, _) = leading_indent(line);
1377        if indent_cols < marker_col {
1378            return None;
1379        }
1380
1381        let idx = byte_index_at_column(line, marker_col);
1382        if idx > line.len() {
1383            return None;
1384        }
1385
1386        let candidate = &line[idx..];
1387        let (candidate_depth, candidate_inner) = count_blockquote_markers(candidate);
1388        if candidate_depth == 0 {
1389            return None;
1390        }
1391
1392        Some((candidate_depth, candidate_inner, candidate, &line[..idx]))
1393    }
1394
1395    fn emit_blockquote_markers(
1396        &mut self,
1397        marker_info: &[marker_utils::BlockQuoteMarkerInfo],
1398        depth: usize,
1399    ) {
1400        for i in 0..depth {
1401            if let Some(info) = marker_info.get(i) {
1402                blockquotes::emit_one_blockquote_marker(
1403                    &mut self.builder,
1404                    info.leading_spaces,
1405                    info.has_trailing_space,
1406                );
1407            }
1408        }
1409    }
1410
1411    fn current_blockquote_depth(&self) -> usize {
1412        blockquotes::current_blockquote_depth(&self.containers)
1413    }
1414
1415    /// Emit or buffer a blockquote marker depending on parser state.
1416    ///
1417    /// If a paragraph is open and we're using integrated parsing, buffer the marker.
1418    /// Otherwise emit it directly to the builder.
1419    fn emit_or_buffer_blockquote_marker(
1420        &mut self,
1421        leading_spaces: usize,
1422        has_trailing_space: bool,
1423    ) {
1424        if let Some(Container::ListItem {
1425            buffer,
1426            marker_only,
1427            ..
1428        }) = self.containers.stack.last_mut()
1429        {
1430            buffer.push_blockquote_marker(leading_spaces, has_trailing_space);
1431            *marker_only = false;
1432            return;
1433        }
1434
1435        // If paragraph is open, buffer the marker (it will be emitted at correct position)
1436        if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1437            // Buffer the marker in the paragraph
1438            paragraphs::append_paragraph_marker(
1439                &mut self.containers,
1440                leading_spaces,
1441                has_trailing_space,
1442            );
1443        } else {
1444            // Emit directly
1445            blockquotes::emit_one_blockquote_marker(
1446                &mut self.builder,
1447                leading_spaces,
1448                has_trailing_space,
1449            );
1450        }
1451    }
1452
1453    fn parse_document_stack(&mut self) {
1454        self.builder.start_node(SyntaxKind::DOCUMENT.into());
1455
1456        log::trace!("Starting document parse");
1457
1458        // Pandoc title block is handled via the block dispatcher.
1459
1460        while self.pos < self.lines.len() {
1461            let line = self.lines[self.pos];
1462
1463            log::trace!("Parsing line {}: {}", self.pos + 1, line);
1464
1465            if self.parse_line(line) {
1466                continue;
1467            }
1468            self.pos += 1;
1469        }
1470
1471        self.close_containers_to(0);
1472        self.builder.finish_node(); // DOCUMENT
1473    }
1474
1475    /// Returns true if the line was consumed.
1476    fn parse_line(&mut self, line: &str) -> bool {
1477        // Count blockquote markers on this line. Inside list items, blockquotes can begin
1478        // at the list content column (e.g. `    > ...` after `1. `), not at column 0.
1479        let (mut bq_depth, mut inner_content) = count_blockquote_markers(line);
1480        let mut bq_marker_line = line;
1481        let mut shifted_bq_prefix = "";
1482        let mut used_shifted_bq = false;
1483        if bq_depth == 0
1484            && let Some((candidate_depth, candidate_inner, candidate_line, candidate_prefix)) =
1485                self.shifted_blockquote_from_list(line)
1486        {
1487            bq_depth = candidate_depth;
1488            inner_content = candidate_inner;
1489            bq_marker_line = candidate_line;
1490            shifted_bq_prefix = candidate_prefix;
1491            used_shifted_bq = true;
1492        }
1493        let current_bq_depth = self.current_blockquote_depth();
1494
1495        let has_blank_before = self.pos == 0 || is_blank_line(self.lines[self.pos - 1]);
1496        let mut blockquote_match: Option<PreparedBlockMatch> = None;
1497        let dispatcher_ctx = if current_bq_depth == 0 {
1498            Some(BlockContext {
1499                content: line,
1500                has_blank_before,
1501                has_blank_before_strict: has_blank_before,
1502                at_document_start: self.pos == 0,
1503                in_fenced_div: self.in_fenced_div(),
1504                blockquote_depth: current_bq_depth,
1505                config: self.config,
1506                content_indent: 0,
1507                indent_to_emit: None,
1508                list_indent_info: None,
1509                in_list: lists::in_list(&self.containers),
1510                in_marker_only_list_item: matches!(
1511                    self.containers.last(),
1512                    Some(Container::ListItem {
1513                        marker_only: true,
1514                        ..
1515                    })
1516                ),
1517                next_line: if self.pos + 1 < self.lines.len() {
1518                    Some(self.lines[self.pos + 1])
1519                } else {
1520                    None
1521                },
1522            })
1523        } else {
1524            None
1525        };
1526
1527        let blockquote_payload = if let Some(dispatcher_ctx) = dispatcher_ctx.as_ref() {
1528            self.block_registry
1529                .detect_prepared(dispatcher_ctx, &self.lines, self.pos)
1530                .and_then(|prepared| {
1531                    if matches!(prepared.effect, BlockEffect::OpenBlockQuote) {
1532                        blockquote_match = Some(prepared);
1533                        blockquote_match.as_ref().and_then(|prepared| {
1534                            prepared
1535                                .payload
1536                                .as_ref()
1537                                .and_then(|payload| payload.downcast_ref::<BlockQuotePrepared>())
1538                                .cloned()
1539                        })
1540                    } else {
1541                        None
1542                    }
1543                })
1544        } else {
1545            None
1546        };
1547
1548        log::trace!(
1549            "parse_line [{}]: bq_depth={}, current_bq={}, depth={}, line={:?}",
1550            self.pos,
1551            bq_depth,
1552            current_bq_depth,
1553            self.containers.depth(),
1554            line.trim_end()
1555        );
1556
1557        // Handle blank lines specially (including blank lines inside blockquotes)
1558        // A line like ">" with nothing after is a blank line inside a blockquote
1559        let is_blank = is_blank_line(line) || (bq_depth > 0 && is_blank_line(inner_content));
1560
1561        if is_blank {
1562            if self.is_paragraph_open()
1563                && paragraphs::has_open_inline_math_environment(&self.containers)
1564            {
1565                paragraphs::append_paragraph_line(
1566                    &mut self.containers,
1567                    &mut self.builder,
1568                    line,
1569                    self.config,
1570                );
1571                self.pos += 1;
1572                return true;
1573            }
1574
1575            // Close paragraph if open
1576            self.close_paragraph_if_open();
1577
1578            // Close Plain node in Definition if open
1579            // Blank lines should close Plain, allowing subsequent content to be siblings
1580            // Emit buffered PLAIN content before continuing
1581            self.emit_buffered_plain_if_needed();
1582
1583            // Note: Blank lines between terms and definitions are now preserved
1584            // and emitted as part of the term parsing logic
1585
1586            // For blank lines inside blockquotes, we need to handle them at the right depth.
1587            // If a shifted blockquote marker was detected in list-item content, preserve the
1588            // leading shifted indentation before the first marker for losslessness.
1589            // First, adjust blockquote depth if needed
1590            if bq_depth > current_bq_depth {
1591                // Open blockquotes
1592                for _ in current_bq_depth..bq_depth {
1593                    self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
1594                    self.containers.push(Container::BlockQuote {});
1595                }
1596            } else if bq_depth < current_bq_depth {
1597                // Close blockquotes down to bq_depth (must use Parser close to emit buffers)
1598                self.close_blockquotes_to_depth(bq_depth);
1599            }
1600
1601            // Peek ahead to determine what containers to keep open
1602            let mut peek = self.pos + 1;
1603            while peek < self.lines.len() && is_blank_line(self.lines[peek]) {
1604                peek += 1;
1605            }
1606
1607            // Determine what containers to keep open based on next line
1608            let levels_to_keep = if peek < self.lines.len() {
1609                ContinuationPolicy::new(self.config, &self.block_registry).compute_levels_to_keep(
1610                    self.current_blockquote_depth(),
1611                    &self.containers,
1612                    &self.lines,
1613                    peek,
1614                    self.lines[peek],
1615                )
1616            } else {
1617                0
1618            };
1619            log::trace!(
1620                "Blank line: depth={}, levels_to_keep={}, next='{}'",
1621                self.containers.depth(),
1622                levels_to_keep,
1623                if peek < self.lines.len() {
1624                    self.lines[peek]
1625                } else {
1626                    "<EOF>"
1627                }
1628            );
1629
1630            // Check if blank line should be buffered in a ListItem BEFORE closing containers
1631
1632            // Close containers down to the level we want to keep
1633            while self.containers.depth() > levels_to_keep {
1634                match self.containers.last() {
1635                    Some(Container::ListItem { .. }) => {
1636                        // levels_to_keep wants to close the ListItem - blank line is between items
1637                        log::trace!(
1638                            "Closing ListItem at blank line (levels_to_keep={} < depth={})",
1639                            levels_to_keep,
1640                            self.containers.depth()
1641                        );
1642                        self.close_containers_to(self.containers.depth() - 1);
1643                    }
1644                    Some(Container::List { .. })
1645                    | Some(Container::FootnoteDefinition { .. })
1646                    | Some(Container::Alert { .. })
1647                    | Some(Container::Paragraph { .. })
1648                    | Some(Container::Definition { .. })
1649                    | Some(Container::DefinitionItem { .. })
1650                    | Some(Container::DefinitionList { .. }) => {
1651                        log::trace!(
1652                            "Closing {:?} at blank line (depth {} > levels_to_keep {})",
1653                            self.containers.last(),
1654                            self.containers.depth(),
1655                            levels_to_keep
1656                        );
1657
1658                        self.close_containers_to(self.containers.depth() - 1);
1659                    }
1660                    _ => break,
1661                }
1662            }
1663
1664            // If we kept a list item open, its first-line text may still be buffered.
1665            // Flush it *before* emitting the blank line node (and its blockquote markers)
1666            // so byte order matches the source.
1667            if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
1668                self.emit_list_item_buffer_if_needed();
1669            }
1670
1671            // Emit blockquote markers for this blank line if inside blockquotes
1672            if bq_depth > 0 {
1673                let marker_info = self.marker_info_for_line(
1674                    blockquote_payload.as_ref(),
1675                    line,
1676                    bq_marker_line,
1677                    shifted_bq_prefix,
1678                    used_shifted_bq,
1679                );
1680                self.emit_blockquote_markers(&marker_info, bq_depth);
1681            }
1682
1683            self.builder.start_node(SyntaxKind::BLANK_LINE.into());
1684            self.builder
1685                .token(SyntaxKind::BLANK_LINE.into(), inner_content);
1686            self.builder.finish_node();
1687
1688            self.pos += 1;
1689            return true;
1690        }
1691
1692        // Handle blockquote depth changes
1693        if bq_depth > current_bq_depth {
1694            // Need to open new blockquote(s)
1695            // But first check blank_before_blockquote requirement
1696            if self.config.extensions.blank_before_blockquote
1697                && current_bq_depth == 0
1698                && !used_shifted_bq
1699                && !blockquote_payload
1700                    .as_ref()
1701                    .map(|payload| payload.can_start)
1702                    .unwrap_or_else(|| blockquotes::can_start_blockquote(self.pos, &self.lines))
1703            {
1704                // Can't start blockquote without blank line - treat as paragraph
1705                // Flush any pending list-item inline buffer first so this line
1706                // stays in source order relative to buffered list text.
1707                self.emit_list_item_buffer_if_needed();
1708                paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
1709                paragraphs::append_paragraph_line(
1710                    &mut self.containers,
1711                    &mut self.builder,
1712                    line,
1713                    self.config,
1714                );
1715                self.pos += 1;
1716                return true;
1717            }
1718
1719            // For nested blockquotes, also need blank line before (blank_before_blockquote)
1720            // Check if previous line inside the blockquote was blank
1721            let can_nest = if current_bq_depth > 0 {
1722                if self.config.extensions.blank_before_blockquote {
1723                    // Check if we're right after a blank line or at start of blockquote
1724                    matches!(self.containers.last(), Some(Container::BlockQuote { .. }))
1725                        || (self.pos > 0 && {
1726                            let prev_line = self.lines[self.pos - 1];
1727                            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
1728                            prev_bq_depth >= current_bq_depth && is_blank_line(prev_inner)
1729                        })
1730                } else {
1731                    true
1732                }
1733            } else {
1734                blockquote_payload
1735                    .as_ref()
1736                    .map(|payload| payload.can_nest)
1737                    .unwrap_or(true)
1738            };
1739
1740            if !can_nest {
1741                // Can't nest deeper - treat extra > as content
1742                // Only strip markers up to current depth
1743                let content_at_current_depth =
1744                    blockquotes::strip_n_blockquote_markers(line, current_bq_depth);
1745
1746                // Emit blockquote markers for current depth (for losslessness)
1747                let marker_info = self.marker_info_for_line(
1748                    blockquote_payload.as_ref(),
1749                    line,
1750                    bq_marker_line,
1751                    shifted_bq_prefix,
1752                    used_shifted_bq,
1753                );
1754                for i in 0..current_bq_depth {
1755                    if let Some(info) = marker_info.get(i) {
1756                        self.emit_or_buffer_blockquote_marker(
1757                            info.leading_spaces,
1758                            info.has_trailing_space,
1759                        );
1760                    }
1761                }
1762
1763                if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1764                    // Lazy continuation with the extra > as content
1765                    paragraphs::append_paragraph_line(
1766                        &mut self.containers,
1767                        &mut self.builder,
1768                        content_at_current_depth,
1769                        self.config,
1770                    );
1771                    self.pos += 1;
1772                    return true;
1773                } else {
1774                    // Start new paragraph with the extra > as content
1775                    paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
1776                    paragraphs::append_paragraph_line(
1777                        &mut self.containers,
1778                        &mut self.builder,
1779                        content_at_current_depth,
1780                        self.config,
1781                    );
1782                    self.pos += 1;
1783                    return true;
1784                }
1785            }
1786
1787            // Preserve source order when a deeper blockquote line arrives while
1788            // list-item text is still buffered (e.g. issue #174).
1789            self.emit_list_item_buffer_if_needed();
1790
1791            // Close paragraph before opening blockquote
1792            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1793                self.close_containers_to(self.containers.depth() - 1);
1794            }
1795
1796            // Parse marker information for all levels
1797            let marker_info = self.marker_info_for_line(
1798                blockquote_payload.as_ref(),
1799                line,
1800                bq_marker_line,
1801                shifted_bq_prefix,
1802                used_shifted_bq,
1803            );
1804
1805            if let (Some(dispatcher_ctx), Some(prepared)) =
1806                (dispatcher_ctx.as_ref(), blockquote_match.as_ref())
1807            {
1808                let _ = self.block_registry.parse_prepared(
1809                    prepared,
1810                    dispatcher_ctx,
1811                    &mut self.builder,
1812                    &self.lines,
1813                    self.pos,
1814                );
1815                for _ in 0..bq_depth {
1816                    self.containers.push(Container::BlockQuote {});
1817                }
1818            } else {
1819                // First, emit markers for existing blockquote levels (before opening new ones)
1820                for level in 0..current_bq_depth {
1821                    if let Some(info) = marker_info.get(level) {
1822                        self.emit_or_buffer_blockquote_marker(
1823                            info.leading_spaces,
1824                            info.has_trailing_space,
1825                        );
1826                    }
1827                }
1828
1829                // Then open new blockquotes and emit their markers
1830                for level in current_bq_depth..bq_depth {
1831                    self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
1832
1833                    // Emit the marker for this new level
1834                    if let Some(info) = marker_info.get(level) {
1835                        blockquotes::emit_one_blockquote_marker(
1836                            &mut self.builder,
1837                            info.leading_spaces,
1838                            info.has_trailing_space,
1839                        );
1840                    }
1841
1842                    self.containers.push(Container::BlockQuote {});
1843                }
1844            }
1845
1846            // Now parse the inner content
1847            // Pass inner_content as line_to_append since markers are already stripped
1848            return self.parse_inner_content(inner_content, Some(inner_content));
1849        } else if bq_depth < current_bq_depth {
1850            // Need to close some blockquotes, but first check for lazy continuation
1851            // Lazy continuation: line with fewer (or zero) > markers continues
1852            // a paragraph that started at a deeper blockquote level. CommonMark
1853            // §5.1 explicitly allows this regardless of how many `>` markers
1854            // are on the lazy line.
1855            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1856                // CommonMark §5.1: lazy continuation does *not* fire if
1857                // the line would itself be a paragraph-interrupting block
1858                // (e.g. a thematic break) — instead the paragraph closes,
1859                // any open blockquotes close, and the line opens that
1860                // block at the outer level. Pandoc keeps the lazy text
1861                // append in this case.
1862                let is_commonmark = self.config.dialect == crate::options::Dialect::CommonMark;
1863                let interrupts_via_hr = is_commonmark && try_parse_horizontal_rule(line).is_some();
1864                let interrupts_via_fence =
1865                    is_commonmark && code_blocks::try_parse_fence_open(line).is_some();
1866                if !interrupts_via_hr && !interrupts_via_fence {
1867                    if bq_depth > 0 {
1868                        // Buffer the explicit `>` markers we have into the
1869                        // paragraph (it's at the deeper blockquote level, so
1870                        // structurally the markers belong to outer levels but
1871                        // they're tucked inside the paragraph for losslessness;
1872                        // the formatter re-emits prefixes from container nesting).
1873                        let marker_info = self.marker_info_for_line(
1874                            blockquote_payload.as_ref(),
1875                            line,
1876                            bq_marker_line,
1877                            shifted_bq_prefix,
1878                            used_shifted_bq,
1879                        );
1880                        for i in 0..bq_depth {
1881                            if let Some(info) = marker_info.get(i) {
1882                                paragraphs::append_paragraph_marker(
1883                                    &mut self.containers,
1884                                    info.leading_spaces,
1885                                    info.has_trailing_space,
1886                                );
1887                            }
1888                        }
1889                        paragraphs::append_paragraph_line(
1890                            &mut self.containers,
1891                            &mut self.builder,
1892                            inner_content,
1893                            self.config,
1894                        );
1895                    } else {
1896                        paragraphs::append_paragraph_line(
1897                            &mut self.containers,
1898                            &mut self.builder,
1899                            line,
1900                            self.config,
1901                        );
1902                    }
1903                    self.pos += 1;
1904                    return true;
1905                }
1906            }
1907            // CommonMark §5.1: a no-`>` line that begins a list marker
1908            // closes the blockquote and starts a fresh list at the outer
1909            // level rather than continuing the inner list. Pandoc keeps
1910            // the inner list going (lazy list continuation across
1911            // blockquote depth).
1912            if bq_depth == 0 && self.config.dialect != crate::options::Dialect::CommonMark {
1913                // Check for lazy list continuation - if we're in a list item and
1914                // this line looks like a list item with matching marker
1915                if lists::in_blockquote_list(&self.containers)
1916                    && let Some(marker_match) = try_parse_list_marker(line, self.config)
1917                {
1918                    let (indent_cols, indent_bytes) = leading_indent(line);
1919                    if let Some(level) = lists::find_matching_list_level(
1920                        &self.containers,
1921                        &marker_match.marker,
1922                        indent_cols,
1923                        self.config.dialect,
1924                    ) {
1925                        // Continue the list inside the blockquote
1926                        // Close containers to the target level, emitting buffers properly
1927                        self.close_containers_to(level + 1);
1928
1929                        // Close any open paragraph or list item at this level
1930                        if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1931                            self.close_containers_to(self.containers.depth() - 1);
1932                        }
1933                        if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
1934                            self.close_containers_to(self.containers.depth() - 1);
1935                        }
1936
1937                        // Check if content is a nested bullet marker
1938                        if let Some(nested_marker) = is_content_nested_bullet_marker(
1939                            line,
1940                            marker_match.marker_len,
1941                            marker_match.spaces_after_bytes,
1942                        ) {
1943                            let list_item = ListItemEmissionInput {
1944                                content: line,
1945                                marker_len: marker_match.marker_len,
1946                                spaces_after_cols: marker_match.spaces_after_cols,
1947                                spaces_after_bytes: marker_match.spaces_after_bytes,
1948                                indent_cols,
1949                                indent_bytes,
1950                                virtual_marker_space: marker_match.virtual_marker_space,
1951                            };
1952                            lists::add_list_item_with_nested_empty_list(
1953                                &mut self.containers,
1954                                &mut self.builder,
1955                                &list_item,
1956                                nested_marker,
1957                            );
1958                        } else {
1959                            let list_item = ListItemEmissionInput {
1960                                content: line,
1961                                marker_len: marker_match.marker_len,
1962                                spaces_after_cols: marker_match.spaces_after_cols,
1963                                spaces_after_bytes: marker_match.spaces_after_bytes,
1964                                indent_cols,
1965                                indent_bytes,
1966                                virtual_marker_space: marker_match.virtual_marker_space,
1967                            };
1968                            lists::add_list_item(
1969                                &mut self.containers,
1970                                &mut self.builder,
1971                                &list_item,
1972                                self.config,
1973                            );
1974                        }
1975                        self.pos += 1;
1976                        return true;
1977                    }
1978                }
1979            }
1980
1981            // Not lazy continuation - close paragraph if open
1982            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
1983                self.close_containers_to(self.containers.depth() - 1);
1984            }
1985
1986            // Close blockquotes down to the new depth (must use Parser close to emit buffers)
1987            self.close_blockquotes_to_depth(bq_depth);
1988
1989            // Parse the inner content at the new depth
1990            if bq_depth > 0 {
1991                // Emit markers at current depth before parsing content
1992                let marker_info = self.marker_info_for_line(
1993                    blockquote_payload.as_ref(),
1994                    line,
1995                    bq_marker_line,
1996                    shifted_bq_prefix,
1997                    used_shifted_bq,
1998                );
1999                for i in 0..bq_depth {
2000                    if let Some(info) = marker_info.get(i) {
2001                        self.emit_or_buffer_blockquote_marker(
2002                            info.leading_spaces,
2003                            info.has_trailing_space,
2004                        );
2005                    }
2006                }
2007                // Content with markers stripped - use inner_content for paragraph appending
2008                return self.parse_inner_content(inner_content, Some(inner_content));
2009            } else {
2010                // Not inside blockquotes - use original line
2011                return self.parse_inner_content(line, None);
2012            }
2013        } else if bq_depth > 0 {
2014            // Same blockquote depth - emit markers and continue parsing inner content
2015            let mut list_item_continuation = false;
2016            let same_depth_marker_info = self.marker_info_for_line(
2017                blockquote_payload.as_ref(),
2018                line,
2019                bq_marker_line,
2020                shifted_bq_prefix,
2021                used_shifted_bq,
2022            );
2023            let has_explicit_same_depth_marker = same_depth_marker_info.len() >= bq_depth;
2024
2025            // Check if we should close the ListItem
2026            // ListItem should continue if the line is properly indented for continuation
2027            if matches!(
2028                self.containers.last(),
2029                Some(Container::ListItem { content_col: _, .. })
2030            ) {
2031                let (indent_cols, _) = leading_indent(inner_content);
2032                let content_indent = self.content_container_indent_to_strip();
2033                let effective_indent = indent_cols.saturating_sub(content_indent);
2034                let content_col = match self.containers.last() {
2035                    Some(Container::ListItem { content_col, .. }) => *content_col,
2036                    _ => 0,
2037                };
2038
2039                // Check if this line starts a new list item at outer level
2040                let is_new_item_at_outer_level =
2041                    if try_parse_list_marker(inner_content, self.config).is_some() {
2042                        effective_indent < content_col
2043                    } else {
2044                        false
2045                    };
2046
2047                // Close ListItem if:
2048                // 1. It's a new list item at an outer (or same) level, OR
2049                // 2. The line is not indented enough to continue the current item
2050                if is_new_item_at_outer_level
2051                    || (effective_indent < content_col && !has_explicit_same_depth_marker)
2052                {
2053                    log::trace!(
2054                        "Closing ListItem: is_new_item={}, effective_indent={} < content_col={}",
2055                        is_new_item_at_outer_level,
2056                        effective_indent,
2057                        content_col
2058                    );
2059                    self.close_containers_to(self.containers.depth() - 1);
2060                } else {
2061                    log::trace!(
2062                        "Keeping ListItem: effective_indent={} >= content_col={}",
2063                        effective_indent,
2064                        content_col
2065                    );
2066                    list_item_continuation = true;
2067                }
2068            }
2069
2070            // Fenced code blocks inside list items need marker emission in this branch.
2071            // If we keep continuation buffering for these lines, opening fence markers in
2072            // blockquote contexts can be dropped from CST text.
2073            if list_item_continuation && code_blocks::try_parse_fence_open(inner_content).is_some()
2074            {
2075                list_item_continuation = false;
2076            }
2077
2078            let continuation_has_explicit_marker = list_item_continuation && {
2079                if has_explicit_same_depth_marker {
2080                    for i in 0..bq_depth {
2081                        if let Some(info) = same_depth_marker_info.get(i) {
2082                            self.emit_or_buffer_blockquote_marker(
2083                                info.leading_spaces,
2084                                info.has_trailing_space,
2085                            );
2086                        }
2087                    }
2088                    true
2089                } else {
2090                    false
2091                }
2092            };
2093
2094            if !list_item_continuation {
2095                let marker_info = self.marker_info_for_line(
2096                    blockquote_payload.as_ref(),
2097                    line,
2098                    bq_marker_line,
2099                    shifted_bq_prefix,
2100                    used_shifted_bq,
2101                );
2102                for i in 0..bq_depth {
2103                    if let Some(info) = marker_info.get(i) {
2104                        self.emit_or_buffer_blockquote_marker(
2105                            info.leading_spaces,
2106                            info.has_trailing_space,
2107                        );
2108                    }
2109                }
2110            }
2111            let line_to_append = if list_item_continuation {
2112                if continuation_has_explicit_marker {
2113                    Some(inner_content)
2114                } else {
2115                    Some(line)
2116                }
2117            } else {
2118                Some(inner_content)
2119            };
2120            return self.parse_inner_content(inner_content, line_to_append);
2121        }
2122
2123        // No blockquote markers - parse as regular content
2124        // But check for lazy continuation first
2125        if current_bq_depth > 0 {
2126            // Check for lazy paragraph continuation
2127            if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
2128                paragraphs::append_paragraph_line(
2129                    &mut self.containers,
2130                    &mut self.builder,
2131                    line,
2132                    self.config,
2133                );
2134                self.pos += 1;
2135                return true;
2136            }
2137
2138            // Check for lazy list continuation
2139            if lists::in_blockquote_list(&self.containers)
2140                && let Some(marker_match) = try_parse_list_marker(line, self.config)
2141            {
2142                let (indent_cols, indent_bytes) = leading_indent(line);
2143                if let Some(level) = lists::find_matching_list_level(
2144                    &self.containers,
2145                    &marker_match.marker,
2146                    indent_cols,
2147                    self.config.dialect,
2148                ) {
2149                    // Close containers to the target level, emitting buffers properly
2150                    self.close_containers_to(level + 1);
2151
2152                    // Close any open paragraph or list item at this level
2153                    if matches!(self.containers.last(), Some(Container::Paragraph { .. })) {
2154                        self.close_containers_to(self.containers.depth() - 1);
2155                    }
2156                    if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
2157                        self.close_containers_to(self.containers.depth() - 1);
2158                    }
2159
2160                    // Check if content is a nested bullet marker
2161                    if let Some(nested_marker) = is_content_nested_bullet_marker(
2162                        line,
2163                        marker_match.marker_len,
2164                        marker_match.spaces_after_bytes,
2165                    ) {
2166                        let list_item = ListItemEmissionInput {
2167                            content: line,
2168                            marker_len: marker_match.marker_len,
2169                            spaces_after_cols: marker_match.spaces_after_cols,
2170                            spaces_after_bytes: marker_match.spaces_after_bytes,
2171                            indent_cols,
2172                            indent_bytes,
2173                            virtual_marker_space: marker_match.virtual_marker_space,
2174                        };
2175                        lists::add_list_item_with_nested_empty_list(
2176                            &mut self.containers,
2177                            &mut self.builder,
2178                            &list_item,
2179                            nested_marker,
2180                        );
2181                    } else {
2182                        let list_item = ListItemEmissionInput {
2183                            content: line,
2184                            marker_len: marker_match.marker_len,
2185                            spaces_after_cols: marker_match.spaces_after_cols,
2186                            spaces_after_bytes: marker_match.spaces_after_bytes,
2187                            indent_cols,
2188                            indent_bytes,
2189                            virtual_marker_space: marker_match.virtual_marker_space,
2190                        };
2191                        lists::add_list_item(
2192                            &mut self.containers,
2193                            &mut self.builder,
2194                            &list_item,
2195                            self.config,
2196                        );
2197                    }
2198                    self.pos += 1;
2199                    return true;
2200                }
2201            }
2202        }
2203
2204        // No blockquote markers - use original line
2205        self.parse_inner_content(line, None)
2206    }
2207
2208    /// Get the total indentation to strip from content containers (footnotes + definitions).
2209    fn content_container_indent_to_strip(&self) -> usize {
2210        self.containers
2211            .stack
2212            .iter()
2213            .filter_map(|c| match c {
2214                Container::FootnoteDefinition { content_col, .. } => Some(*content_col),
2215                Container::Definition { content_col, .. } => Some(*content_col),
2216                _ => None,
2217            })
2218            .sum()
2219    }
2220
2221    /// Parse content inside blockquotes (or at top level).
2222    ///
2223    /// `content` - The content to parse (may have indent/markers stripped)
2224    /// `line_to_append` - Optional line to use when appending to paragraphs.
2225    ///                    If None, uses self.lines[self.pos]
2226    fn parse_inner_content(&mut self, content: &str, line_to_append: Option<&str>) -> bool {
2227        log::trace!(
2228            "parse_inner_content [{}]: depth={}, last={:?}, content={:?}",
2229            self.pos,
2230            self.containers.depth(),
2231            self.containers.last(),
2232            content.trim_end()
2233        );
2234        // Calculate how much indentation should be stripped for content containers
2235        // (definitions, footnotes) FIRST, so we can check for block markers correctly
2236        let content_indent = self.content_container_indent_to_strip();
2237        let (stripped_content, indent_to_emit) = if content_indent > 0 {
2238            let (indent_cols, _) = leading_indent(content);
2239            if indent_cols >= content_indent {
2240                let idx = byte_index_at_column(content, content_indent);
2241                (&content[idx..], Some(&content[..idx]))
2242            } else {
2243                // Line has less indent than required - preserve leading whitespace
2244                let trimmed_start = content.trim_start();
2245                let ws_len = content.len() - trimmed_start.len();
2246                if ws_len > 0 {
2247                    (trimmed_start, Some(&content[..ws_len]))
2248                } else {
2249                    (content, None)
2250                }
2251            }
2252        } else {
2253            (content, None)
2254        };
2255
2256        if self.config.extensions.alerts
2257            && self.current_blockquote_depth() > 0
2258            && !self.in_active_alert()
2259            && !self.is_paragraph_open()
2260            && let Some(marker) = Self::alert_marker_from_content(stripped_content)
2261        {
2262            let (_, newline_str) = strip_newline(stripped_content);
2263            self.builder.start_node(SyntaxKind::ALERT.into());
2264            self.builder.token(SyntaxKind::ALERT_MARKER.into(), marker);
2265            if !newline_str.is_empty() {
2266                self.builder.token(SyntaxKind::NEWLINE.into(), newline_str);
2267            }
2268            self.containers.push(Container::Alert {
2269                blockquote_depth: self.current_blockquote_depth(),
2270            });
2271            self.pos += 1;
2272            return true;
2273        }
2274
2275        // Check if we're in a Definition container (with or without an open PLAIN)
2276        // Continuation lines should be added to PLAIN, not treated as new blocks
2277        // BUT: Don't treat lines with block element markers as continuations
2278        if matches!(self.containers.last(), Some(Container::Definition { .. })) {
2279            let is_definition_marker =
2280                definition_lists::try_parse_definition_marker(stripped_content).is_some()
2281                    && !stripped_content.starts_with(':');
2282            if content_indent == 0 && is_definition_marker {
2283                // Definition markers at top-level should start a new definition.
2284            } else {
2285                let policy = ContinuationPolicy::new(self.config, &self.block_registry);
2286
2287                if policy.definition_plain_can_continue(
2288                    stripped_content,
2289                    content,
2290                    content_indent,
2291                    &BlockContext {
2292                        content: stripped_content,
2293                        has_blank_before: self.pos == 0 || is_blank_line(self.lines[self.pos - 1]),
2294                        has_blank_before_strict: self.pos == 0
2295                            || is_blank_line(self.lines[self.pos - 1]),
2296                        at_document_start: self.pos == 0 && self.current_blockquote_depth() == 0,
2297                        in_fenced_div: self.in_fenced_div(),
2298                        blockquote_depth: self.current_blockquote_depth(),
2299                        config: self.config,
2300                        content_indent,
2301                        indent_to_emit: None,
2302                        list_indent_info: None,
2303                        in_list: lists::in_list(&self.containers),
2304                        in_marker_only_list_item: matches!(
2305                            self.containers.last(),
2306                            Some(Container::ListItem {
2307                                marker_only: true,
2308                                ..
2309                            })
2310                        ),
2311                        next_line: if self.pos + 1 < self.lines.len() {
2312                            Some(self.lines[self.pos + 1])
2313                        } else {
2314                            None
2315                        },
2316                    },
2317                    &self.lines,
2318                    self.pos,
2319                ) {
2320                    let content_line = stripped_content;
2321                    let (text_without_newline, newline_str) = strip_newline(content_line);
2322                    let indent_prefix = if !text_without_newline.trim().is_empty() {
2323                        indent_to_emit.unwrap_or("")
2324                    } else {
2325                        ""
2326                    };
2327                    let content_line = format!("{}{}", indent_prefix, text_without_newline);
2328
2329                    if let Some(Container::Definition {
2330                        plain_open,
2331                        plain_buffer,
2332                        ..
2333                    }) = self.containers.stack.last_mut()
2334                    {
2335                        let line_with_newline = if !newline_str.is_empty() {
2336                            format!("{}{}", content_line, newline_str)
2337                        } else {
2338                            content_line
2339                        };
2340                        plain_buffer.push_line(line_with_newline);
2341                        *plain_open = true;
2342                    }
2343
2344                    self.pos += 1;
2345                    return true;
2346                }
2347            }
2348        }
2349
2350        // Handle blockquotes that appear after stripping content-container indentation
2351        // (e.g. `    > quote` inside a definition list item).
2352        if content_indent > 0 {
2353            let (bq_depth, inner_content) = count_blockquote_markers(stripped_content);
2354            let current_bq_depth = self.current_blockquote_depth();
2355            let in_footnote_definition = self
2356                .containers
2357                .stack
2358                .iter()
2359                .any(|container| matches!(container, Container::FootnoteDefinition { .. }));
2360
2361            if bq_depth > 0 {
2362                if in_footnote_definition
2363                    && self.config.extensions.blank_before_blockquote
2364                    && current_bq_depth == 0
2365                    && !blockquotes::can_start_blockquote(self.pos, &self.lines)
2366                {
2367                    // Respect blank_before_blockquote even when `>` appears only
2368                    // after stripping content-container indentation (e.g. footnotes).
2369                    // In that case the marker should be treated as paragraph text.
2370                } else {
2371                    // If definition/list plain text is buffered, flush it before opening nested
2372                    // blockquotes so block order remains lossless and stable across reparse.
2373                    self.emit_buffered_plain_if_needed();
2374                    self.emit_list_item_buffer_if_needed();
2375
2376                    // Blockquotes can nest inside content containers; preserve the stripped indentation
2377                    // as WHITESPACE before the first marker for losslessness.
2378                    self.close_paragraph_if_open();
2379
2380                    if bq_depth > current_bq_depth {
2381                        let marker_info = parse_blockquote_marker_info(stripped_content);
2382
2383                        // Open new blockquotes and emit their markers.
2384                        for level in current_bq_depth..bq_depth {
2385                            self.builder.start_node(SyntaxKind::BLOCK_QUOTE.into());
2386
2387                            if level == current_bq_depth
2388                                && let Some(indent_str) = indent_to_emit
2389                            {
2390                                self.builder
2391                                    .token(SyntaxKind::WHITESPACE.into(), indent_str);
2392                            }
2393
2394                            if let Some(info) = marker_info.get(level) {
2395                                blockquotes::emit_one_blockquote_marker(
2396                                    &mut self.builder,
2397                                    info.leading_spaces,
2398                                    info.has_trailing_space,
2399                                );
2400                            }
2401
2402                            self.containers.push(Container::BlockQuote {});
2403                        }
2404                    } else if bq_depth < current_bq_depth {
2405                        self.close_blockquotes_to_depth(bq_depth);
2406                    } else {
2407                        // Same depth: emit markers for losslessness.
2408                        let marker_info = parse_blockquote_marker_info(stripped_content);
2409                        self.emit_blockquote_markers(&marker_info, bq_depth);
2410                    }
2411
2412                    return self.parse_inner_content(inner_content, Some(inner_content));
2413                }
2414            }
2415        }
2416
2417        // Store the stripped content for later use
2418        let content = stripped_content;
2419
2420        if self.is_paragraph_open()
2421            && (paragraphs::has_open_inline_math_environment(&self.containers)
2422                || paragraphs::has_open_display_math_dollars(&self.containers))
2423        {
2424            paragraphs::append_paragraph_line(
2425                &mut self.containers,
2426                &mut self.builder,
2427                line_to_append.unwrap_or(self.lines[self.pos]),
2428                self.config,
2429            );
2430            self.pos += 1;
2431            return true;
2432        }
2433
2434        // Precompute dispatcher match once per line (reused by multiple branches below).
2435        // This covers: blocks requiring blank lines, blocks that can interrupt paragraphs,
2436        // and blocks that can appear without blank lines (e.g. reference definitions).
2437        use super::blocks::lists;
2438        use super::blocks::paragraphs;
2439        let list_indent_info = if lists::in_list(&self.containers) {
2440            let content_col = paragraphs::current_content_col(&self.containers);
2441            if content_col > 0 {
2442                Some(super::block_dispatcher::ListIndentInfo { content_col })
2443            } else {
2444                None
2445            }
2446        } else {
2447            None
2448        };
2449
2450        let next_line = if self.pos + 1 < self.lines.len() {
2451            // For lookahead-based blocks (e.g. setext headings), the dispatcher expects
2452            // `ctx.next_line` to be in the same “inner content” form as `ctx.content`.
2453            Some(count_blockquote_markers(self.lines[self.pos + 1]).1)
2454        } else {
2455            None
2456        };
2457
2458        let current_bq_depth = self.current_blockquote_depth();
2459        if let Some(alert_bq_depth) = self.active_alert_blockquote_depth()
2460            && current_bq_depth < alert_bq_depth
2461        {
2462            while matches!(self.containers.last(), Some(Container::Alert { .. })) {
2463                self.close_containers_to(self.containers.depth() - 1);
2464            }
2465        }
2466
2467        let dispatcher_ctx = BlockContext {
2468            content,
2469            has_blank_before: false,        // filled in later
2470            has_blank_before_strict: false, // filled in later
2471            at_document_start: false,       // filled in later
2472            in_fenced_div: self.in_fenced_div(),
2473            blockquote_depth: current_bq_depth,
2474            config: self.config,
2475            content_indent,
2476            indent_to_emit,
2477            list_indent_info,
2478            in_list: lists::in_list(&self.containers),
2479            in_marker_only_list_item: matches!(
2480                self.containers.last(),
2481                Some(Container::ListItem {
2482                    marker_only: true,
2483                    ..
2484                })
2485            ),
2486            next_line,
2487        };
2488
2489        // We'll update these two fields shortly (after they are computed), but we can still
2490        // use this ctx shape to avoid rebuilding repeated context objects.
2491        let mut dispatcher_ctx = dispatcher_ctx;
2492
2493        // Setext heading folded over a list item's buffered first-line text.
2494        // Must run before block detection so that an HR-shaped underline like
2495        // `---` doesn't get claimed by the thematic-break parser.
2496        if self.try_fold_list_item_buffer_into_setext(stripped_content) {
2497            return true;
2498        }
2499
2500        // Initial detection (before blank/doc-start are computed). Note: this can
2501        // match reference definitions, but footnotes are handled explicitly later.
2502        let dispatcher_match =
2503            self.block_registry
2504                .detect_prepared(&dispatcher_ctx, &self.lines, self.pos);
2505
2506        // Check for heading (needs blank line before, or at start of container)
2507        // Note: for fenced div nesting, the line immediately after a div opening fence
2508        // should be treated like the start of a container (Pandoc allows nested fences
2509        // without an intervening blank line). Similarly, the first line after a metadata
2510        // block (YAML/Pandoc title/MMD title) is treated as having a blank before it.
2511        let after_metadata_block = std::mem::replace(&mut self.after_metadata_block, false);
2512        let has_blank_before = if self.pos == 0 || after_metadata_block {
2513            true
2514        } else {
2515            let prev_line = self.lines[self.pos - 1];
2516            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
2517            let (prev_inner_no_nl, _) = strip_newline(prev_inner);
2518            let prev_is_fenced_div_open = self.config.extensions.fenced_divs
2519                && fenced_divs::try_parse_div_fence_open(
2520                    strip_n_blockquote_markers(prev_inner_no_nl, prev_bq_depth).trim_start(),
2521                )
2522                .is_some();
2523
2524            let prev_line_blank = is_blank_line(prev_line);
2525            prev_line_blank
2526                || prev_is_fenced_div_open
2527                || matches!(self.containers.last(), Some(Container::BlockQuote { .. }))
2528                || !self.previous_block_requires_blank_before_heading()
2529        };
2530
2531        // For indented code blocks, we need a stricter condition - only actual blank lines count
2532        // Being at document start (pos == 0) is OK only if we're not inside a blockquote
2533        let at_document_start = self.pos == 0 && current_bq_depth == 0;
2534
2535        let prev_line_blank = if self.pos > 0 {
2536            let prev_line = self.lines[self.pos - 1];
2537            let (prev_bq_depth, prev_inner) = count_blockquote_markers(prev_line);
2538            is_blank_line(prev_line) || (prev_bq_depth > 0 && is_blank_line(prev_inner))
2539        } else {
2540            false
2541        };
2542        let has_blank_before_strict = at_document_start || prev_line_blank;
2543
2544        dispatcher_ctx.has_blank_before = has_blank_before;
2545        dispatcher_ctx.has_blank_before_strict = has_blank_before_strict;
2546        dispatcher_ctx.at_document_start = at_document_start;
2547
2548        let dispatcher_match =
2549            if dispatcher_ctx.has_blank_before || dispatcher_ctx.at_document_start {
2550                // Recompute now that blank/doc-start conditions are known.
2551                self.block_registry
2552                    .detect_prepared(&dispatcher_ctx, &self.lines, self.pos)
2553            } else {
2554                dispatcher_match
2555            };
2556
2557        if has_blank_before {
2558            if let Some(env_name) = extract_environment_name(content)
2559                && is_inline_math_environment(env_name)
2560            {
2561                if !self.is_paragraph_open() {
2562                    paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
2563                }
2564                paragraphs::append_paragraph_line(
2565                    &mut self.containers,
2566                    &mut self.builder,
2567                    line_to_append.unwrap_or(self.lines[self.pos]),
2568                    self.config,
2569                );
2570                self.pos += 1;
2571                return true;
2572            }
2573
2574            if let Some(block_match) = dispatcher_match.as_ref() {
2575                let detection = block_match.detection;
2576
2577                match detection {
2578                    BlockDetectionResult::YesCanInterrupt => {
2579                        self.emit_list_item_buffer_if_needed();
2580                        if self.is_paragraph_open() {
2581                            self.close_containers_to(self.containers.depth() - 1);
2582                        }
2583                    }
2584                    BlockDetectionResult::Yes => {
2585                        self.prepare_for_block_element();
2586                    }
2587                    BlockDetectionResult::No => unreachable!(),
2588                }
2589
2590                if matches!(block_match.effect, BlockEffect::CloseFencedDiv) {
2591                    self.close_containers_to_fenced_div();
2592                }
2593
2594                if matches!(block_match.effect, BlockEffect::OpenFootnoteDefinition) {
2595                    self.close_open_footnote_definition();
2596                }
2597
2598                let lines_consumed = self.block_registry.parse_prepared(
2599                    block_match,
2600                    &dispatcher_ctx,
2601                    &mut self.builder,
2602                    &self.lines,
2603                    self.pos,
2604                );
2605
2606                if matches!(
2607                    self.block_registry.parser_name(block_match),
2608                    "yaml_metadata" | "pandoc_title_block" | "mmd_title_block"
2609                ) {
2610                    self.after_metadata_block = true;
2611                }
2612
2613                match block_match.effect {
2614                    BlockEffect::None => {}
2615                    BlockEffect::OpenFencedDiv => {
2616                        self.containers.push(Container::FencedDiv {});
2617                    }
2618                    BlockEffect::CloseFencedDiv => {
2619                        self.close_fenced_div();
2620                    }
2621                    BlockEffect::OpenFootnoteDefinition => {
2622                        self.handle_footnote_open_effect(block_match, content);
2623                    }
2624                    BlockEffect::OpenList => {
2625                        self.handle_list_open_effect(block_match, content, indent_to_emit);
2626                    }
2627                    BlockEffect::OpenDefinitionList => {
2628                        self.handle_definition_list_effect(block_match, content, indent_to_emit);
2629                    }
2630                    BlockEffect::OpenBlockQuote => {
2631                        // Detection only for now; keep core blockquote handling intact.
2632                    }
2633                }
2634
2635                if lines_consumed == 0 {
2636                    log::warn!(
2637                        "block parser made no progress at line {} (parser={})",
2638                        self.pos + 1,
2639                        self.block_registry.parser_name(block_match)
2640                    );
2641                    return false;
2642                }
2643
2644                self.pos += lines_consumed;
2645                return true;
2646            }
2647        } else if let Some(block_match) = dispatcher_match.as_ref() {
2648            // Without blank-before, only allow interrupting blocks OR blocks that are
2649            // explicitly allowed without blank lines (e.g. reference definitions).
2650            let parser_name = self.block_registry.parser_name(block_match);
2651            match block_match.detection {
2652                BlockDetectionResult::YesCanInterrupt => {
2653                    if matches!(block_match.effect, BlockEffect::OpenFencedDiv)
2654                        && self.is_paragraph_open()
2655                    {
2656                        // Fenced divs must not interrupt paragraphs without a blank line.
2657                        if !self.is_paragraph_open() {
2658                            paragraphs::start_paragraph_if_needed(
2659                                &mut self.containers,
2660                                &mut self.builder,
2661                            );
2662                        }
2663                        paragraphs::append_paragraph_line(
2664                            &mut self.containers,
2665                            &mut self.builder,
2666                            line_to_append.unwrap_or(self.lines[self.pos]),
2667                            self.config,
2668                        );
2669                        self.pos += 1;
2670                        return true;
2671                    }
2672
2673                    if matches!(block_match.effect, BlockEffect::OpenList)
2674                        && self.is_paragraph_open()
2675                        && !lists::in_list(&self.containers)
2676                        && self.content_container_indent_to_strip() == 0
2677                    {
2678                        // CommonMark §5.2: bullet lists and ordered lists with
2679                        // start = 1 may interrupt a paragraph; ordered lists
2680                        // with any other start cannot. Pandoc-markdown forbids
2681                        // *any* list from interrupting a paragraph without a
2682                        // blank line.
2683                        let allow_interrupt =
2684                            self.config.dialect == crate::options::Dialect::CommonMark && {
2685                                use super::block_dispatcher::ListPrepared;
2686                                use super::blocks::lists::OrderedMarker;
2687                                let prepared = block_match
2688                                    .payload
2689                                    .as_ref()
2690                                    .and_then(|p| p.downcast_ref::<ListPrepared>());
2691                                match prepared.map(|p| &p.marker) {
2692                                    Some(ListMarker::Bullet(_)) => true,
2693                                    Some(ListMarker::Ordered(OrderedMarker::Decimal {
2694                                        number,
2695                                        ..
2696                                    })) => number == "1",
2697                                    _ => false,
2698                                }
2699                            };
2700                        if !allow_interrupt {
2701                            paragraphs::append_paragraph_line(
2702                                &mut self.containers,
2703                                &mut self.builder,
2704                                line_to_append.unwrap_or(self.lines[self.pos]),
2705                                self.config,
2706                            );
2707                            self.pos += 1;
2708                            return true;
2709                        }
2710                    }
2711
2712                    // CommonMark spec example #312: a "list marker" at indent
2713                    // ≥ 4 isn't actually a marker when it can't reach the
2714                    // deepest item's content column AND no list level matches
2715                    // at that indent. Treat as lazy paragraph continuation of
2716                    // the deepest open list item or paragraph rather than
2717                    // flushing the buffer and opening a new sibling list.
2718                    if matches!(block_match.effect, BlockEffect::OpenList)
2719                        && self.try_lazy_list_continuation(block_match, content)
2720                    {
2721                        self.pos += 1;
2722                        return true;
2723                    }
2724
2725                    self.emit_list_item_buffer_if_needed();
2726                    if self.is_paragraph_open() {
2727                        self.close_containers_to(self.containers.depth() - 1);
2728                    }
2729
2730                    // CommonMark §5.2: a thematic break / ATX heading /
2731                    // fenced code at column 0 cannot continue an open list
2732                    // item whose content column is greater than the line's
2733                    // indent — close the surrounding list before emitting.
2734                    // OpenList is excluded so that a same-level marker still
2735                    // continues the list rather than closing it.
2736                    if self.config.dialect == crate::options::Dialect::CommonMark
2737                        && !matches!(block_match.effect, BlockEffect::OpenList)
2738                    {
2739                        let (indent_cols, _) = leading_indent(content);
2740                        self.close_lists_above_indent(indent_cols);
2741                    }
2742                }
2743                BlockDetectionResult::Yes => {
2744                    // CommonMark multi-line setext: when an open paragraph is
2745                    // followed by a setext underline, the entire paragraph
2746                    // becomes the heading content. The dispatcher reports
2747                    // setext at the line *before* the underline (the last text
2748                    // line); fold the buffered paragraph + this line into a
2749                    // single HEADING. Pandoc-markdown disagrees (it never
2750                    // forms a multi-line setext), so this branch is dialect-
2751                    // gated; under Pandoc, a setext detection while a
2752                    // paragraph is open never reaches this point because
2753                    // `blank_before_header` is on by default and gates out the
2754                    // detection earlier in `SetextHeadingParser::detect_prepared`.
2755                    if parser_name == "setext_heading"
2756                        && self.is_paragraph_open()
2757                        && self.config.dialect == crate::options::Dialect::CommonMark
2758                    {
2759                        let text_line = self.lines[self.pos];
2760                        let underline_line = self.lines[self.pos + 1];
2761                        let underline_char = underline_line.trim().chars().next().unwrap_or('=');
2762                        let level = if underline_char == '=' { 1 } else { 2 };
2763                        self.emit_setext_heading_folding_paragraph(
2764                            text_line,
2765                            underline_line,
2766                            level,
2767                        );
2768                        self.pos += 2;
2769                        return true;
2770                    }
2771
2772                    // Keep ambiguous fenced-div openers from interrupting an
2773                    // active paragraph without a blank line.
2774                    if parser_name == "fenced_div_open" && self.is_paragraph_open() {
2775                        if !self.is_paragraph_open() {
2776                            paragraphs::start_paragraph_if_needed(
2777                                &mut self.containers,
2778                                &mut self.builder,
2779                            );
2780                        }
2781                        paragraphs::append_paragraph_line(
2782                            &mut self.containers,
2783                            &mut self.builder,
2784                            line_to_append.unwrap_or(self.lines[self.pos]),
2785                            self.config,
2786                        );
2787                        self.pos += 1;
2788                        return true;
2789                    }
2790
2791                    // Reference definitions cannot interrupt a paragraph
2792                    // (CommonMark §4.7 / Pandoc-markdown agree).
2793                    if parser_name == "reference_definition" && self.is_paragraph_open() {
2794                        paragraphs::append_paragraph_line(
2795                            &mut self.containers,
2796                            &mut self.builder,
2797                            line_to_append.unwrap_or(self.lines[self.pos]),
2798                            self.config,
2799                        );
2800                        self.pos += 1;
2801                        return true;
2802                    }
2803                }
2804                BlockDetectionResult::No => unreachable!(),
2805            }
2806
2807            if !matches!(block_match.detection, BlockDetectionResult::No) {
2808                if matches!(block_match.effect, BlockEffect::CloseFencedDiv) {
2809                    self.close_containers_to_fenced_div();
2810                }
2811
2812                if matches!(block_match.effect, BlockEffect::OpenFootnoteDefinition) {
2813                    self.close_open_footnote_definition();
2814                }
2815
2816                let lines_consumed = self.block_registry.parse_prepared(
2817                    block_match,
2818                    &dispatcher_ctx,
2819                    &mut self.builder,
2820                    &self.lines,
2821                    self.pos,
2822                );
2823
2824                match block_match.effect {
2825                    BlockEffect::None => {}
2826                    BlockEffect::OpenFencedDiv => {
2827                        self.containers.push(Container::FencedDiv {});
2828                    }
2829                    BlockEffect::CloseFencedDiv => {
2830                        self.close_fenced_div();
2831                    }
2832                    BlockEffect::OpenFootnoteDefinition => {
2833                        self.handle_footnote_open_effect(block_match, content);
2834                    }
2835                    BlockEffect::OpenList => {
2836                        self.handle_list_open_effect(block_match, content, indent_to_emit);
2837                    }
2838                    BlockEffect::OpenDefinitionList => {
2839                        self.handle_definition_list_effect(block_match, content, indent_to_emit);
2840                    }
2841                    BlockEffect::OpenBlockQuote => {
2842                        // Detection only for now; keep core blockquote handling intact.
2843                    }
2844                }
2845
2846                if lines_consumed == 0 {
2847                    log::warn!(
2848                        "block parser made no progress at line {} (parser={})",
2849                        self.pos + 1,
2850                        self.block_registry.parser_name(block_match)
2851                    );
2852                    return false;
2853                }
2854
2855                self.pos += lines_consumed;
2856                return true;
2857            }
2858        }
2859
2860        // Check for line block (if line_blocks extension is enabled)
2861        if self.config.extensions.line_blocks
2862            && (has_blank_before || self.pos == 0)
2863            && try_parse_line_block_start(content).is_some()
2864            // Guard against context-stripped content (e.g. inside blockquotes) that
2865            // looks like a line block while the raw source line does not. Calling
2866            // parse_line_block on raw lines in that state would consume 0 lines.
2867            && try_parse_line_block_start(self.lines[self.pos]).is_some()
2868        {
2869            log::trace!("Parsed line block at line {}", self.pos);
2870            // Close paragraph before opening line block
2871            self.close_paragraph_if_open();
2872
2873            let new_pos = parse_line_block(&self.lines, self.pos, &mut self.builder, self.config);
2874            if new_pos > self.pos {
2875                self.pos = new_pos;
2876                return true;
2877            }
2878        }
2879
2880        // Paragraph or list item continuation
2881        // Check if we're inside a ListItem - if so, buffer the content instead of emitting
2882        if matches!(self.containers.last(), Some(Container::ListItem { .. })) {
2883            log::trace!(
2884                "Inside ListItem - buffering content: {:?}",
2885                line_to_append.unwrap_or(self.lines[self.pos]).trim_end()
2886            );
2887            // Inside list item - buffer content for later parsing
2888            let line = line_to_append.unwrap_or(self.lines[self.pos]);
2889
2890            // Add line to buffer in the ListItem container
2891            if let Some(Container::ListItem {
2892                buffer,
2893                marker_only,
2894                ..
2895            }) = self.containers.stack.last_mut()
2896            {
2897                buffer.push_text(line);
2898                if !is_blank_line(line) {
2899                    *marker_only = false;
2900                }
2901            }
2902
2903            self.pos += 1;
2904            return true;
2905        }
2906
2907        log::trace!(
2908            "Not in ListItem - creating paragraph for: {:?}",
2909            line_to_append.unwrap_or(self.lines[self.pos]).trim_end()
2910        );
2911        // Not in list item - create paragraph as usual
2912        paragraphs::start_paragraph_if_needed(&mut self.containers, &mut self.builder);
2913        // For lossless parsing: use line_to_append if provided (e.g., for blockquotes
2914        // where markers have been stripped), otherwise use the original line
2915        let line = line_to_append.unwrap_or(self.lines[self.pos]);
2916        paragraphs::append_paragraph_line(
2917            &mut self.containers,
2918            &mut self.builder,
2919            line,
2920            self.config,
2921        );
2922        self.pos += 1;
2923        true
2924    }
2925
2926    fn fenced_div_container_index(&self) -> Option<usize> {
2927        self.containers
2928            .stack
2929            .iter()
2930            .rposition(|c| matches!(c, Container::FencedDiv { .. }))
2931    }
2932
2933    fn close_containers_to_fenced_div(&mut self) {
2934        if let Some(index) = self.fenced_div_container_index() {
2935            self.close_containers_to(index + 1);
2936        }
2937    }
2938
2939    fn close_fenced_div(&mut self) {
2940        if let Some(index) = self.fenced_div_container_index() {
2941            self.close_containers_to(index);
2942        }
2943    }
2944
2945    fn in_fenced_div(&self) -> bool {
2946        self.containers
2947            .stack
2948            .iter()
2949            .any(|c| matches!(c, Container::FencedDiv { .. }))
2950    }
2951}