mq_edit/document/
line_analyzer.rs1#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum TableAlignment {
7 Left, Center, Right, None, }
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum LineType {
15 Heading(usize), ListItem,
17 OrderedListItem,
18 TaskListItem(bool), Blockquote,
20 CodeFence(Option<String>), InCode, HorizontalRule,
23 Image(String, String), TableHeader(Vec<String>), TableSeparator(Vec<TableAlignment>), TableRow(Vec<String>), FrontMatterDelimiter, FrontMatterContent, Text,
30}
31
32pub struct LineAnalyzer;
33
34impl LineAnalyzer {
35 pub fn analyze_line(line: &str) -> LineType {
37 let trimmed = line.trim_start();
38
39 if (trimmed == "---" || trimmed == "+++") && line == trimmed {
41 return LineType::FrontMatterDelimiter;
42 }
43
44 if let Some(rest) = trimmed.strip_prefix('#') {
46 let mut level = 1;
47 let mut chars = rest.chars();
48 while let Some('#') = chars.next() {
49 level += 1;
50 if level > 6 {
51 break;
52 }
53 }
54 if level <= 6
55 && rest
56 .chars()
57 .nth(level - 1)
58 .is_some_and(|c| c.is_whitespace())
59 {
60 return LineType::Heading(level);
61 }
62 }
63
64 if trimmed.starts_with("---")
66 || trimmed.starts_with("***")
67 || trimmed.starts_with("___")
68 && trimmed
69 .chars()
70 .all(|c| c == '-' || c == '*' || c == '_' || c.is_whitespace())
71 {
72 return LineType::HorizontalRule;
73 }
74
75 if trimmed.starts_with("```") {
77 let lang = trimmed
78 .strip_prefix("```")
79 .map(|s| s.trim())
80 .filter(|s| !s.is_empty())
81 .map(|s| s.to_string());
82 return LineType::CodeFence(lang);
83 }
84
85 if trimmed.starts_with("- [") {
87 if trimmed.contains("- [x]") || trimmed.contains("- [X]") {
88 return LineType::TaskListItem(true);
89 } else if trimmed.contains("- [ ]") {
90 return LineType::TaskListItem(false);
91 }
92 }
93
94 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
96 return LineType::ListItem;
97 }
98
99 if let Some(ch) = trimmed.chars().next()
101 && ch.is_ascii_digit()
102 {
103 let rest = &trimmed[1..];
104 if rest.starts_with(". ") || rest.starts_with(") ") {
105 return LineType::OrderedListItem;
106 }
107 }
108
109 if trimmed.starts_with("> ") {
111 return LineType::Blockquote;
112 }
113
114 if trimmed.starts_with("
117 {
118 let alt_text = &trimmed[2..alt_end];
119 let rest = &trimmed[alt_end + 2..];
120 if let Some(path_end) = rest.find(')') {
121 let path = &rest[..path_end];
122 return LineType::Image(alt_text.to_string(), path.to_string());
123 }
124 }
125
126 LineType::Text
127 }
128
129 pub fn contains_bold(line: &str) -> bool {
131 line.contains("**") || line.contains("__")
132 }
133
134 pub fn contains_italic(line: &str) -> bool {
136 line.contains('*') || line.contains('_')
137 }
138
139 pub fn contains_strikethrough(line: &str) -> bool {
141 line.contains("~~")
142 }
143
144 pub fn contains_inline_code(line: &str) -> bool {
146 line.contains('`') && !line.trim().starts_with("```")
147 }
148
149 pub fn contains_link(line: &str) -> bool {
151 line.contains('[') && line.contains("](")
152 }
153
154 pub fn is_table_row(line: &str) -> bool {
156 let trimmed = line.trim();
157 trimmed.contains('|') && !trimmed.starts_with("```")
158 }
159
160 pub fn is_table_separator(line: &str) -> bool {
162 let trimmed = line.trim();
163 if !trimmed.contains('|') {
164 return false;
165 }
166 let content: String = trimmed
168 .chars()
169 .filter(|c| *c != '|' && !c.is_whitespace())
170 .collect();
171 !content.is_empty() && content.chars().all(|c| c == '-' || c == ':')
172 }
173
174 pub fn parse_table_cells(line: &str) -> Vec<String> {
176 let trimmed = line.trim();
177 let stripped = trimmed.trim_matches('|');
178 stripped
179 .split('|')
180 .map(|cell| cell.trim().to_string())
181 .collect()
182 }
183
184 pub fn parse_table_alignment(line: &str) -> Vec<TableAlignment> {
186 Self::parse_table_cells(line)
187 .iter()
188 .map(|cell| {
189 let cell = cell.trim();
190 let starts_colon = cell.starts_with(':');
191 let ends_colon = cell.ends_with(':');
192 match (starts_colon, ends_colon) {
193 (true, true) => TableAlignment::Center,
194 (true, false) => TableAlignment::Left,
195 (false, true) => TableAlignment::Right,
196 (false, false) => TableAlignment::None,
197 }
198 })
199 .collect()
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_heading_detection() {
209 assert_eq!(
210 LineAnalyzer::analyze_line("# Heading 1"),
211 LineType::Heading(1)
212 );
213 assert_eq!(
214 LineAnalyzer::analyze_line("## Heading 2"),
215 LineType::Heading(2)
216 );
217 assert_eq!(
218 LineAnalyzer::analyze_line("### Heading 3"),
219 LineType::Heading(3)
220 );
221 }
222
223 #[test]
224 fn test_list_detection() {
225 assert_eq!(LineAnalyzer::analyze_line("- Item"), LineType::ListItem);
226 assert_eq!(LineAnalyzer::analyze_line("* Item"), LineType::ListItem);
227 assert_eq!(
228 LineAnalyzer::analyze_line("1. Item"),
229 LineType::OrderedListItem
230 );
231 }
232
233 #[test]
234 fn test_task_list() {
235 assert_eq!(
236 LineAnalyzer::analyze_line("- [ ] Todo"),
237 LineType::TaskListItem(false)
238 );
239 assert_eq!(
240 LineAnalyzer::analyze_line("- [x] Done"),
241 LineType::TaskListItem(true)
242 );
243 }
244
245 #[test]
246 fn test_blockquote() {
247 assert_eq!(LineAnalyzer::analyze_line("> Quote"), LineType::Blockquote);
248 }
249
250 #[test]
251 fn test_code_fence() {
252 assert_eq!(
253 LineAnalyzer::analyze_line("```rust"),
254 LineType::CodeFence(Some("rust".to_string()))
255 );
256 assert_eq!(LineAnalyzer::analyze_line("```"), LineType::CodeFence(None));
257 }
258
259 #[test]
260 fn test_is_table_row() {
261 assert!(LineAnalyzer::is_table_row("| Name | Age |"));
262 assert!(LineAnalyzer::is_table_row("|Name|Age|"));
263 assert!(LineAnalyzer::is_table_row("| Name | Age | City |"));
264 assert!(!LineAnalyzer::is_table_row("Normal text"));
265 assert!(!LineAnalyzer::is_table_row("```|code|```"));
268 }
269
270 #[test]
271 fn test_is_table_separator() {
272 assert!(LineAnalyzer::is_table_separator("|---|---|"));
273 assert!(LineAnalyzer::is_table_separator("| --- | --- |"));
274 assert!(LineAnalyzer::is_table_separator("| :--- | ---: |"));
275 assert!(LineAnalyzer::is_table_separator("|:---:|:---:|"));
276 assert!(LineAnalyzer::is_table_separator("| :--- | :---: | ---: |"));
277 assert!(!LineAnalyzer::is_table_separator("| Name | Age |"));
278 assert!(!LineAnalyzer::is_table_separator("Normal text"));
279 }
280
281 #[test]
282 fn test_parse_table_cells() {
283 let cells = LineAnalyzer::parse_table_cells("| Name | Age |");
284 assert_eq!(cells, vec!["Name", "Age"]);
285
286 let cells = LineAnalyzer::parse_table_cells("|Name|Age|");
287 assert_eq!(cells, vec!["Name", "Age"]);
288
289 let cells = LineAnalyzer::parse_table_cells("| Name | Age | City |");
290 assert_eq!(cells, vec!["Name", "Age", "City"]);
291
292 let cells = LineAnalyzer::parse_table_cells("| Spaced | Content |");
293 assert_eq!(cells, vec!["Spaced", "Content"]);
294 }
295
296 #[test]
297 fn test_parse_table_alignment() {
298 let alignments = LineAnalyzer::parse_table_alignment("| :--- | ---: | :---: | --- |");
299 assert_eq!(
300 alignments,
301 vec![
302 TableAlignment::Left,
303 TableAlignment::Right,
304 TableAlignment::Center,
305 TableAlignment::None,
306 ]
307 );
308
309 let alignments = LineAnalyzer::parse_table_alignment("|:---|---:|:---:|---|");
310 assert_eq!(
311 alignments,
312 vec![
313 TableAlignment::Left,
314 TableAlignment::Right,
315 TableAlignment::Center,
316 TableAlignment::None,
317 ]
318 );
319 }
320
321 #[test]
322 fn test_front_matter_delimiter() {
323 assert_eq!(
324 LineAnalyzer::analyze_line("---"),
325 LineType::FrontMatterDelimiter
326 );
327 assert_eq!(
328 LineAnalyzer::analyze_line("+++"),
329 LineType::FrontMatterDelimiter
330 );
331 assert_eq!(LineAnalyzer::analyze_line("--- "), LineType::HorizontalRule);
333 assert_eq!(LineAnalyzer::analyze_line(" ---"), LineType::HorizontalRule);
335 }
336}