Skip to main content

panache_parser/parser/
core.rs

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