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