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 || !trimmed.as_bytes().iter().all(|b| *b == b'$') {
20        return;
21    }
22
23    let run_len = trimmed.len();
24    if run_len < 2 {
25        return;
26    }
27
28    if let Some(open_len) = *open_display_math_dollar_count {
29        if run_len >= open_len {
30            *open_display_math_dollar_count = None;
31        }
32    } else {
33        *open_display_math_dollar_count = Some(run_len);
34    }
35}
36
37fn is_inside_footnote_definition(containers: &ContainerStack) -> bool {
38    containers
39        .stack
40        .iter()
41        .any(|c| matches!(c, Container::FootnoteDefinition { .. }))
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    let in_footnote = is_inside_footnote_definition(containers);
81
82    // Buffer the line (with newline for losslessness)
83    // Works for ALL paragraphs including those in blockquotes
84    if let Some(Container::Paragraph {
85        buffer,
86        open_inline_math_envs,
87        open_display_math_dollar_count,
88    }) = containers.stack.last_mut()
89    {
90        buffer.push_text(line);
91
92        let line_no_newline = line.trim_end_matches(&['\r', '\n'][..]);
93        if in_footnote {
94            update_display_math_dollar_state(line_no_newline, open_display_math_dollar_count);
95        } else {
96            *open_display_math_dollar_count = None;
97        }
98        if let Some(env_name) = extract_environment_name(line_no_newline)
99            && is_inline_math_environment(&env_name)
100        {
101            open_inline_math_envs.push(env_name);
102            return;
103        }
104
105        if let Some(end_name) = extract_end_environment_name(line_no_newline)
106            && open_inline_math_envs
107                .last()
108                .is_some_and(|open| open == end_name)
109        {
110            open_inline_math_envs.pop();
111        }
112    }
113}
114
115/// Buffer a blockquote marker in the current paragraph.
116///
117/// Called when processing blockquote continuation lines while a paragraph is open
118/// and using integrated inline parsing. The marker will be emitted at the correct
119/// position when the paragraph is closed.
120pub(in crate::parser) fn append_paragraph_marker(
121    containers: &mut ContainerStack,
122    leading_spaces: usize,
123    has_trailing_space: bool,
124) {
125    if let Some(Container::Paragraph { buffer, .. }) = containers.stack.last_mut() {
126        buffer.push_marker(leading_spaces, has_trailing_space);
127    }
128}
129
130pub(in crate::parser) fn has_open_inline_math_environment(containers: &ContainerStack) -> bool {
131    matches!(
132        containers.last(),
133        Some(Container::Paragraph {
134            open_inline_math_envs,
135            open_display_math_dollar_count,
136            ..
137        }) if !open_inline_math_envs.is_empty() || open_display_math_dollar_count.is_some()
138    )
139}
140
141/// Get the current content column from the container stack.
142pub(in crate::parser) fn current_content_col(containers: &ContainerStack) -> usize {
143    containers
144        .stack
145        .iter()
146        .rev()
147        .find_map(|c| match c {
148            Container::ListItem { content_col, .. } => Some(*content_col),
149            Container::FootnoteDefinition { content_col, .. } => Some(*content_col),
150            _ => None,
151        })
152        .unwrap_or(0)
153}