panache_parser/parser/blocks/
paragraphs.rs1use 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, OpenDisplayMath};
11use crate::parser::utils::helpers::trim_end_newlines;
12use crate::parser::utils::text_buffer::ParagraphBuffer;
13
14fn dollar_run(trimmed: &str) -> Option<(usize, &str)> {
17 let run_len = trimmed.bytes().take_while(|b| *b == b'$').count();
18 if run_len < 2 {
19 return None;
20 }
21 Some((run_len, &trimmed[run_len..]))
22}
23
24fn scan_bracket_delimiters(
32 line: &str,
33 mut state: Option<OpenDisplayMath>,
34 config: &ParserOptions,
35) -> Option<OpenDisplayMath> {
36 let single = config.extensions.tex_math_single_backslash;
37 let double = config.extensions.tex_math_double_backslash;
38 if !single && !double {
39 return state;
40 }
41
42 let bytes = line.as_bytes();
43 let mut i = 0;
44 while i < bytes.len() {
45 match state {
46 None => {
47 if double && bytes[i..].starts_with(br"\\[") {
48 state = Some(OpenDisplayMath::DoubleBrackets);
49 i += 3;
50 } else if single && bytes[i..].starts_with(br"\[") {
51 state = Some(OpenDisplayMath::SingleBrackets);
52 i += 2;
53 } else {
54 i += 1;
55 }
56 }
57 Some(OpenDisplayMath::SingleBrackets) => {
58 if bytes[i..].starts_with(br"\]") {
59 state = None;
60 i += 2;
61 } else {
62 i += 1;
63 }
64 }
65 Some(OpenDisplayMath::DoubleBrackets) => {
66 if bytes[i..].starts_with(br"\\]") {
67 state = None;
68 i += 3;
69 } else {
70 i += 1;
71 }
72 }
73 Some(OpenDisplayMath::Dollars(_)) => return state,
76 }
77 }
78 state
79}
80
81pub(in crate::parser) fn update_display_math_state(
90 line_no_newline: &str,
91 open_display_math: &mut Option<OpenDisplayMath>,
92 config: &ParserOptions,
93) {
94 match *open_display_math {
95 Some(OpenDisplayMath::Dollars(open_len)) => {
96 let trimmed = line_no_newline.trim();
97 if let Some((run_len, rest)) = dollar_run(trimmed) {
98 let is_pure_dollars = rest.is_empty();
99 let is_quarto_equation_attr_closer = rest.starts_with(char::is_whitespace) && {
100 let attr = rest.trim_start();
101 attr.starts_with('{') && attr.ends_with('}')
102 };
103 if (is_pure_dollars || is_quarto_equation_attr_closer) && run_len >= open_len {
104 *open_display_math = None;
105 }
106 }
107 }
108 Some(OpenDisplayMath::SingleBrackets) | Some(OpenDisplayMath::DoubleBrackets) => {
109 *open_display_math =
110 scan_bracket_delimiters(line_no_newline, *open_display_math, config);
111 }
112 None => {
113 let trimmed = line_no_newline.trim();
114 if let Some((run_len, rest)) = dollar_run(trimmed)
115 && rest.is_empty()
116 {
117 *open_display_math = Some(OpenDisplayMath::Dollars(run_len));
118 } else {
119 *open_display_math = scan_bracket_delimiters(line_no_newline, None, config);
120 }
121 }
122 }
123}
124
125fn extract_end_environment_name(line: &str) -> Option<&str> {
126 let trimmed = line.trim_start();
127 if !trimmed.starts_with("\\end{") {
128 return None;
129 }
130 let rest = &trimmed[5..];
131 let close = rest.find('}')?;
132 let name = &rest[..close];
133 if name.is_empty() {
134 return None;
135 }
136 Some(name)
137}
138
139pub(in crate::parser) fn start_paragraph_if_needed(
146 containers: &mut ContainerStack,
147 builder: &mut GreenNodeBuilder<'static>,
148) {
149 if !matches!(containers.last(), Some(Container::Paragraph { .. })) {
150 let start_checkpoint = builder.checkpoint();
151 containers.push(Container::Paragraph {
152 buffer: ParagraphBuffer::new(),
153 open_inline_math_envs: Vec::new(),
154 open_display_math: None,
155 start_checkpoint,
156 });
157 }
158}
159
160pub(in crate::parser) fn append_paragraph_line(
162 containers: &mut ContainerStack,
163 _builder: &mut GreenNodeBuilder<'static>,
164 line: &str,
165 config: &ParserOptions,
166) {
167 if let Some(Container::Paragraph {
170 buffer,
171 open_inline_math_envs,
172 open_display_math,
173 ..
174 }) = containers.stack.last_mut()
175 {
176 buffer.push_text(line);
177
178 let line_no_newline = trim_end_newlines(line);
179 update_display_math_state(line_no_newline, open_display_math, config);
185 if let Some(env_name) = extract_environment_name(line_no_newline)
186 && is_inline_math_environment(env_name)
187 {
188 open_inline_math_envs.push(env_name.to_string());
189 return;
190 }
191
192 if let Some(end_name) = extract_end_environment_name(line_no_newline)
193 && open_inline_math_envs
194 .last()
195 .is_some_and(|open| open == end_name)
196 {
197 open_inline_math_envs.pop();
198 }
199 }
200}
201
202pub(in crate::parser) fn append_paragraph_marker(
208 containers: &mut ContainerStack,
209 leading_spaces: usize,
210 has_trailing_space: bool,
211) {
212 if let Some(Container::Paragraph { buffer, .. }) = containers.stack.last_mut() {
213 buffer.push_marker(leading_spaces, has_trailing_space);
214 }
215}
216
217pub(in crate::parser) fn has_open_inline_math_environment(containers: &ContainerStack) -> bool {
218 matches!(
219 containers.last(),
220 Some(Container::Paragraph {
221 open_inline_math_envs,
222 ..
223 }) if !open_inline_math_envs.is_empty()
224 )
225}
226
227pub(in crate::parser) fn has_open_display_math(containers: &ContainerStack) -> bool {
228 matches!(
229 containers.last(),
230 Some(Container::Paragraph {
231 open_display_math: Some(_),
232 ..
233 })
234 )
235}
236
237pub(in crate::parser) fn current_content_col(containers: &ContainerStack) -> usize {
239 containers
240 .stack
241 .iter()
242 .rev()
243 .find_map(|c| match c {
244 Container::ListItem { content_col, .. } => Some(*content_col),
245 Container::FootnoteDefinition { content_col, .. } => Some(*content_col),
246 _ => None,
247 })
248 .unwrap_or(0)
249}