rsmarkdown_core/
pattern.rs1use crate::scan::{is_ws, js_trim};
4
5pub 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
23pub 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
40pub fn is_standalone_dash(line: &str) -> bool {
42 js_trim(line) == "-"
43}
44
45pub 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
54pub 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
66pub 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
78pub 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
87pub 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
96pub 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
105pub 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}