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 crate::syntax::SyntaxKind;
8use rowan::GreenNodeBuilder;
9
10use crate::parser::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
11use crate::parser::utils::container_stack::{Container, ContainerStack};
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.
59pub(in crate::parser) fn start_paragraph_if_needed(
60    containers: &mut ContainerStack,
61    builder: &mut GreenNodeBuilder<'static>,
62) {
63    if !matches!(containers.last(), Some(Container::Paragraph { .. })) {
64        builder.start_node(SyntaxKind::PARAGRAPH.into());
65        containers.push(Container::Paragraph {
66            buffer: ParagraphBuffer::new(),
67            open_inline_math_envs: Vec::new(),
68            open_display_math_dollar_count: None,
69        });
70    }
71}
72
73/// Append a line to the current paragraph (preserving losslessness).
74pub(in crate::parser) fn append_paragraph_line(
75    containers: &mut ContainerStack,
76    _builder: &mut GreenNodeBuilder<'static>,
77    line: &str,
78    _config: &ParserOptions,
79) {
80    // Buffer the line (with newline for losslessness)
81    // Works for ALL paragraphs including those in blockquotes
82    if let Some(Container::Paragraph {
83        buffer,
84        open_inline_math_envs,
85        open_display_math_dollar_count,
86    }) = containers.stack.last_mut()
87    {
88        buffer.push_text(line);
89
90        let line_no_newline = line.trim_end_matches(&['\r', '\n'][..]);
91        // Track standalone `$$` delimiter lines for all paragraphs so we keep
92        // multi-line display math in a single paragraph parse context.
93        // This prevents `$$` + `\begin{...}` forms from being split into
94        // PARAGRAPH + TEX_BLOCK across parse passes.
95        update_display_math_dollar_state(line_no_newline, open_display_math_dollar_count);
96        if let Some(env_name) = extract_environment_name(line_no_newline)
97            && is_inline_math_environment(&env_name)
98        {
99            open_inline_math_envs.push(env_name);
100            return;
101        }
102
103        if let Some(end_name) = extract_end_environment_name(line_no_newline)
104            && open_inline_math_envs
105                .last()
106                .is_some_and(|open| open == end_name)
107        {
108            open_inline_math_envs.pop();
109        }
110    }
111}
112
113/// Buffer a blockquote marker in the current paragraph.
114///
115/// Called when processing blockquote continuation lines while a paragraph is open
116/// and using integrated inline parsing. The marker will be emitted at the correct
117/// position when the paragraph is closed.
118pub(in crate::parser) fn append_paragraph_marker(
119    containers: &mut ContainerStack,
120    leading_spaces: usize,
121    has_trailing_space: bool,
122) {
123    if let Some(Container::Paragraph { buffer, .. }) = containers.stack.last_mut() {
124        buffer.push_marker(leading_spaces, has_trailing_space);
125    }
126}
127
128pub(in crate::parser) fn has_open_inline_math_environment(containers: &ContainerStack) -> bool {
129    matches!(
130        containers.last(),
131        Some(Container::Paragraph {
132            open_inline_math_envs,
133            ..
134        }) if !open_inline_math_envs.is_empty()
135    )
136}
137
138pub(in crate::parser) fn has_open_display_math_dollars(containers: &ContainerStack) -> bool {
139    matches!(
140        containers.last(),
141        Some(Container::Paragraph {
142            open_display_math_dollar_count,
143            ..
144        }) if open_display_math_dollar_count.is_some()
145    )
146}
147
148/// Get the current content column from the container stack.
149pub(in crate::parser) fn current_content_col(containers: &ContainerStack) -> usize {
150    containers
151        .stack
152        .iter()
153        .rev()
154        .find_map(|c| match c {
155            Container::ListItem { content_col, .. } => Some(*content_col),
156            Container::FootnoteDefinition { content_col, .. } => Some(*content_col),
157            _ => None,
158        })
159        .unwrap_or(0)
160}