Skip to main content

rsmarkdown_core/
pattern.rs

1//! Shared line patterns (ported from `pattern.ts`) as small predicates/helpers.
2
3use crate::scan::{is_ws, js_trim};
4
5/// `/^\|.*\|.*\|/` — line looks like a table row (>= 3 pipes).
6pub fn is_table_row_line(line: &str) -> bool {
7    let line = js_trim(line);
8    if !line.starts_with('|') {
9        return false;
10    }
11    line.matches('|').count() >= 3
12}
13
14fn is_separator_cell(cell: &str) -> bool {
15    let cell = js_trim(cell);
16    if cell.is_empty() {
17        return false;
18    }
19    let stripped = cell.trim_matches(':');
20    stripped.chars().all(|c| c == '-') && stripped.chars().count() >= 3
21}
22
23/// `/^\|[\s:]*-{3,}[\s:]*(?:\|[\s:]*-{3,}[\s:]*)+\|?$/`
24pub fn is_table_separator_line(line: &str) -> bool {
25    let line = js_trim(line);
26    if !line.starts_with('|') {
27        return false;
28    }
29    let rest = &line[1..];
30    let mut cells: Vec<&str> = rest.split('|').collect();
31    if rest.ends_with('|') {
32        cells.pop();
33    }
34    if cells.len() < 2 {
35        return false;
36    }
37    cells.iter().all(|c| is_separator_cell(c))
38}
39
40/// `/^\s*-$/`
41pub fn is_standalone_dash(line: &str) -> bool {
42    js_trim(line) == "-"
43}
44
45/// `/^\s*-\s+$/`
46pub fn is_dash_with_space(line: &str) -> bool {
47    let t = js_trim(line);
48    let Some(rest) = t.strip_prefix('-') else {
49        return false;
50    };
51    !rest.is_empty() && rest.chars().all(is_ws)
52}
53
54/// `/^\s*- \[[x ]\]/i`
55pub fn is_task_list(line: &str) -> bool {
56    let t = js_trim(line);
57    let Some(rest) = t.strip_prefix("- [") else {
58        return false;
59    };
60    let mut chars = rest.chars();
61    let first = chars.next().unwrap_or(' ');
62    let second = chars.next().unwrap_or(' ');
63    (first == 'x' || first == 'X' || first == ' ') && second == ']'
64}
65
66/// `/^\s*-\s*\[\s*$/`
67pub fn is_incomplete_task_list(line: &str) -> bool {
68    let t = js_trim(line);
69    let Some(rest) = t.strip_prefix('-') else {
70        return false;
71    };
72    let Some(rest) = rest.trim_start_matches(is_ws).strip_prefix('[') else {
73        return false;
74    };
75    rest.chars().all(is_ws)
76}
77
78/// `/^>\s*-$/`
79pub fn is_quote_standalone_dash(line: &str) -> bool {
80    let t = js_trim(line);
81    let Some(rest) = t.strip_prefix('>') else {
82        return false;
83    };
84    is_standalone_dash(js_trim(rest))
85}
86
87/// `/^>\s*- \[[x ]\]/i`
88pub fn is_quote_task_list(line: &str) -> bool {
89    let t = js_trim(line);
90    let Some(rest) = t.strip_prefix('>') else {
91        return false;
92    };
93    is_task_list(js_trim(rest))
94}
95
96/// `/^>\s*-\s*\[\s*$/`
97pub fn is_quote_incomplete_task_list(line: &str) -> bool {
98    let t = js_trim(line);
99    let Some(rest) = t.strip_prefix('>') else {
100        return false;
101    };
102    is_incomplete_task_list(js_trim(rest))
103}
104
105/// Remove a trailing standalone `-` line (pattern `/(\n\n?)-[ \t]*$/`):
106/// returns content with that dash line removed, newlines preserved.
107pub fn remove_trailing_standalone_dash(content: &str) -> String {
108    let bytes = content.as_bytes();
109    let mut end = bytes.len();
110    while end > 0 && (bytes[end - 1] == b' ' || bytes[end - 1] == b'\t') {
111        end -= 1;
112    }
113    if end == 0 || bytes[end - 1] != b'-' {
114        return content.to_string();
115    }
116    let dash = end - 1;
117    if dash == 0 || bytes[dash - 1] != b'\n' {
118        return content.to_string();
119    }
120    let newline = dash - 1;
121    let keep_start = if newline > 0 && bytes[newline - 1] == b'\n' {
122        newline - 1
123    } else {
124        newline
125    };
126    content[..keep_start + 1].to_string()
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn separator_lines() {
135        assert!(is_table_separator_line("| --- | --- |"));
136        assert!(is_table_separator_line("| :---: | ---: |"));
137        assert!(is_table_separator_line("| --- | ---"));
138        assert!(!is_table_separator_line("| a | b |"));
139        assert!(!is_table_separator_line("| --- |"));
140        assert!(!is_table_separator_line("| ---"));
141    }
142
143    #[test]
144    fn task_lines() {
145        assert!(is_task_list("- [ ] todo"));
146        assert!(is_task_list("- [x] done"));
147        assert!(is_task_list("- [X] done"));
148        assert!(!is_task_list("- []"));
149        assert!(is_incomplete_task_list("- ["));
150        assert!(is_incomplete_task_list("-[ "));
151    }
152
153    #[test]
154    fn dash_removal() {
155        assert_eq!(
156            remove_trailing_standalone_dash("- [ ] Task 1\n-"),
157            "- [ ] Task 1\n"
158        );
159        assert_eq!(remove_trailing_standalone_dash("a\n\n-"), "a\n");
160        assert_eq!(remove_trailing_standalone_dash("no dash"), "no dash");
161    }
162}