1use fancy_regex::Regex as FancyRegex;
2use regex::Regex;
3use std::sync::LazyLock;
4
5static UNORDERED_LIST_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)([*+-])(\s+)").unwrap());
7static ORDERED_LIST_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(\d+\.)(\s+)").unwrap());
8
9static UNORDERED_LIST_NO_SPACE_PATTERN: LazyLock<FancyRegex> =
11 LazyLock::new(|| FancyRegex::new(r"^(\s*)(?:(?<!\*)\*(?!\*)|[+-])([^\s\*])").unwrap());
12static ORDERED_LIST_NO_SPACE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(\d+\.)([^\s])").unwrap());
13
14static UNORDERED_LIST_MULTIPLE_SPACE_PATTERN: LazyLock<Regex> =
16 LazyLock::new(|| Regex::new(r"^(\s*)([*+-])(\s{2,})").unwrap());
17static ORDERED_LIST_MULTIPLE_SPACE_PATTERN: LazyLock<Regex> =
18 LazyLock::new(|| Regex::new(r"^(\s*)(\d+\.)(\s{2,})").unwrap());
19
20pub static LIST_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)([-*+]|\d+\.)(\s*)").unwrap());
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum ListMarkerType {
26 Asterisk,
27 Plus,
28 Minus,
29 Ordered,
30}
31
32#[derive(Debug, Clone)]
34pub struct ListItem {
35 pub indentation: usize,
36 pub marker_type: ListMarkerType,
37 pub marker: String,
38 pub content: String,
39 pub spaces_after_marker: usize,
40}
41
42pub struct ListUtils;
44
45impl ListUtils {
46 pub fn calculate_indentation(s: &str) -> usize {
48 s.chars()
49 .take_while(|c| c.is_whitespace())
50 .map(|c| if c == '\t' { 4 } else { 1 })
51 .sum()
52 }
53
54 pub fn is_list_item(line: &str) -> bool {
56 if line.is_empty() {
58 return false;
59 }
60
61 let trimmed = line.trim_start();
62 if trimmed.is_empty() {
63 return false;
64 }
65
66 let Some(first_char) = trimmed.chars().next() else {
68 return false;
69 };
70 match first_char {
71 '*' | '+' | '-' => {
72 if trimmed.len() > 1 {
73 let mut chars = trimmed.chars();
74 chars.next(); if let Some(second_char) = chars.next() {
76 return second_char.is_whitespace();
77 }
78 }
79 false
80 }
81 '0'..='9' => {
82 let dot_pos = trimmed.find('.');
84 if let Some(pos) = dot_pos
85 && pos > 0
86 && pos < trimmed.len() - 1
87 {
88 let after_dot = &trimmed[pos + 1..];
89 return after_dot.starts_with(' ');
90 }
91 false
92 }
93 _ => false,
94 }
95 }
96
97 pub fn is_unordered_list_item(line: &str) -> bool {
99 if line.is_empty() {
101 return false;
102 }
103
104 let trimmed = line.trim_start();
105 if trimmed.is_empty() {
106 return false;
107 }
108
109 let Some(first_char) = trimmed.chars().next() else {
111 return false;
112 };
113 if (first_char == '*' || first_char == '+' || first_char == '-')
114 && trimmed.len() > 1
115 && let Some(second_char) = trimmed.chars().nth(1)
116 {
117 return second_char.is_whitespace();
118 }
119
120 false
121 }
122
123 pub fn is_ordered_list_item(line: &str) -> bool {
125 if line.is_empty() {
127 return false;
128 }
129
130 let trimmed = line.trim_start();
131 if trimmed.is_empty() {
132 return false;
133 }
134
135 let Some(first_char) = trimmed.chars().next() else {
136 return false;
137 };
138
139 if !first_char.is_ascii_digit() {
140 return false;
141 }
142
143 let dot_pos = trimmed.find('.');
145 if let Some(pos) = dot_pos
146 && pos > 0
147 && pos < trimmed.len() - 1
148 {
149 let after_dot = &trimmed[pos + 1..];
150 return after_dot.starts_with(' ');
151 }
152
153 false
154 }
155
156 pub fn is_list_item_without_space(line: &str) -> bool {
158 if line.trim_start().starts_with("**") {
160 return false;
161 }
162
163 if line.trim_start().contains("**") || line.trim_start().contains("__") {
165 return false;
166 }
167
168 if crate::utils::skip_context::is_table_line(line) {
170 return false;
171 }
172
173 let trimmed = line.trim();
175 if !trimmed.is_empty() {
176 if trimmed.chars().all(|c| c == '-' || c.is_whitespace()) {
178 return false;
179 }
180
181 if trimmed.contains('-') && trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace()) {
184 return false;
185 }
186 }
187
188 if line.trim_start().matches('*').count() >= 2 {
190 return false;
191 }
192
193 UNORDERED_LIST_NO_SPACE_PATTERN.is_match(line).unwrap_or(false) || ORDERED_LIST_NO_SPACE_PATTERN.is_match(line)
195 }
196
197 pub fn is_list_item_with_multiple_spaces(line: &str) -> bool {
199 UNORDERED_LIST_MULTIPLE_SPACE_PATTERN.is_match(line) || ORDERED_LIST_MULTIPLE_SPACE_PATTERN.is_match(line)
200 }
201
202 pub fn parse_list_item(line: &str) -> Option<ListItem> {
204 if let Some(captures) = UNORDERED_LIST_PATTERN.captures(line) {
206 let indentation = captures.get(1).map_or(0, |m| Self::calculate_indentation(m.as_str()));
207 let marker = captures.get(2).unwrap().as_str();
208 let spaces = captures.get(3).map_or(0, |m| m.as_str().len());
209 let raw_indentation = captures.get(1).map_or(0, |m| m.as_str().len());
210 let content_start = raw_indentation + marker.len() + spaces;
211 let content = if content_start < line.len() {
212 line[content_start..].to_string()
213 } else {
214 String::new()
215 };
216
217 let marker_type = match marker {
218 "*" => ListMarkerType::Asterisk,
219 "+" => ListMarkerType::Plus,
220 "-" => ListMarkerType::Minus,
221 _ => unreachable!("UNORDERED_LIST_PATTERN regex guarantees marker is [*+-]"),
222 };
223
224 return Some(ListItem {
225 indentation,
226 marker_type,
227 marker: marker.to_string(),
228 content,
229 spaces_after_marker: spaces,
230 });
231 }
232
233 if let Some(captures) = ORDERED_LIST_PATTERN.captures(line) {
235 let indentation = captures.get(1).map_or(0, |m| Self::calculate_indentation(m.as_str()));
236 let marker = captures.get(2).unwrap().as_str();
237 let spaces = captures.get(3).map_or(0, |m| m.as_str().len());
238 let raw_indentation = captures.get(1).map_or(0, |m| m.as_str().len());
239 let content_start = raw_indentation + marker.len() + spaces;
240 let content = if content_start < line.len() {
241 line[content_start..].to_string()
242 } else {
243 String::new()
244 };
245
246 return Some(ListItem {
247 indentation,
248 marker_type: ListMarkerType::Ordered,
249 marker: marker.to_string(),
250 content,
251 spaces_after_marker: spaces,
252 });
253 }
254
255 None
256 }
257
258 pub fn is_list_continuation(line: &str, prev_list_item: &ListItem) -> bool {
260 if line.trim().is_empty() {
261 return false;
262 }
263
264 let indentation = Self::calculate_indentation(line);
266
267 let min_indent = prev_list_item.indentation + prev_list_item.marker.len() + prev_list_item.spaces_after_marker;
269 indentation >= min_indent && !Self::is_list_item(line)
270 }
271
272 pub fn fix_list_item_without_space(line: &str) -> String {
274 if let Ok(Some(captures)) = UNORDERED_LIST_NO_SPACE_PATTERN.captures(line) {
276 let indentation = captures.get(1).map_or("", |m| m.as_str());
277 let marker = captures.get(2).map_or("", |m| m.as_str());
278 let content = captures.get(3).map_or("", |m| m.as_str());
279 return format!("{indentation}{marker} {content}");
280 }
281
282 if let Some(captures) = ORDERED_LIST_NO_SPACE_PATTERN.captures(line) {
284 let indentation = captures.get(1).map_or("", |m| m.as_str());
285 let marker = captures.get(2).map_or("", |m| m.as_str());
286 let content = captures.get(3).map_or("", |m| m.as_str());
287 return format!("{indentation}{marker} {content}");
288 }
289
290 line.to_string()
291 }
292
293 pub fn fix_list_item_with_multiple_spaces(line: &str) -> String {
295 if let Some(captures) = UNORDERED_LIST_MULTIPLE_SPACE_PATTERN.captures(line) {
296 let leading_space = captures.get(1).map_or("", |m| m.as_str());
297 let marker = captures.get(2).map_or("", |m| m.as_str());
298 let spaces = captures.get(3).map_or("", |m| m.as_str());
299
300 let start_pos = leading_space.len() + marker.len() + spaces.len();
302 let content = if start_pos < line.len() { &line[start_pos..] } else { "" };
303
304 return format!("{leading_space}{marker} {content}");
306 }
307
308 if let Some(captures) = ORDERED_LIST_MULTIPLE_SPACE_PATTERN.captures(line) {
309 let leading_space = captures.get(1).map_or("", |m| m.as_str());
310 let marker = captures.get(2).map_or("", |m| m.as_str());
311 let spaces = captures.get(3).map_or("", |m| m.as_str());
312
313 let start_pos = leading_space.len() + marker.len() + spaces.len();
315 let content = if start_pos < line.len() { &line[start_pos..] } else { "" };
316
317 return format!("{leading_space}{marker} {content}");
319 }
320
321 line.to_string()
323 }
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum ListType {
328 Unordered,
329 Ordered,
330}
331
332pub fn is_list_item(line: &str) -> Option<(ListType, String, usize)> {
334 let trimmed_line = line.trim();
335 if trimmed_line.is_empty() {
336 return None;
337 }
338 if trimmed_line.chars().all(|c| c == '-' || c == ' ') && trimmed_line.chars().filter(|&c| c == '-').count() >= 3 {
340 return None;
341 }
342 if trimmed_line.chars().all(|c| c == '*' || c == ' ') && trimmed_line.chars().filter(|&c| c == '*').count() >= 3 {
343 return None;
344 }
345 if let Some(cap) = LIST_REGEX.captures(line) {
346 let marker = &cap[2];
347 let spaces = cap[3].len();
348 let list_type = if marker.chars().next().is_some_and(|c| c.is_ascii_digit()) {
349 ListType::Ordered
350 } else {
351 ListType::Unordered
352 };
353 return Some((list_type, cap[0].to_string(), spaces));
354 }
355 None
356}
357
358pub fn is_multi_line_item(lines: &[&str], current_idx: usize) -> bool {
360 if current_idx >= lines.len() - 1 {
361 return false;
362 }
363 let next_line = lines[current_idx + 1].trim();
364 if next_line.is_empty() {
365 return false;
366 }
367 if is_list_item(next_line).is_some() {
368 return false;
369 }
370 let curr_indent = ListUtils::calculate_indentation(lines[current_idx]);
371 let next_indent = ListUtils::calculate_indentation(lines[current_idx + 1]);
372 next_indent > curr_indent
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn test_is_list_item_without_space() {
381 assert!(!ListUtils::is_list_item_without_space("- Item with space"));
383 assert!(!ListUtils::is_list_item_without_space("* Item with space"));
384 assert!(!ListUtils::is_list_item_without_space("+ Item with space"));
385 assert!(!ListUtils::is_list_item_without_space("1. Item with space"));
386
387 assert!(ListUtils::is_list_item_without_space("-No space"));
389 assert!(ListUtils::is_list_item_without_space("*No space"));
390 assert!(ListUtils::is_list_item_without_space("+No space"));
391 assert!(ListUtils::is_list_item_without_space("1.No space"));
392
393 assert!(!ListUtils::is_list_item_without_space("Regular text"));
395 assert!(!ListUtils::is_list_item_without_space(""));
396 assert!(!ListUtils::is_list_item_without_space(" "));
397 assert!(!ListUtils::is_list_item_without_space("# Heading"));
398
399 assert!(!ListUtils::is_list_item_without_space("**Bold text**"));
401 assert!(!ListUtils::is_list_item_without_space("__Bold text__"));
402 assert!(!ListUtils::is_list_item_without_space("*Italic text*"));
403 assert!(!ListUtils::is_list_item_without_space("_Italic text_"));
404
405 assert!(!ListUtils::is_list_item_without_space("| **Heading** | Content |"));
407 assert!(!ListUtils::is_list_item_without_space("**Bold** | Normal"));
408 assert!(!ListUtils::is_list_item_without_space("| Cell 1 | **Bold** |"));
409
410 assert!(!ListUtils::is_list_item_without_space("---"));
412 assert!(!ListUtils::is_list_item_without_space("----------"));
413 assert!(!ListUtils::is_list_item_without_space(" --- "));
414
415 assert!(!ListUtils::is_list_item_without_space("|--------|---------|"));
417 assert!(!ListUtils::is_list_item_without_space("|:-------|:-------:|"));
418 assert!(!ListUtils::is_list_item_without_space("| ------ | ------- |"));
419 assert!(!ListUtils::is_list_item_without_space("---------|----------|"));
420 assert!(!ListUtils::is_list_item_without_space(":--------|:--------:"));
421 }
422
423 #[test]
424 fn test_is_list_item() {
425 assert!(ListUtils::is_list_item("- Item"));
427 assert!(ListUtils::is_list_item("* Item"));
428 assert!(ListUtils::is_list_item("+ Item"));
429 assert!(ListUtils::is_list_item("1. Item"));
430 assert!(ListUtils::is_list_item(" - Indented item"));
431
432 assert!(!ListUtils::is_list_item("Regular text"));
434 assert!(!ListUtils::is_list_item(""));
435 assert!(!ListUtils::is_list_item(" "));
436 assert!(!ListUtils::is_list_item("# Heading"));
437 assert!(!ListUtils::is_list_item("**Bold text**"));
438 assert!(!ListUtils::is_list_item("| Cell 1 | Cell 2 |"));
439 }
440
441 #[test]
442 fn test_complex_nested_lists() {
443 assert!(ListUtils::is_list_item("- Level 1"));
445 assert!(ListUtils::is_list_item(" - Level 2"));
446 assert!(ListUtils::is_list_item(" - Level 3"));
447 assert!(ListUtils::is_list_item(" - Level 4"));
448 assert!(ListUtils::is_list_item(" - Level 5"));
449
450 assert!(ListUtils::is_list_item("* Main item"));
452 assert!(ListUtils::is_list_item(" - Sub item"));
453 assert!(ListUtils::is_list_item(" + Sub-sub item"));
454 assert!(ListUtils::is_list_item(" * Deep item"));
455
456 assert!(ListUtils::is_list_item("- Unordered"));
458 assert!(ListUtils::is_list_item(" 1. First ordered"));
459 assert!(ListUtils::is_list_item(" 2. Second ordered"));
460 assert!(ListUtils::is_list_item(" - Back to unordered"));
461
462 assert!(ListUtils::is_list_item("\t- Tab indented"));
464 assert!(ListUtils::is_list_item("\t\t- Double tab"));
465 assert!(ListUtils::is_list_item("\t - Tab plus spaces"));
466 assert!(ListUtils::is_list_item(" \t- Spaces plus tab"));
467 }
468
469 #[test]
470 fn test_parse_list_item_edge_cases() {
471 let unicode_item = ListUtils::parse_list_item("- ๆต่ฏ้กน็ฎ ๐").unwrap();
473 assert_eq!(unicode_item.content, "ๆต่ฏ้กน็ฎ ๐");
474
475 let empty_item = ListUtils::parse_list_item("- ").unwrap();
477 assert_eq!(empty_item.content, "");
478
479 let multi_space = ListUtils::parse_list_item("- Multiple spaces").unwrap();
481 assert_eq!(multi_space.spaces_after_marker, 3);
482 assert_eq!(multi_space.content, "Multiple spaces");
483
484 let long_number = ListUtils::parse_list_item("999999. Item").unwrap();
486 assert_eq!(long_number.marker, "999999.");
487 assert_eq!(long_number.marker_type, ListMarkerType::Ordered);
488
489 if let Some(marker_only) = ListUtils::parse_list_item("*") {
491 assert_eq!(marker_only.content, "");
492 assert_eq!(marker_only.spaces_after_marker, 0);
493 }
494 }
495
496 #[test]
497 fn test_nested_list_detection() {
498 let lines = vec![
500 ("- Item 1", 0),
501 (" - Item 1.1", 2),
502 (" - Item 1.1.1", 4),
503 (" - Item 1.1.1.1", 6),
504 (" - Item 1.1.2", 4),
505 (" - Item 1.2", 2),
506 ("- Item 2", 0),
507 ];
508
509 for (line, expected_indent) in lines {
510 let item = ListUtils::parse_list_item(line).unwrap();
511 assert_eq!(item.indentation, expected_indent, "Failed for line: {line}");
512 }
513 }
514
515 #[test]
516 fn test_mixed_list_markers() {
517 let markers = vec![
519 ("* Asterisk", ListMarkerType::Asterisk),
520 ("+ Plus", ListMarkerType::Plus),
521 ("- Minus", ListMarkerType::Minus),
522 ("1. Ordered", ListMarkerType::Ordered),
523 ("42. Ordered", ListMarkerType::Ordered),
524 ];
525
526 for (line, expected_type) in markers {
527 let item = ListUtils::parse_list_item(line).unwrap();
528 assert_eq!(item.marker_type, expected_type, "Failed for line: {line}");
529 }
530 }
531
532 #[test]
533 fn test_list_item_without_space_edge_cases() {
534 assert!(ListUtils::is_list_item_without_space("*a"));
536 assert!(ListUtils::is_list_item_without_space("+b"));
537 assert!(ListUtils::is_list_item_without_space("-c"));
538 assert!(ListUtils::is_list_item_without_space("1.d"));
539
540 assert!(!ListUtils::is_list_item_without_space("*"));
542 assert!(!ListUtils::is_list_item_without_space("+"));
543 assert!(!ListUtils::is_list_item_without_space("-"));
544
545 assert!(!ListUtils::is_list_item_without_space("Text ends with -"));
547 assert!(!ListUtils::is_list_item_without_space("Text ends with *"));
548 assert!(!ListUtils::is_list_item_without_space("Number ends with 1."));
549 }
550
551 #[test]
552 fn test_list_item_with_multiple_spaces() {
553 assert!(ListUtils::is_list_item_with_multiple_spaces("- Two spaces"));
555 assert!(ListUtils::is_list_item_with_multiple_spaces("* Three spaces"));
556 assert!(ListUtils::is_list_item_with_multiple_spaces("+ Four spaces"));
557 assert!(ListUtils::is_list_item_with_multiple_spaces("1. Two spaces"));
558
559 assert!(!ListUtils::is_list_item_with_multiple_spaces("- One space"));
561 assert!(!ListUtils::is_list_item_with_multiple_spaces("* One space"));
562 assert!(!ListUtils::is_list_item_with_multiple_spaces("+ One space"));
563 assert!(!ListUtils::is_list_item_with_multiple_spaces("1. One space"));
564 }
565
566 #[test]
567 fn test_complex_content_in_lists() {
568 let bold_item = ListUtils::parse_list_item("- **Bold** content").unwrap();
570 assert_eq!(bold_item.content, "**Bold** content");
571
572 let link_item = ListUtils::parse_list_item("* [Link](url) in list").unwrap();
573 assert_eq!(link_item.content, "[Link](url) in list");
574
575 let code_item = ListUtils::parse_list_item("+ Item with `code`").unwrap();
576 assert_eq!(code_item.content, "Item with `code`");
577
578 let html_item = ListUtils::parse_list_item("- Item with <span>HTML</span>").unwrap();
580 assert_eq!(html_item.content, "Item with <span>HTML</span>");
581
582 let emoji_item = ListUtils::parse_list_item("1. ๐ Party time!").unwrap();
584 assert_eq!(emoji_item.content, "๐ Party time!");
585 }
586
587 #[test]
588 fn test_ambiguous_list_markers() {
589 assert!(!ListUtils::is_list_item("2 + 2 = 4"));
593 assert!(!ListUtils::is_list_item("5 - 3 = 2"));
594 assert!(!ListUtils::is_list_item("3 * 3 = 9"));
595
596 assert!(!ListUtils::is_list_item("*emphasis*"));
598 assert!(!ListUtils::is_list_item("**strong**"));
599 assert!(!ListUtils::is_list_item("***strong emphasis***"));
600
601 assert!(!ListUtils::is_list_item("2023-01-01 - 2023-12-31"));
603
604 assert!(ListUtils::is_list_item("- 2023-01-01 - 2023-12-31"));
606 assert!(ListUtils::is_list_item("* emphasis text here"));
607 }
608
609 #[test]
610 fn test_deeply_nested_complex_lists() {
611 let complex_doc = vec",
617 " * Different marker",
618 " + Yet another marker",
619 " - Maximum nesting?",
620 " 1. Can we go deeper?",
621 " - Apparently yes!",
622 ];
623
624 for line in complex_doc {
625 assert!(ListUtils::is_list_item(line), "Failed to recognize: {line}");
626 let item = ListUtils::parse_list_item(line).unwrap();
627 assert!(
628 !item.content.is_empty()
629 || line.trim().ends_with('-')
630 || line.trim().ends_with('*')
631 || line.trim().ends_with('+')
632 );
633 }
634 }
635
636 #[test]
637 fn test_parse_list_item_comprehensive() {
638 let test_cases = vec![
640 ("- Simple item", 0, ListMarkerType::Minus, "-", "Simple item"),
641 (" * Indented", 2, ListMarkerType::Asterisk, "*", "Indented"),
642 (" 1. Ordered", 4, ListMarkerType::Ordered, "1.", "Ordered"),
643 ("\t+ Tab indent", 4, ListMarkerType::Plus, "+", "Tab indent"), ];
645
646 for (line, expected_indent, expected_type, expected_marker, expected_content) in test_cases {
647 let item = ListUtils::parse_list_item(line);
648 assert!(item.is_some(), "Failed to parse: {line}");
649 let item = item.unwrap();
650 assert_eq!(item.indentation, expected_indent, "Wrong indentation for: {line}");
651 assert_eq!(item.marker_type, expected_type, "Wrong marker type for: {line}");
652 assert_eq!(item.marker, expected_marker, "Wrong marker for: {line}");
653 assert_eq!(item.content, expected_content, "Wrong content for: {line}");
654 }
655 }
656
657 #[test]
658 fn test_special_characters_in_lists() {
659 let special_cases = vec![
661 "- Item with $ dollar sign",
662 "* Item with ^ caret",
663 "+ Item with \\ backslash",
664 "- Item with | pipe",
665 "1. Item with ( ) parentheses",
666 "2. Item with [ ] brackets",
667 "3. Item with { } braces",
668 ];
669
670 for line in special_cases {
671 assert!(ListUtils::is_list_item(line), "Failed for: {line}");
672 let item = ListUtils::parse_list_item(line);
673 assert!(item.is_some(), "Failed to parse: {line}");
674 }
675 }
676
677 #[test]
678 fn test_list_continuations() {
679 let continuation = "- This is a very long list item that \
681 continues on the next line";
682 assert!(ListUtils::is_list_item(continuation));
683
684 let indented_cont = " - Another long item that \
686 continues with proper indentation";
687 assert!(ListUtils::is_list_item(indented_cont));
688 }
689
690 #[test]
691 fn test_performance_edge_cases() {
692 let long_content = "x".repeat(10000);
694 let long_line = format!("- {long_content}");
695 assert!(ListUtils::is_list_item(&long_line));
696
697 let many_spaces = " ".repeat(100);
699 let spaced_line = format!("{many_spaces}- Item");
700 assert!(ListUtils::is_list_item(&spaced_line));
701
702 let big_number = format!("{}. Item", "9".repeat(20));
704 assert!(ListUtils::is_list_item(&big_number));
705 }
706
707 #[test]
708 fn test_is_unordered_list_item() {
709 assert!(ListUtils::is_unordered_list_item("- Item"));
711 assert!(ListUtils::is_unordered_list_item("* Item"));
712 assert!(ListUtils::is_unordered_list_item("+ Item"));
713
714 assert!(!ListUtils::is_unordered_list_item("1. Item"));
716 assert!(!ListUtils::is_unordered_list_item("99. Item"));
717
718 assert!(!ListUtils::is_unordered_list_item("-Item"));
720 assert!(!ListUtils::is_unordered_list_item("*Item"));
721 assert!(!ListUtils::is_unordered_list_item("+Item"));
722 }
723
724 #[test]
725 fn test_calculate_indentation() {
726 assert_eq!(ListUtils::calculate_indentation(""), 0);
728 assert_eq!(ListUtils::calculate_indentation(" "), 4);
729 assert_eq!(ListUtils::calculate_indentation("\t"), 4);
730 assert_eq!(ListUtils::calculate_indentation("\t\t"), 8);
731 assert_eq!(ListUtils::calculate_indentation(" \t"), 6); assert_eq!(ListUtils::calculate_indentation("\t "), 6); assert_eq!(ListUtils::calculate_indentation("\t\t "), 10); assert_eq!(ListUtils::calculate_indentation(" \t \t"), 12); }
736
737 #[test]
738 fn test_is_ordered_list_item() {
739 assert!(ListUtils::is_ordered_list_item("1. Item"));
741 assert!(ListUtils::is_ordered_list_item("99. Item"));
742 assert!(ListUtils::is_ordered_list_item("1234567890. Item"));
743
744 assert!(!ListUtils::is_ordered_list_item("- Item"));
746 assert!(!ListUtils::is_ordered_list_item("* Item"));
747 assert!(!ListUtils::is_ordered_list_item("+ Item"));
748
749 assert!(!ListUtils::is_ordered_list_item("1.Item"));
751 assert!(!ListUtils::is_ordered_list_item("99.Item"));
752 }
753}