Skip to main content

panache_parser/parser/blocks/
paragraphs.rs

1//! Paragraph handling utilities.
2//!
3//! Note: Most paragraph logic is in the main Parser since paragraphs
4//! are tightly integrated with container handling.
5
6use crate::options::ParserOptions;
7use rowan::GreenNodeBuilder;
8
9use crate::parser::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
10use crate::parser::utils::container_stack::{Container, ContainerStack};
11use crate::parser::utils::helpers::trim_end_newlines;
12use crate::parser::utils::text_buffer::ParagraphBuffer;
13
14fn update_display_math_dollar_state(
15    line_no_newline: &str,
16    open_display_math_dollar_count: &mut Option<usize>,
17) {
18    let trimmed = line_no_newline.trim();
19    if trimmed.len() < 2 {
20        return;
21    }
22
23    let run_len = trimmed.bytes().take_while(|b| *b == b'$').count();
24    if run_len < 2 {
25        return;
26    }
27
28    let rest = &trimmed[run_len..];
29    let is_pure_dollars = rest.is_empty();
30    let is_quarto_equation_attr_closer = rest.starts_with(char::is_whitespace) && {
31        let attr = rest.trim_start();
32        attr.starts_with('{') && attr.ends_with('}')
33    };
34
35    if let Some(open_len) = *open_display_math_dollar_count {
36        if (is_pure_dollars || is_quarto_equation_attr_closer) && run_len >= open_len {
37            *open_display_math_dollar_count = None;
38        }
39    } else if is_pure_dollars {
40        *open_display_math_dollar_count = Some(run_len);
41    }
42}
43
44fn extract_end_environment_name(line: &str) -> Option<&str> {
45    let trimmed = line.trim_start();
46    if !trimmed.starts_with("\\end{") {
47        return None;
48    }
49    let rest = &trimmed[5..];
50    let close = rest.find('}')?;
51    let name = &rest[..close];
52    if name.is_empty() {
53        return None;
54    }
55    Some(name)
56}
57
58/// Start a paragraph if not already in one.
59///
60/// Takes a checkpoint at the current builder position so the paragraph can be
61/// retroactively wrapped as `PARAGRAPH` on close, or as `HEADING` for
62/// multi-line setext heading promotion. Nothing is emitted into the builder
63/// here; emission happens at close via `start_node_at(checkpoint, kind)`.
64pub(in crate::parser) fn start_paragraph_if_needed(
65    containers: &mut ContainerStack,
66    builder: &mut GreenNodeBuilder<'static>,
67) {
68    if !matches!(containers.last(), Some(Container::Paragraph { .. })) {
69        let start_checkpoint = builder.checkpoint();
70        containers.push(Container::Paragraph {
71            buffer: ParagraphBuffer::new(),
72            open_inline_math_envs: Vec::new(),
73            open_display_math_dollar_count: None,
74            start_checkpoint,
75        });
76    }
77}
78
79/// Append a line to the current paragraph (preserving losslessness).
80pub(in crate::parser) fn append_paragraph_line(
81    containers: &mut ContainerStack,
82    _builder: &mut GreenNodeBuilder<'static>,
83    line: &str,
84    _config: &ParserOptions,
85) {
86    // Buffer the line (with newline for losslessness)
87    // Works for ALL paragraphs including those in blockquotes
88    if let Some(Container::Paragraph {
89        buffer,
90        open_inline_math_envs,
91        open_display_math_dollar_count,
92        ..
93    }) = containers.stack.last_mut()
94    {
95        buffer.push_text(line);
96
97        let line_no_newline = trim_end_newlines(line);
98        // Track standalone `$$` delimiter lines for all paragraphs so we keep
99        // multi-line display math in a single paragraph parse context.
100        // This prevents `$$` + `\begin{...}` forms from being split into
101        // PARAGRAPH + TEX_BLOCK across parse passes.
102        update_display_math_dollar_state(line_no_newline, open_display_math_dollar_count);
103        if let Some(env_name) = extract_environment_name(line_no_newline)
104            && is_inline_math_environment(env_name)
105        {
106            open_inline_math_envs.push(env_name.to_string());
107            return;
108        }
109
110        if let Some(end_name) = extract_end_environment_name(line_no_newline)
111            && open_inline_math_envs
112                .last()
113                .is_some_and(|open| open == end_name)
114        {
115            open_inline_math_envs.pop();
116        }
117    }
118}
119
120/// Buffer a blockquote marker in the current paragraph.
121///
122/// Called when processing blockquote continuation lines while a paragraph is open
123/// and using integrated inline parsing. The marker will be emitted at the correct
124/// position when the paragraph is closed.
125pub(in crate::parser) fn append_paragraph_marker(
126    containers: &mut ContainerStack,
127    leading_spaces: usize,
128    has_trailing_space: bool,
129) {
130    if let Some(Container::Paragraph { buffer, .. }) = containers.stack.last_mut() {
131        buffer.push_marker(leading_spaces, has_trailing_space);
132    }
133}
134
135pub(in crate::parser) fn has_open_inline_math_environment(containers: &ContainerStack) -> bool {
136    matches!(
137        containers.last(),
138        Some(Container::Paragraph {
139            open_inline_math_envs,
140            ..
141        }) if !open_inline_math_envs.is_empty()
142    )
143}
144
145pub(in crate::parser) fn has_open_display_math_dollars(containers: &ContainerStack) -> bool {
146    matches!(
147        containers.last(),
148        Some(Container::Paragraph {
149            open_display_math_dollar_count,
150            ..
151        }) if open_display_math_dollar_count.is_some()
152    )
153}
154
155/// Get the current content column from the container stack.
156pub(in crate::parser) fn current_content_col(containers: &ContainerStack) -> usize {
157    containers
158        .stack
159        .iter()
160        .rev()
161        .find_map(|c| match c {
162            Container::ListItem { content_col, .. } => Some(*content_col),
163            Container::FootnoteDefinition { content_col, .. } => Some(*content_col),
164            _ => None,
165        })
166        .unwrap_or(0)
167}