Skip to main content

panache_parser/parser/
core.rs

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