Skip to main content

panache_parser/parser/
core.rs

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