Skip to main content

panache_parser/parser/utils/
helpers.rs

1//! Shared utilities for block parsing.
2
3use crate::syntax::SyntaxKind;
4use rowan::GreenNodeBuilder;
5
6/// Helper to emit a line's text and newline tokens separately.
7/// Lines from split_lines_inclusive contain trailing newlines (LF or CRLF) that must be separated.
8pub(crate) fn emit_line_tokens(builder: &mut GreenNodeBuilder<'static>, line: &str) {
9    // Handle both CRLF and LF line endings
10    if let Some(text) = line.strip_suffix("\r\n") {
11        builder.token(SyntaxKind::TEXT.into(), text);
12        builder.token(SyntaxKind::NEWLINE.into(), "\r\n");
13    } else if let Some(text) = line.strip_suffix('\n') {
14        builder.token(SyntaxKind::TEXT.into(), text);
15        builder.token(SyntaxKind::NEWLINE.into(), "\n");
16    } else {
17        // No trailing newline (last line of input)
18        builder.token(SyntaxKind::TEXT.into(), line);
19    }
20}
21
22/// Strip up to N leading spaces from a line.
23/// This is the generalized version of the previous strip_leading_spaces (which stripped up to 3).
24pub(crate) fn strip_leading_spaces_n(line: &str, max_spaces: usize) -> &str {
25    let spaces_to_strip = line
26        .chars()
27        .take(max_spaces)
28        .take_while(|&c| c == ' ')
29        .count();
30    &line[spaces_to_strip..]
31}
32
33/// Strip up to 3 leading spaces from a line.
34/// This is a convenience wrapper for the common case in Markdown parsing.
35pub(crate) fn strip_leading_spaces(line: &str) -> &str {
36    strip_leading_spaces_n(line, 3)
37}
38
39/// Strip trailing newline (LF or CRLF) from a line, returning the content and the newline string.
40/// Returns (content_without_newline, newline_str).
41pub(crate) fn strip_newline(line: &str) -> (&str, &str) {
42    if let Some(content) = line.strip_suffix("\r\n") {
43        (content, "\r\n")
44    } else if let Some(content) = line.strip_suffix('\n') {
45        (content, "\n")
46    } else {
47        (line, "")
48    }
49}
50
51/// Split input into lines while preserving line endings (LF or CRLF).
52/// This is like split_inclusive but handles both \n and \r\n.
53pub(crate) fn split_lines_inclusive(input: &str) -> Vec<&str> {
54    if input.is_empty() {
55        return vec![];
56    }
57
58    let mut lines = Vec::new();
59    let mut start = 0;
60    let bytes = input.as_bytes();
61    let len = bytes.len();
62
63    let mut i = 0;
64    while i < len {
65        if bytes[i] == b'\n' {
66            // Found LF, include it in the line
67            lines.push(&input[start..=i]);
68            start = i + 1;
69            i += 1;
70        } else if bytes[i] == b'\r' && i + 1 < len && bytes[i + 1] == b'\n' {
71            // Found CRLF, include both in the line
72            lines.push(&input[start..=i + 1]);
73            start = i + 2;
74            i += 2;
75        } else {
76            i += 1;
77        }
78    }
79
80    // Add remaining text if any (last line without newline)
81    if start < len {
82        lines.push(&input[start..]);
83    }
84
85    lines
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_strip_leading_spaces_n() {
94        assert_eq!(strip_leading_spaces_n("   text", 3), "text");
95        assert_eq!(strip_leading_spaces_n("  text", 3), "text");
96        assert_eq!(strip_leading_spaces_n(" text", 3), "text");
97        assert_eq!(strip_leading_spaces_n("text", 3), "text");
98        assert_eq!(strip_leading_spaces_n("    text", 3), " text");
99    }
100
101    #[test]
102    fn test_strip_newline() {
103        assert_eq!(strip_newline("text\n"), ("text", "\n"));
104        assert_eq!(strip_newline("text\r\n"), ("text", "\r\n"));
105        assert_eq!(strip_newline("text"), ("text", ""));
106    }
107}