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