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