Skip to main content

panache_parser/parser/
core.rs

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