Skip to main content

panache_parser/parser/blocks/
line_blocks.rs

1use crate::options::ParserOptions;
2use crate::syntax::SyntaxKind;
3use rowan::GreenNodeBuilder;
4
5use crate::parser::utils::helpers::strip_newline;
6use crate::parser::utils::inline_emission;
7
8/// Try to parse the start of a line block.
9/// Returns Some(()) if this line starts a line block (| followed by space or end of line).
10pub fn try_parse_line_block_start(line: &str) -> Option<()> {
11    let trimmed = line.trim_start();
12    if trimmed.starts_with("| ") || trimmed == "|" {
13        Some(())
14    } else {
15        None
16    }
17}
18
19/// Parse a complete line block starting at current position.
20/// Returns the new position after the line block.
21pub fn parse_line_block(
22    lines: &[&str],
23    start_pos: usize,
24    builder: &mut GreenNodeBuilder<'static>,
25    config: &ParserOptions,
26) -> usize {
27    log::trace!("Parsing line block at line {}", start_pos + 1);
28
29    builder.start_node(SyntaxKind::LINE_BLOCK.into());
30
31    let mut pos = start_pos;
32
33    while pos < lines.len() {
34        let line = lines[pos];
35
36        // Check if this is a line block line (starts with |)
37        if let Some(content_start) = parse_line_block_line_marker(line) {
38            // This is a line block line
39            builder.start_node(SyntaxKind::LINE_BLOCK_LINE.into());
40
41            // Emit the marker
42            builder.token(SyntaxKind::LINE_BLOCK_MARKER.into(), &line[..content_start]);
43
44            // Emit the content (preserving leading spaces)
45            let content = &line[content_start..];
46
47            // Split off trailing newline if present
48            let (content_without_newline, newline_str) = strip_newline(content);
49
50            if !content_without_newline.is_empty() {
51                inline_emission::emit_inlines(builder, content_without_newline, config);
52            }
53
54            if !newline_str.is_empty() {
55                builder.token(SyntaxKind::NEWLINE.into(), newline_str);
56            }
57
58            builder.finish_node(); // LineBlockLine
59            pos += 1;
60
61            // Check for continuation lines (lines that start with space)
62            while pos < lines.len() {
63                let next_line = lines[pos];
64
65                // Continuation line must start with space and not be a new line block line
66                if next_line.starts_with(' ') && !next_line.trim_start().starts_with("| ") {
67                    // This is a continuation of the previous line
68                    builder.start_node(SyntaxKind::LINE_BLOCK_LINE.into());
69
70                    // Split off trailing newline if present
71                    let (line_without_newline, newline_str) = strip_newline(next_line);
72
73                    if !line_without_newline.is_empty() {
74                        inline_emission::emit_inlines(builder, line_without_newline, config);
75                    }
76
77                    if !newline_str.is_empty() {
78                        builder.token(SyntaxKind::NEWLINE.into(), newline_str);
79                    }
80
81                    builder.finish_node(); // LineBlockLine
82                    pos += 1;
83                } else {
84                    break;
85                }
86            }
87        } else {
88            // Not a line block line, end the line block
89            break;
90        }
91    }
92
93    builder.finish_node(); // LineBlock
94
95    log::trace!("Parsed line block: lines {}-{}", start_pos + 1, pos);
96
97    pos
98}
99
100/// Parse a line block marker and return the index where content starts.
101/// Returns Some(index) if the line starts with "| " or just "|", None otherwise.
102fn parse_line_block_line_marker(line: &str) -> Option<usize> {
103    // Line block lines start with | followed by a space or end of line
104    // We need to handle leading whitespace (indentation)
105    let trimmed_start = line.len() - line.trim_start().len();
106    let after_indent = &line[trimmed_start..];
107
108    if after_indent.starts_with("| ") {
109        Some(trimmed_start + 2) // Skip "| "
110    } else if after_indent == "|" || after_indent == "|\n" {
111        Some(trimmed_start + 1) // Just "|", no space
112    } else {
113        None
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_try_parse_line_block_start() {
123        assert!(try_parse_line_block_start("| Some text").is_some());
124        assert!(try_parse_line_block_start("| ").is_some());
125        assert!(try_parse_line_block_start("|").is_some()); // Empty line block
126        assert!(try_parse_line_block_start("  | Some text").is_some());
127
128        // Not line blocks
129        assert!(try_parse_line_block_start("|No space").is_none());
130        assert!(try_parse_line_block_start("Regular text").is_none());
131        assert!(try_parse_line_block_start("").is_none());
132    }
133
134    #[test]
135    fn test_parse_line_block_marker() {
136        assert_eq!(parse_line_block_line_marker("| Some text"), Some(2));
137        assert_eq!(parse_line_block_line_marker("| "), Some(2));
138        assert_eq!(parse_line_block_line_marker("|"), Some(1)); // Empty line block
139        assert_eq!(parse_line_block_line_marker("  | Indented"), Some(4));
140
141        // Not valid
142        assert_eq!(parse_line_block_line_marker("|No space"), None);
143        assert_eq!(parse_line_block_line_marker("Regular"), None);
144    }
145
146    #[test]
147    fn test_simple_line_block() {
148        let input = vec!["| Line one", "| Line two", "| Line three"];
149
150        let mut builder = GreenNodeBuilder::new();
151        let new_pos = parse_line_block(&input, 0, &mut builder, &ParserOptions::default());
152
153        assert_eq!(new_pos, 3);
154    }
155
156    #[test]
157    fn test_line_block_with_continuation() {
158        let input = vec![
159            "| This is a long line",
160            "  that continues here",
161            "| Second line",
162        ];
163
164        let mut builder = GreenNodeBuilder::new();
165        let new_pos = parse_line_block(&input, 0, &mut builder, &ParserOptions::default());
166
167        assert_eq!(new_pos, 3);
168    }
169
170    #[test]
171    fn test_line_block_with_indentation() {
172        let input = vec!["| First line", "|    Indented line", "| Back to normal"];
173
174        let mut builder = GreenNodeBuilder::new();
175        let new_pos = parse_line_block(&input, 0, &mut builder, &ParserOptions::default());
176
177        assert_eq!(new_pos, 3);
178    }
179
180    #[test]
181    fn test_line_block_stops_at_non_line_block() {
182        let input = vec!["| Line one", "| Line two", "Regular paragraph"];
183
184        let mut builder = GreenNodeBuilder::new();
185        let new_pos = parse_line_block(&input, 0, &mut builder, &ParserOptions::default());
186
187        assert_eq!(new_pos, 2); // Should stop before "Regular paragraph"
188    }
189}