1use fancy_regex::Regex as FancyRegex;
2use lazy_static::lazy_static;
3use regex::Regex;
4use std::sync::{Arc, Mutex};
5
6lazy_static! {
7 static ref CODE_BLOCK_START_REGEX: Regex = Regex::new(r"^(\s*)(```|~~~)(.*)$").unwrap();
9 static ref CODE_BLOCK_END_REGEX: Regex = Regex::new(r"^(\s*)(```|~~~)\s*$").unwrap();
10 static ref INDENTED_CODE_BLOCK_REGEX: Regex = Regex::new(r"^(\s{4,})(.+)$").unwrap();
11
12 static ref UNORDERED_LIST_REGEX: FancyRegex = FancyRegex::new(r"^(?P<indent>[ \t]*)(?P<marker>[*+-])(?P<after>[ \t]*)(?P<content>.*)$").unwrap();
14 static ref ORDERED_LIST_REGEX: FancyRegex = FancyRegex::new(r"^(?P<indent>[ \t]*)(?P<marker>\d+\.)(?P<after>[ \t]*)(?P<content>.*)$").unwrap();
15
16 static ref CODE_SPAN_REGEX: Regex = Regex::new(r"`+").unwrap();
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Range {
23 pub start: usize,
24 pub end: usize,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum CodeBlockType {
30 Fenced,
31 Indented,
32}
33
34#[derive(Debug, Clone)]
36pub struct CodeBlock {
37 pub range: Range,
38 pub block_type: CodeBlockType,
39 pub start_line: usize,
40 pub end_line: usize,
41 pub language: Option<String>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ListMarkerType {
47 Asterisk,
48 Plus,
49 Minus,
50 Ordered,
51}
52
53#[derive(Debug, Clone)]
55pub struct ListItem {
56 pub line_number: usize, pub indentation: usize,
58 pub indent_str: String, pub marker_type: ListMarkerType,
60 pub marker: String,
61 pub content: String,
62 pub spaces_after_marker: usize,
63 pub nesting_level: usize,
64 pub parent_line_number: Option<usize>,
65 pub blockquote_depth: usize, pub blockquote_prefix: String, }
68
69#[derive(Debug, Default, Clone)]
72pub struct ElementCache {
73 content: Option<String>,
75 line_count: usize,
76
77 code_blocks: Vec<CodeBlock>,
79 code_block_line_map: Vec<bool>, code_spans: Vec<Range>,
83
84 list_items: Vec<ListItem>,
86 list_line_map: Vec<bool>, }
88
89impl ElementCache {
90 pub fn new(content: &str) -> Self {
92 let mut cache = ElementCache {
93 content: Some(content.to_string()),
94 line_count: content.lines().count(),
95 code_blocks: Vec::new(),
96 code_block_line_map: Vec::new(),
97 code_spans: Vec::new(),
98 list_items: Vec::new(),
99 list_line_map: Vec::new(),
100 };
101
102 cache.code_block_line_map = vec![false; cache.line_count];
104 cache.list_line_map = vec![false; cache.line_count];
105
106 cache.populate_code_blocks();
108 cache.populate_code_spans();
109 cache.populate_list_items();
110
111 cache
112 }
113
114 fn calculate_indentation_width(indent_str: &str, tab_width: usize) -> usize {
117 let mut width = 0;
118 for ch in indent_str.chars() {
119 if ch == '\t' {
120 width = ((width / tab_width) + 1) * tab_width;
122 } else if ch == ' ' {
123 width += 1;
124 } else {
125 break;
127 }
128 }
129 width
130 }
131
132 fn calculate_indentation_width_default(indent_str: &str) -> usize {
134 Self::calculate_indentation_width(indent_str, 4)
135 }
136
137 pub fn is_in_code_block(&self, line_num: usize) -> bool {
139 if line_num == 0 || line_num > self.code_block_line_map.len() {
140 return false;
141 }
142 self.code_block_line_map[line_num - 1] }
144
145 pub fn is_in_code_span(&self, position: usize) -> bool {
147 self.code_spans
148 .iter()
149 .any(|span| position >= span.start && position < span.end)
150 }
151
152 pub fn is_list_item(&self, line_num: usize) -> bool {
154 if line_num == 0 || line_num > self.list_line_map.len() {
155 return false;
156 }
157 self.list_line_map[line_num - 1] }
159
160 pub fn get_list_item(&self, line_num: usize) -> Option<&ListItem> {
162 self.list_items.iter().find(|item| item.line_number == line_num)
163 }
164
165 pub fn get_list_items(&self) -> &[ListItem] {
167 &self.list_items
168 }
169
170 pub fn get_code_blocks(&self) -> &[CodeBlock] {
172 &self.code_blocks
173 }
174
175 pub fn get_code_spans(&self) -> &[Range] {
177 &self.code_spans
178 }
179
180 fn populate_code_blocks(&mut self) {
182 if let Some(content) = &self.content {
183 let lines: Vec<&str> = content.lines().collect();
184 let mut in_fenced_block = false;
185 let mut fence_marker = String::new();
186 let mut block_start_line = 0;
187 let mut block_language = String::new();
188
189 for (i, line) in lines.iter().enumerate() {
190 if in_fenced_block {
191 self.code_block_line_map[i] = true;
193
194 if line.trim().starts_with(&fence_marker) {
195 let start_pos =
197 lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
198 let end_pos = lines[0..=i].join("\n").len();
199
200 self.code_blocks.push(CodeBlock {
201 range: Range {
202 start: start_pos,
203 end: end_pos,
204 },
205 block_type: CodeBlockType::Fenced,
206 start_line: block_start_line + 1, end_line: i + 1, language: if !block_language.is_empty() {
209 Some(block_language.clone())
210 } else {
211 None
212 },
213 });
214
215 in_fenced_block = false;
216 fence_marker.clear();
217 block_language.clear();
218 }
219 } else if let Some(caps) = CODE_BLOCK_START_REGEX.captures(line) {
220 fence_marker = caps.get(2).map_or("```", |m| m.as_str()).to_string();
222 in_fenced_block = true;
223 block_start_line = i;
224 block_language = caps.get(3).map_or("", |m| m.as_str().trim()).to_string();
225 self.code_block_line_map[i] = true;
226 } else if INDENTED_CODE_BLOCK_REGEX.is_match(line) {
227 let is_unordered_list = UNORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
229 let is_ordered_list = ORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
230 if !is_unordered_list && !is_ordered_list {
231 self.code_block_line_map[i] = true;
233 let start_pos = lines[0..i].join("\n").len() + if i > 0 { 1 } else { 0 };
237 let end_pos = start_pos + line.len();
238 self.code_blocks.push(CodeBlock {
239 range: Range {
240 start: start_pos,
241 end: end_pos,
242 },
243 block_type: CodeBlockType::Indented,
244 start_line: i + 1, end_line: i + 1, language: None,
247 });
248 }
249 }
250 }
251
252 if in_fenced_block {
254 let start_pos = lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
255 let end_pos = content.len();
256
257 self.code_blocks.push(CodeBlock {
258 range: Range {
259 start: start_pos,
260 end: end_pos,
261 },
262 block_type: CodeBlockType::Fenced,
263 start_line: block_start_line + 1, end_line: lines.len(), language: if !block_language.is_empty() {
266 Some(block_language)
267 } else {
268 None
269 },
270 });
271 }
272 }
273 }
274
275 fn populate_code_spans(&mut self) {
277 if let Some(content) = &self.content {
278 let mut i = 0;
280 while i < content.len() {
281 if let Some(m) = CODE_SPAN_REGEX.find_at(content, i) {
282 let backtick_length = m.end() - m.start();
283 let start = m.start();
284
285 if let Some(end_pos) = content[m.end()..].find(&"`".repeat(backtick_length)) {
287 let end = m.end() + end_pos + backtick_length;
288 self.code_spans.push(Range { start, end });
289 i = end;
290 } else {
291 i = m.end();
292 }
293 } else {
294 break;
295 }
296 }
297 }
298 }
299
300 fn populate_list_items(&mut self) {
302 if let Some(content) = &self.content {
303 let lines: Vec<&str> = content.lines().collect();
304 let mut prev_items: Vec<(usize, usize, usize)> = Vec::new(); for (i, line) in lines.iter().enumerate() {
306 if line.trim().is_empty() {
308 continue;
309 }
310 let (blockquote_depth, blockquote_prefix, rest) = Self::parse_blockquote_prefix(line);
312 if let Some(item) = self.parse_list_item(
314 rest,
315 i + 1,
316 &mut prev_items,
317 blockquote_depth,
318 blockquote_prefix.clone(),
319 ) {
320 self.list_items.push(item);
321 self.list_line_map[i] = true;
322 }
323 }
324 }
325 }
326
327 fn parse_blockquote_prefix(line: &str) -> (usize, String, &str) {
329 let mut rest = line;
330 let mut prefix = String::new();
331 let mut depth = 0;
332 loop {
333 let trimmed = rest.trim_start();
334 if let Some(after) = trimmed.strip_prefix('>') {
335 let mut chars = after.chars();
337 let mut space_count = 0;
338 if let Some(' ') = chars.next() {
339 space_count = 1;
340 }
341 let (spaces, after_marker) = after.split_at(space_count);
342 prefix.push('>');
343 prefix.push_str(spaces);
344 rest = after_marker;
345 depth += 1;
346 } else {
347 break;
348 }
349 }
350 (depth, prefix, rest)
351 }
352
353 fn calculate_nesting_level(
355 &self,
356 indent: usize,
357 blockquote_depth: usize,
358 prev_items: &mut Vec<(usize, usize, usize)>,
359 ) -> usize {
360 let mut nesting_level = 0;
361
362 if let Some(&(_last_bq, last_indent, last_level)) =
364 prev_items.iter().rev().find(|(bq, _, _)| *bq == blockquote_depth)
365 {
366 use std::cmp::Ordering;
367 match indent.cmp(&last_indent) {
368 Ordering::Greater => {
369 nesting_level = last_level + 1;
371 }
372 Ordering::Equal => {
373 nesting_level = last_level;
375 }
376 Ordering::Less => {
377 let mut found_level = None;
379
380 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
382 if prev_bq == blockquote_depth && prev_indent == indent {
383 found_level = Some(prev_level);
384 break;
385 }
386 }
387
388 if found_level.is_none() && indent > 0 && last_indent > 0 {
391 let diff = (indent as i32 - last_indent as i32).abs();
393 if diff <= 2 && indent <= 8 && last_indent <= 8 {
394 let has_lower_indent = prev_items.iter().rev().take(3).any(|(bq, prev_indent, _)| {
396 *bq == blockquote_depth && *prev_indent < indent.min(last_indent)
397 });
398 if has_lower_indent {
399 found_level = Some(last_level);
400 }
401 }
402 }
403
404 if found_level.is_none() {
406 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
407 if prev_bq == blockquote_depth && prev_indent < indent {
408 found_level = Some(prev_level);
409 break;
410 }
411 }
412 }
413
414 nesting_level = found_level.unwrap_or(0);
415 }
416 }
417 }
418
419 while let Some(&(prev_bq, prev_indent, _)) = prev_items.last() {
421 if prev_bq != blockquote_depth || prev_indent < indent {
422 break;
423 }
424 prev_items.pop();
425 }
426 prev_items.push((blockquote_depth, indent, nesting_level));
427 nesting_level
428 }
429
430 fn parse_list_item(
432 &self,
433 line: &str,
434 line_num: usize,
435 prev_items: &mut Vec<(usize, usize, usize)>,
436 blockquote_depth: usize,
437 blockquote_prefix: String,
438 ) -> Option<ListItem> {
439 match UNORDERED_LIST_REGEX.captures(line) {
440 Ok(Some(captures)) => {
441 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
442 let indentation = Self::calculate_indentation_width_default(&indent_str);
443 let marker = captures.name("marker").unwrap().as_str();
444 let after = captures.name("after").map_or("", |m| m.as_str());
445 let spaces = after.len();
446 let raw_content = captures.name("content").map_or("", |m| m.as_str());
447 let content = raw_content.trim_start().to_string();
448 let marker_type = match marker {
449 "*" => ListMarkerType::Asterisk,
450 "+" => ListMarkerType::Plus,
451 "-" => ListMarkerType::Minus,
452 _ => unreachable!(),
453 };
454 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
455 let parent_line_number = prev_items
457 .iter()
458 .rev()
459 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
460 .map(|(_, _, line_num)| *line_num);
461 return Some(ListItem {
462 line_number: line_num,
463 indentation,
464 indent_str,
465 marker_type,
466 marker: marker.to_string(),
467 content,
468 spaces_after_marker: spaces,
469 nesting_level,
470 parent_line_number,
471 blockquote_depth,
472 blockquote_prefix,
473 });
474 }
475 Ok(None) => {
476 }
478 Err(_) => {}
479 }
480 match ORDERED_LIST_REGEX.captures(line) {
481 Ok(Some(captures)) => {
482 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
483 let indentation = Self::calculate_indentation_width_default(&indent_str);
484 let marker = captures.name("marker").unwrap().as_str();
485 let spaces = captures.name("after").map_or(0, |m| m.as_str().len());
486 let content = captures
487 .name("content")
488 .map_or("", |m| m.as_str())
489 .trim_start()
490 .to_string();
491 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
492 let parent_line_number = prev_items
494 .iter()
495 .rev()
496 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
497 .map(|(_, _, line_num)| *line_num);
498 return Some(ListItem {
499 line_number: line_num,
500 indentation,
501 indent_str,
502 marker_type: ListMarkerType::Ordered,
503 marker: marker.to_string(),
504 content,
505 spaces_after_marker: spaces,
506 nesting_level,
507 parent_line_number,
508 blockquote_depth,
509 blockquote_prefix,
510 });
511 }
512 Ok(None) => {}
513 Err(_) => {}
514 }
515 None
516 }
517}
518
519lazy_static! {
521 static ref ELEMENT_CACHE: Arc<Mutex<Option<ElementCache>>> = Arc::new(Mutex::new(None));
522}
523
524pub fn get_element_cache(content: &str) -> ElementCache {
526 {
528 let cache_guard = ELEMENT_CACHE.lock().unwrap();
529
530 if let Some(existing_cache) = &*cache_guard
532 && let Some(cached_content) = &existing_cache.content
533 && cached_content == content
534 {
535 return existing_cache.clone(); }
537 }
538
539 let new_cache = ElementCache::new(content);
541
542 {
544 let mut cache_guard = ELEMENT_CACHE.lock().unwrap();
545 *cache_guard = Some(new_cache.clone());
546 }
547
548 new_cache
549}
550
551pub fn reset_element_cache() {
553 let mut cache_guard = ELEMENT_CACHE.lock().unwrap();
554 *cache_guard = None;
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_code_block_detection() {
563 let content = "Regular text\n\n```rust\nfn main() {\n println!(\"Hello\");\n}\n```\n\nMore text";
564 let cache = ElementCache::new(content);
565
566 assert_eq!(cache.code_blocks.len(), 1);
567 assert_eq!(cache.code_blocks[0].start_line, 3);
568 assert_eq!(cache.code_blocks[0].end_line, 7);
569 assert_eq!(cache.code_blocks[0].block_type, CodeBlockType::Fenced);
570 assert_eq!(cache.code_blocks[0].language, Some("rust".to_string()));
571
572 assert!(!cache.is_in_code_block(1));
573 assert!(!cache.is_in_code_block(2));
574 assert!(cache.is_in_code_block(3));
575 assert!(cache.is_in_code_block(4));
576 assert!(cache.is_in_code_block(5));
577 assert!(cache.is_in_code_block(6));
578 assert!(cache.is_in_code_block(7));
579 assert!(!cache.is_in_code_block(8));
580 assert!(!cache.is_in_code_block(9));
581 }
582
583 #[test]
584 fn test_list_item_detection_simple() {
585 let content =
586 "# Heading\n\n- First item\n - Nested item\n- Second item\n\n1. Ordered item\n 1. Nested ordered\n";
587 let cache = ElementCache::new(content);
588 assert_eq!(cache.list_items.len(), 5);
589 assert_eq!(cache.list_items[0].line_number, 3);
591 assert_eq!(cache.list_items[0].marker, "-");
592 assert_eq!(cache.list_items[0].nesting_level, 0);
593 assert_eq!(cache.list_items[1].line_number, 4);
595 assert_eq!(cache.list_items[1].marker, "-");
596 assert_eq!(cache.list_items[1].nesting_level, 1);
597 assert_eq!(cache.list_items[2].line_number, 5);
599 assert_eq!(cache.list_items[2].marker, "-");
600 assert_eq!(cache.list_items[2].nesting_level, 0);
601 assert_eq!(cache.list_items[3].line_number, 7);
603 assert_eq!(cache.list_items[3].marker, "1.");
604 assert_eq!(cache.list_items[3].nesting_level, 0);
605 assert_eq!(cache.list_items[4].line_number, 8);
607 assert_eq!(cache.list_items[4].marker, "1.");
608 assert_eq!(cache.list_items[4].nesting_level, 1);
609 }
610
611 #[test]
612 fn test_list_item_detection_complex() {
613 let complex = " * Level 1 item 1\n - Level 2 item 1\n + Level 3 item 1\n - Level 2 item 2\n * Level 1 item 2\n\n* Top\n + Nested\n - Deep\n * Deeper\n + Deepest\n";
614 let cache = ElementCache::new(complex);
615
616 assert_eq!(cache.list_items.len(), 10);
618 assert_eq!(cache.list_items[0].marker, "*");
620 assert_eq!(cache.list_items[0].nesting_level, 0);
621 assert_eq!(cache.list_items[1].marker, "-");
622 assert_eq!(cache.list_items[1].nesting_level, 1);
623 assert_eq!(cache.list_items[2].marker, "+");
624 assert_eq!(cache.list_items[2].nesting_level, 2);
625 assert_eq!(cache.list_items[3].marker, "-");
626 assert_eq!(cache.list_items[3].nesting_level, 1);
627 assert_eq!(cache.list_items[4].marker, "*");
628 assert_eq!(cache.list_items[4].nesting_level, 0);
629 assert_eq!(cache.list_items[5].marker, "*");
630 assert_eq!(cache.list_items[5].nesting_level, 0);
631 assert_eq!(cache.list_items[6].marker, "+");
632 assert_eq!(cache.list_items[6].nesting_level, 1);
633 assert_eq!(cache.list_items[7].marker, "-");
634 assert_eq!(cache.list_items[7].nesting_level, 2);
635 assert_eq!(cache.list_items[8].marker, "*");
636 assert_eq!(cache.list_items[8].nesting_level, 3);
637 assert_eq!(cache.list_items[9].marker, "+");
638 assert_eq!(cache.list_items[9].nesting_level, 4);
639 let expected_nesting = vec![0, 1, 2, 1, 0, 0, 1, 2, 3, 4];
640 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
641 assert_eq!(
642 actual_nesting, expected_nesting,
643 "Nesting levels should match expected values"
644 );
645 }
646
647 #[test]
648 fn test_list_item_detection_edge() {
649 let edge = "* Item 1\n\n - Nested 1\n + Nested 2\n\n* Item 2\n";
650 let cache = ElementCache::new(edge);
651 assert_eq!(cache.list_items.len(), 4);
652
653 let expected_nesting = vec![0, 1, 1, 0];
659 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
660 assert_eq!(
661 actual_nesting, expected_nesting,
662 "Nesting levels should be calculated based on indentation, not reset by blank lines"
663 );
664 }
665
666 #[test]
667 fn test_code_span_detection() {
668 let content = "Here is some `inline code` and here are ``nested `code` spans``";
669 let cache = ElementCache::new(content);
670
671 assert_eq!(cache.code_spans.len(), 2);
673
674 let span1_content = &content[cache.code_spans[0].start..cache.code_spans[0].end];
676 assert_eq!(span1_content, "`inline code`");
677
678 let span2_content = &content[cache.code_spans[1].start..cache.code_spans[1].end];
679 assert_eq!(span2_content, "``nested `code` spans``");
680 }
681
682 #[test]
683 fn test_get_element_cache() {
684 let content1 = "Test content";
685 let content2 = "Different content";
686
687 let cache1 = get_element_cache(content1);
689
690 let cache2 = get_element_cache(content1);
692
693 let cache3 = get_element_cache(content2);
695
696 assert_eq!(cache1.content.as_ref().unwrap(), content1);
697 assert_eq!(cache2.content.as_ref().unwrap(), content1);
698 assert_eq!(cache3.content.as_ref().unwrap(), content2);
699 }
700
701 #[test]
702 fn test_list_item_detection_deep_nesting_and_edge_cases() {
703 let content = "\
705* Level 1
706 - Level 2
707 + Level 3
708 * Level 4
709 - Level 5
710 + Level 6
711* Sibling 1
712 * Sibling 2
713\n - After blank line, not nested\n\n\t* Tab indented\n * 8 spaces indented\n* After excessive indent\n";
714 let cache = ElementCache::new(content);
715 let _expected_markers = ["*", "-", "+", "*", "-", "+", "*", "*", "-", "*", "*", "*"];
717 let _expected_indents = [0, 4, 8, 0, 4, 8, 0, 4, 8, 12, 16, 20];
718 let expected_content = vec![
719 "Level 1",
720 "Level 2",
721 "Level 3",
722 "Level 4",
723 "Level 5",
724 "Level 6",
725 "Sibling 1",
726 "Sibling 2",
727 "After blank line, not nested",
728 "Tab indented", "8 spaces indented", "After excessive indent",
731 ];
732 let actual_content: Vec<_> = cache.list_items.iter().map(|item| item.content.clone()).collect();
733 assert_eq!(
734 actual_content, expected_content,
735 "List item contents should match expected values"
736 );
737 let expected_nesting = vec![0, 1, 2, 3, 4, 5, 0, 1, 1, 1, 2, 0];
740 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
741 assert_eq!(
742 actual_nesting, expected_nesting,
743 "Nesting levels should match expected values"
744 );
745 assert!(
747 cache
748 .list_items
749 .iter()
750 .any(|item| item.marker == "*" && item.indentation >= 1),
751 "Tab or 8-space indented item not detected"
752 );
753 let after_blank = cache
755 .list_items
756 .iter()
757 .find(|item| item.content.contains("After blank line"));
758 assert!(after_blank.is_some());
759 assert_eq!(
760 after_blank.unwrap().nesting_level,
761 1,
762 "Item after blank line should maintain nesting based on indentation"
763 );
764 }
765
766 #[test]
767 fn test_tab_indentation_calculation() {
768 let content = "* Level 0\n\t* Tab indented (should be level 1)\n\t\t* Double tab (should be level 2)\n * 4 spaces (should be level 1)\n * 8 spaces (should be level 2)\n";
770 let cache = ElementCache::new(content);
771
772 assert_eq!(cache.list_items.len(), 5);
773
774 assert_eq!(cache.list_items[0].indentation, 0); assert_eq!(cache.list_items[1].indentation, 4); assert_eq!(cache.list_items[2].indentation, 8); assert_eq!(cache.list_items[3].indentation, 4); assert_eq!(cache.list_items[4].indentation, 8); assert_eq!(cache.list_items[0].nesting_level, 0);
783 assert_eq!(cache.list_items[1].nesting_level, 1);
784 assert_eq!(cache.list_items[2].nesting_level, 2);
785 assert_eq!(cache.list_items[3].nesting_level, 1);
786 assert_eq!(cache.list_items[4].nesting_level, 2);
787 }
788
789 #[test]
790 fn test_mixed_tabs_and_spaces_indentation() {
791 let content = "* Level 0\n\t * Tab + 2 spaces (should be level 1)\n \t* 2 spaces + tab (should be level 1)\n\t\t * 2 tabs + 2 spaces (should be level 2)\n";
793
794 reset_element_cache();
796 let cache = ElementCache::new(content);
797
798 assert_eq!(cache.list_items.len(), 4);
799
800 assert_eq!(cache.list_items[0].indentation, 0); assert_eq!(cache.list_items[1].indentation, 6); assert_eq!(cache.list_items[2].indentation, 4); assert_eq!(cache.list_items[3].indentation, 10); assert_eq!(cache.list_items[0].nesting_level, 0);
808 assert_eq!(cache.list_items[1].nesting_level, 1);
809 assert_eq!(cache.list_items[2].nesting_level, 1);
810 assert_eq!(cache.list_items[3].nesting_level, 2);
811 }
812
813 #[test]
814 fn test_tab_width_configuration() {
815 let content = "\t* Single tab\n\t\t* Double tab\n";
817 let cache = ElementCache::new(content);
818
819 assert_eq!(cache.list_items.len(), 2);
820
821 assert_eq!(cache.list_items[0].indentation, 4); assert_eq!(cache.list_items[1].indentation, 8); assert_eq!(cache.list_items[0].nesting_level, 0);
827 assert_eq!(cache.list_items[1].nesting_level, 1);
828 }
829
830 #[test]
831 fn test_tab_expansion_debug() {
832 assert_eq!(ElementCache::calculate_indentation_width_default(""), 0);
834 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 1);
835 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 2);
836 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 4);
837 assert_eq!(ElementCache::calculate_indentation_width_default("\t"), 4);
838 assert_eq!(ElementCache::calculate_indentation_width_default("\t\t"), 8);
839 assert_eq!(ElementCache::calculate_indentation_width_default("\t "), 6); assert_eq!(ElementCache::calculate_indentation_width_default(" \t"), 4); assert_eq!(ElementCache::calculate_indentation_width_default("\t\t "), 10);
842 }
844
845 #[test]
846 fn test_mixed_tabs_debug() {
847 let content = "* Level 0\n\t * Tab + 2 spaces (should be level 1)\n \t* 2 spaces + tab (should be level 1)\n\t\t * 2 tabs + 2 spaces (should be level 2)\n";
849 let cache = ElementCache::new(content);
850
851 println!("Number of list items: {}", cache.list_items.len());
852 for (i, item) in cache.list_items.iter().enumerate() {
853 println!(
854 "Item {}: indent_str={:?}, indentation={}, content={:?}",
855 i, item.indent_str, item.indentation, item.content
856 );
857 }
858
859 assert_eq!(ElementCache::calculate_indentation_width_default("\t "), 6); assert_eq!(ElementCache::calculate_indentation_width_default(" \t"), 4); assert_eq!(ElementCache::calculate_indentation_width_default("\t\t "), 10);
863 }
865}