panache_parser/parser/blocks/
line_blocks.rs1use crate::options::ParserOptions;
2use crate::syntax::SyntaxKind;
3use rowan::GreenNodeBuilder;
4
5use super::blockquotes::strip_n_blockquote_markers;
6use super::code_blocks::{emit_content_line_prefixes, strip_list_indent};
7use super::container_prefix::advance_columns;
8use crate::parser::utils::container_stack::byte_index_at_column;
9use crate::parser::utils::helpers::strip_newline;
10use crate::parser::utils::inline_emission;
11
12pub fn try_parse_line_block_start(line: &str) -> Option<()> {
15 let trimmed = line.trim_start();
16 if trimmed.starts_with("| ") || trimmed == "|" {
17 Some(())
18 } else {
19 None
20 }
21}
22
23#[allow(clippy::too_many_arguments)]
35pub fn parse_line_block(
36 lines: &[&str],
37 start_pos: usize,
38 builder: &mut GreenNodeBuilder<'static>,
39 config: &ParserOptions,
40 bq_depth: usize,
41 list_content_col: usize,
42 list_marker_consumed_on_line_0: bool,
43 bq_outer: bool,
44 content_indent: usize,
45) -> usize {
46 log::trace!("Parsing line block at line {}", start_pos + 1);
47
48 builder.start_node(SyntaxKind::LINE_BLOCK.into());
49
50 let mut pos = start_pos;
51 let mut first_line = true;
52
53 while pos < lines.len() {
54 let raw_line = lines[pos];
55
56 let kind = if first_line {
57 LineKind::Marker
60 } else {
61 let peek = silent_strip_container_prefix(
62 raw_line,
63 bq_depth,
64 list_content_col,
65 bq_outer,
66 content_indent,
67 );
68 if parse_line_block_line_marker(peek).is_some() {
69 LineKind::Marker
70 } else if peek.starts_with(' ') && !peek.trim_start().starts_with("| ") {
71 LineKind::Continuation
72 } else {
73 break;
74 }
75 };
76
77 builder.start_node(SyntaxKind::LINE_BLOCK_LINE.into());
78
79 let stripped = if first_line {
84 emit_open_line_prefixes(
85 builder,
86 raw_line,
87 bq_depth,
88 list_content_col,
89 list_marker_consumed_on_line_0,
90 bq_outer,
91 content_indent,
92 )
93 } else {
94 emit_content_line_prefixes(
95 builder,
96 raw_line,
97 bq_depth,
98 list_content_col,
99 bq_outer,
100 content_indent,
101 )
102 };
103
104 match kind {
105 LineKind::Marker => {
106 let content_start = parse_line_block_line_marker(stripped)
107 .expect("marker presence verified upstream");
108 builder.token(
109 SyntaxKind::LINE_BLOCK_MARKER.into(),
110 &stripped[..content_start],
111 );
112 let content = &stripped[content_start..];
113 let (content_without_newline, newline_str) = strip_newline(content);
114 if !content_without_newline.is_empty() {
115 inline_emission::emit_inlines(builder, content_without_newline, config, false);
116 }
117 if !newline_str.is_empty() {
118 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
119 }
120 }
121 LineKind::Continuation => {
122 let (line_without_newline, newline_str) = strip_newline(stripped);
123 if !line_without_newline.is_empty() {
124 inline_emission::emit_inlines(builder, line_without_newline, config, false);
125 }
126 if !newline_str.is_empty() {
127 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
128 }
129 }
130 }
131
132 builder.finish_node(); pos += 1;
134 first_line = false;
135 }
136
137 builder.finish_node(); log::trace!("Parsed line block: lines {}-{}", start_pos + 1, pos);
140
141 pos
142}
143
144enum LineKind {
145 Marker,
146 Continuation,
147}
148
149fn silent_strip_container_prefix<'a>(
153 line: &'a str,
154 bq_depth: usize,
155 list_content_col: usize,
156 bq_outer: bool,
157 content_indent: usize,
158) -> &'a str {
159 let mut s = line;
160 let strip_bq = |s: &mut &'a str| {
161 if bq_depth > 0 {
162 *s = strip_n_blockquote_markers(s, bq_depth);
163 }
164 };
165 let strip_list = |s: &mut &'a str| {
166 if list_content_col > 0 {
167 *s = strip_list_indent(s, list_content_col);
168 }
169 };
170 if bq_outer {
171 strip_bq(&mut s);
172 strip_list(&mut s);
173 } else {
174 strip_list(&mut s);
175 strip_bq(&mut s);
176 }
177 if content_indent > 0 {
178 let indent_bytes = byte_index_at_column(s, content_indent);
179 if s.len() >= indent_bytes {
180 s = &s[indent_bytes..];
181 }
182 }
183 s
184}
185
186fn emit_open_line_prefixes<'a>(
191 builder: &mut GreenNodeBuilder<'static>,
192 source_line: &'a str,
193 bq_depth: usize,
194 list_content_col: usize,
195 list_marker_consumed_on_line_0: bool,
196 bq_outer: bool,
197 content_indent: usize,
198) -> &'a str {
199 let mut s: &'a str = source_line;
200 let mut pending_ws_start: Option<usize> = None;
201 let suppress_list = list_marker_consumed_on_line_0;
202
203 let flush_ws = |builder: &mut GreenNodeBuilder<'static>,
204 pending: &mut Option<usize>,
205 current_offset: usize| {
206 if let Some(start) = *pending
207 && current_offset > start
208 {
209 builder.token(
210 SyntaxKind::WHITESPACE.into(),
211 &source_line[start..current_offset],
212 );
213 }
214 *pending = None;
215 };
216
217 let do_strip_list = |s: &mut &'a str, pending: &mut Option<usize>| {
218 if list_content_col == 0 {
219 return;
220 }
221 let stripped = if suppress_list {
222 advance_columns(s, list_content_col)
223 } else {
224 strip_list_indent(s, list_content_col)
225 };
226 let consumed = s.len() - stripped.len();
227 if consumed > 0 {
228 let start = source_line.len() - s.len();
229 if !suppress_list && pending.is_none() {
230 *pending = Some(start);
231 }
232 *s = stripped;
233 }
234 };
235
236 let do_strip_bq =
237 |builder: &mut GreenNodeBuilder<'static>, s: &mut &'a str, pending: &mut Option<usize>| {
238 if bq_depth == 0 {
239 return;
240 }
241 let current_offset = source_line.len() - s.len();
242 flush_ws(builder, pending, current_offset);
243 *s = strip_n_blockquote_markers(s, bq_depth);
244 };
245
246 if bq_outer {
247 do_strip_bq(builder, &mut s, &mut pending_ws_start);
248 do_strip_list(&mut s, &mut pending_ws_start);
249 } else {
250 do_strip_list(&mut s, &mut pending_ws_start);
251 do_strip_bq(builder, &mut s, &mut pending_ws_start);
252 }
253
254 if content_indent > 0 {
255 let indent_bytes = byte_index_at_column(s, content_indent);
256 if s.len() >= indent_bytes && indent_bytes > 0 {
257 let start = source_line.len() - s.len();
258 if pending_ws_start.is_none() {
259 pending_ws_start = Some(start);
260 }
261 s = &s[indent_bytes..];
262 }
263 }
264
265 let final_offset = source_line.len() - s.len();
266 flush_ws(builder, &mut pending_ws_start, final_offset);
267 s
268}
269
270fn parse_line_block_line_marker(line: &str) -> Option<usize> {
273 let trimmed_start = line.len() - line.trim_start().len();
276 let after_indent = &line[trimmed_start..];
277
278 if after_indent.starts_with("| ") {
279 Some(trimmed_start + 2) } else if after_indent == "|" || after_indent == "|\n" {
281 Some(trimmed_start + 1) } else {
283 None
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_try_parse_line_block_start() {
293 assert!(try_parse_line_block_start("| Some text").is_some());
294 assert!(try_parse_line_block_start("| ").is_some());
295 assert!(try_parse_line_block_start("|").is_some()); assert!(try_parse_line_block_start(" | Some text").is_some());
297
298 assert!(try_parse_line_block_start("|No space").is_none());
300 assert!(try_parse_line_block_start("Regular text").is_none());
301 assert!(try_parse_line_block_start("").is_none());
302 }
303
304 #[test]
305 fn test_parse_line_block_marker() {
306 assert_eq!(parse_line_block_line_marker("| Some text"), Some(2));
307 assert_eq!(parse_line_block_line_marker("| "), Some(2));
308 assert_eq!(parse_line_block_line_marker("|"), Some(1)); assert_eq!(parse_line_block_line_marker(" | Indented"), Some(4));
310
311 assert_eq!(parse_line_block_line_marker("|No space"), None);
313 assert_eq!(parse_line_block_line_marker("Regular"), None);
314 }
315
316 #[test]
317 fn test_simple_line_block() {
318 let input = vec!["| Line one", "| Line two", "| Line three"];
319
320 let mut builder = GreenNodeBuilder::new();
321 let new_pos = parse_line_block(
322 &input,
323 0,
324 &mut builder,
325 &ParserOptions::default(),
326 0,
327 0,
328 false,
329 false,
330 0,
331 );
332
333 assert_eq!(new_pos, 3);
334 }
335
336 #[test]
337 fn test_line_block_with_continuation() {
338 let input = vec![
339 "| This is a long line",
340 " that continues here",
341 "| Second line",
342 ];
343
344 let mut builder = GreenNodeBuilder::new();
345 let new_pos = parse_line_block(
346 &input,
347 0,
348 &mut builder,
349 &ParserOptions::default(),
350 0,
351 0,
352 false,
353 false,
354 0,
355 );
356
357 assert_eq!(new_pos, 3);
358 }
359
360 #[test]
361 fn test_line_block_with_indentation() {
362 let input = vec!["| First line", "| Indented line", "| Back to normal"];
363
364 let mut builder = GreenNodeBuilder::new();
365 let new_pos = parse_line_block(
366 &input,
367 0,
368 &mut builder,
369 &ParserOptions::default(),
370 0,
371 0,
372 false,
373 false,
374 0,
375 );
376
377 assert_eq!(new_pos, 3);
378 }
379
380 #[test]
381 fn test_line_block_stops_at_non_line_block() {
382 let input = vec!["| Line one", "| Line two", "Regular paragraph"];
383
384 let mut builder = GreenNodeBuilder::new();
385 let new_pos = parse_line_block(
386 &input,
387 0,
388 &mut builder,
389 &ParserOptions::default(),
390 0,
391 0,
392 false,
393 false,
394 0,
395 );
396
397 assert_eq!(new_pos, 2); }
399}