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::container_prefix::{
7 StrippedLines, advance_columns, bq_outer_of_list, strip_list_indent,
8};
9use crate::parser::utils::container_stack::byte_index_at_column;
10use crate::parser::utils::helpers::strip_newline;
11use crate::parser::utils::inline_emission;
12
13pub fn try_parse_line_block_start(line: &str) -> Option<()> {
16 let trimmed = line.trim_start();
17 if trimmed.starts_with("| ") || trimmed == "|" {
18 Some(())
19 } else {
20 None
21 }
22}
23
24pub(crate) fn parse_line_block(
36 window: &StrippedLines<'_, '_>,
37 builder: &mut GreenNodeBuilder<'static>,
38 config: &ParserOptions,
39) -> usize {
40 let lines = window.raw();
41 let start_pos = window.pos();
42 let prefix = window.prefix();
47 let bq_depth = prefix.bq_depth();
48 let list_content_col = prefix.list_content_col();
49 let list_marker_consumed_on_line_0 = prefix.list_marker_consumed_on_line_0;
50 let bq_outer = bq_outer_of_list(prefix);
51 let content_indent = prefix.content_indent();
52
53 log::trace!("Parsing line block at line {}", start_pos + 1);
54
55 builder.start_node(SyntaxKind::LINE_BLOCK.into());
56
57 let mut pos = start_pos;
58 let mut first_line = true;
59
60 while pos < lines.len() {
61 let raw_line = lines[pos];
62
63 let kind = if first_line {
64 LineKind::Marker
67 } else {
68 let peek = window.strip_at(pos);
69 if parse_line_block_line_marker(peek).is_some() {
70 LineKind::Marker
71 } else if peek.starts_with(' ') && !peek.trim_start().starts_with("| ") {
72 LineKind::Continuation
73 } else {
74 break;
75 }
76 };
77
78 builder.start_node(SyntaxKind::LINE_BLOCK_LINE.into());
79
80 let stripped = if first_line {
85 emit_open_line_prefixes(
86 builder,
87 raw_line,
88 bq_depth,
89 list_content_col,
90 list_marker_consumed_on_line_0,
91 bq_outer,
92 content_indent,
93 )
94 } else {
95 window.emit_prefix_at(builder, pos)
96 };
97
98 match kind {
99 LineKind::Marker => {
100 let content_start = parse_line_block_line_marker(stripped)
101 .expect("marker presence verified upstream");
102 builder.token(
103 SyntaxKind::LINE_BLOCK_MARKER.into(),
104 &stripped[..content_start],
105 );
106 let content = &stripped[content_start..];
107 let (content_without_newline, newline_str) = strip_newline(content);
108 if !content_without_newline.is_empty() {
109 inline_emission::emit_inlines(builder, content_without_newline, config, false);
110 }
111 if !newline_str.is_empty() {
112 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
113 }
114 }
115 LineKind::Continuation => {
116 let (line_without_newline, newline_str) = strip_newline(stripped);
117 if !line_without_newline.is_empty() {
118 inline_emission::emit_inlines(builder, line_without_newline, config, false);
119 }
120 if !newline_str.is_empty() {
121 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
122 }
123 }
124 }
125
126 builder.finish_node(); pos += 1;
128 first_line = false;
129 }
130
131 builder.finish_node(); log::trace!("Parsed line block: lines {}-{}", start_pos + 1, pos);
134
135 pos
136}
137
138enum LineKind {
139 Marker,
140 Continuation,
141}
142
143fn emit_open_line_prefixes<'a>(
148 builder: &mut GreenNodeBuilder<'static>,
149 source_line: &'a str,
150 bq_depth: usize,
151 list_content_col: usize,
152 list_marker_consumed_on_line_0: bool,
153 bq_outer: bool,
154 content_indent: usize,
155) -> &'a str {
156 let mut s: &'a str = source_line;
157 let mut pending_ws_start: Option<usize> = None;
158 let suppress_list = list_marker_consumed_on_line_0;
159
160 let flush_ws = |builder: &mut GreenNodeBuilder<'static>,
161 pending: &mut Option<usize>,
162 current_offset: usize| {
163 if let Some(start) = *pending
164 && current_offset > start
165 {
166 builder.token(
167 SyntaxKind::WHITESPACE.into(),
168 &source_line[start..current_offset],
169 );
170 }
171 *pending = None;
172 };
173
174 let do_strip_list = |s: &mut &'a str, pending: &mut Option<usize>| {
175 if list_content_col == 0 {
176 return;
177 }
178 let stripped = if suppress_list {
179 advance_columns(s, list_content_col)
180 } else {
181 strip_list_indent(s, list_content_col)
182 };
183 let consumed = s.len() - stripped.len();
184 if consumed > 0 {
185 let start = source_line.len() - s.len();
186 if !suppress_list && pending.is_none() {
187 *pending = Some(start);
188 }
189 *s = stripped;
190 }
191 };
192
193 let do_strip_bq =
194 |builder: &mut GreenNodeBuilder<'static>, s: &mut &'a str, pending: &mut Option<usize>| {
195 if bq_depth == 0 {
196 return;
197 }
198 let current_offset = source_line.len() - s.len();
199 flush_ws(builder, pending, current_offset);
200 *s = strip_n_blockquote_markers(s, bq_depth);
201 };
202
203 if bq_outer {
204 do_strip_bq(builder, &mut s, &mut pending_ws_start);
205 do_strip_list(&mut s, &mut pending_ws_start);
206 } else {
207 do_strip_list(&mut s, &mut pending_ws_start);
208 do_strip_bq(builder, &mut s, &mut pending_ws_start);
209 }
210
211 if content_indent > 0 {
212 let indent_bytes = byte_index_at_column(s, content_indent);
213 if s.len() >= indent_bytes && indent_bytes > 0 {
214 let start = source_line.len() - s.len();
215 if pending_ws_start.is_none() {
216 pending_ws_start = Some(start);
217 }
218 s = &s[indent_bytes..];
219 }
220 }
221
222 let final_offset = source_line.len() - s.len();
223 flush_ws(builder, &mut pending_ws_start, final_offset);
224 s
225}
226
227fn parse_line_block_line_marker(line: &str) -> Option<usize> {
230 let trimmed_start = line.len() - line.trim_start().len();
233 let after_indent = &line[trimmed_start..];
234
235 if after_indent.starts_with("| ") {
236 Some(trimmed_start + 2) } else if after_indent == "|" || after_indent == "|\n" {
238 Some(trimmed_start + 1) } else {
240 None
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::super::container_prefix::ContainerPrefix;
247 use super::*;
248
249 #[test]
250 fn test_try_parse_line_block_start() {
251 assert!(try_parse_line_block_start("| Some text").is_some());
252 assert!(try_parse_line_block_start("| ").is_some());
253 assert!(try_parse_line_block_start("|").is_some()); assert!(try_parse_line_block_start(" | Some text").is_some());
255
256 assert!(try_parse_line_block_start("|No space").is_none());
258 assert!(try_parse_line_block_start("Regular text").is_none());
259 assert!(try_parse_line_block_start("").is_none());
260 }
261
262 #[test]
263 fn test_parse_line_block_marker() {
264 assert_eq!(parse_line_block_line_marker("| Some text"), Some(2));
265 assert_eq!(parse_line_block_line_marker("| "), Some(2));
266 assert_eq!(parse_line_block_line_marker("|"), Some(1)); assert_eq!(parse_line_block_line_marker(" | Indented"), Some(4));
268
269 assert_eq!(parse_line_block_line_marker("|No space"), None);
271 assert_eq!(parse_line_block_line_marker("Regular"), None);
272 }
273
274 #[test]
275 fn test_simple_line_block() {
276 let input = vec!["| Line one", "| Line two", "| Line three"];
277
278 let mut builder = GreenNodeBuilder::new();
279 let prefix = ContainerPrefix::default();
280 let window = StrippedLines::new(&input, 0, &prefix);
281 let new_pos = parse_line_block(&window, &mut builder, &ParserOptions::default());
282
283 assert_eq!(new_pos, 3);
284 }
285
286 #[test]
287 fn test_line_block_with_continuation() {
288 let input = vec![
289 "| This is a long line",
290 " that continues here",
291 "| Second line",
292 ];
293
294 let mut builder = GreenNodeBuilder::new();
295 let prefix = ContainerPrefix::default();
296 let window = StrippedLines::new(&input, 0, &prefix);
297 let new_pos = parse_line_block(&window, &mut builder, &ParserOptions::default());
298
299 assert_eq!(new_pos, 3);
300 }
301
302 #[test]
303 fn test_line_block_with_indentation() {
304 let input = vec!["| First line", "| Indented line", "| Back to normal"];
305
306 let mut builder = GreenNodeBuilder::new();
307 let prefix = ContainerPrefix::default();
308 let window = StrippedLines::new(&input, 0, &prefix);
309 let new_pos = parse_line_block(&window, &mut builder, &ParserOptions::default());
310
311 assert_eq!(new_pos, 3);
312 }
313
314 #[test]
315 fn test_line_block_stops_at_non_line_block() {
316 let input = vec!["| Line one", "| Line two", "Regular paragraph"];
317
318 let mut builder = GreenNodeBuilder::new();
319 let prefix = ContainerPrefix::default();
320 let window = StrippedLines::new(&input, 0, &prefix);
321 let new_pos = parse_line_block(&window, &mut builder, &ParserOptions::default());
322
323 assert_eq!(new_pos, 2); }
325}