1use fancy_regex::Regex as FancyRegex;
2use regex::Regex;
3use std::hash::{Hash, Hasher};
4use std::sync::LazyLock;
5use std::sync::{Arc, Mutex};
6
7static CODE_BLOCK_START_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(```|~~~)(.*)$").unwrap());
9static INDENTED_CODE_BLOCK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s{4,})(.+)$").unwrap());
10
11static UNORDERED_LIST_REGEX: LazyLock<FancyRegex> = LazyLock::new(|| {
13 FancyRegex::new(r"^(?P<indent>[ \t]*)(?P<marker>[*+-])(?P<after>[ \t]*)(?P<content>.*)$").unwrap()
14});
15static ORDERED_LIST_REGEX: LazyLock<FancyRegex> = LazyLock::new(|| {
16 FancyRegex::new(r"^(?P<indent>[ \t]*)(?P<marker>\d+\.)(?P<after>[ \t]*)(?P<content>.*)$").unwrap()
17});
18
19static CODE_SPAN_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"`+").unwrap());
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Range {
25 pub start: usize,
26 pub end: usize,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CodeBlockType {
32 Fenced,
33 Indented,
34}
35
36#[derive(Debug, Clone)]
38pub struct CodeBlock {
39 pub range: Range,
40 pub block_type: CodeBlockType,
41 pub start_line: usize,
42 pub end_line: usize,
43 pub language: Option<String>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ListMarkerType {
49 Asterisk,
50 Plus,
51 Minus,
52 Ordered,
53}
54
55#[derive(Debug, Clone)]
57pub struct ListItem {
58 pub line_number: usize, pub indentation: usize,
60 pub indent_str: String, pub marker_type: ListMarkerType,
62 pub marker: String,
63 pub content: String,
64 pub spaces_after_marker: usize,
65 pub nesting_level: usize,
66 pub parent_line_number: Option<usize>,
67 pub blockquote_depth: usize, pub blockquote_prefix: String, }
70
71#[derive(Debug, Default, Clone)]
74pub struct ElementCache {
75 content_hash: u64,
77
78 code_blocks: Vec<CodeBlock>,
80 code_block_line_map: Vec<bool>, code_spans: Vec<Range>,
84
85 list_items: Vec<ListItem>,
87 list_line_map: Vec<bool>, }
89
90impl ElementCache {
91 fn compute_content_hash(content: &str) -> u64 {
93 let mut hasher = std::collections::hash_map::DefaultHasher::new();
94 content.hash(&mut hasher);
95 hasher.finish()
96 }
97
98 pub fn new(content: &str) -> Self {
100 let content_hash = Self::compute_content_hash(content);
101 let line_count = content.lines().count();
102
103 let mut cache = ElementCache {
104 content_hash,
105 code_blocks: Vec::new(),
106 code_block_line_map: vec![false; line_count],
107 code_spans: Vec::new(),
108 list_items: Vec::new(),
109 list_line_map: vec![false; line_count],
110 };
111
112 cache.populate_code_blocks(content);
114 cache.populate_code_spans(content);
115 cache.populate_list_items(content);
116
117 cache
118 }
119
120 pub fn is_valid_for(&self, content: &str) -> bool {
122 Self::compute_content_hash(content) == self.content_hash
123 }
124
125 pub fn calculate_indentation_width(indent_str: &str, tab_width: usize) -> usize {
135 let mut width = 0;
136 for ch in indent_str.chars() {
137 if ch == '\t' {
138 width = ((width / tab_width) + 1) * tab_width;
140 } else if ch == ' ' {
141 width += 1;
142 } else {
143 break;
145 }
146 }
147 width
148 }
149
150 pub fn calculate_indentation_width_default(indent_str: &str) -> usize {
152 Self::calculate_indentation_width(indent_str, 4)
153 }
154
155 pub fn is_in_code_block(&self, line_num: usize) -> bool {
157 if line_num == 0 || line_num > self.code_block_line_map.len() {
158 return false;
159 }
160 self.code_block_line_map[line_num - 1] }
162
163 pub fn is_in_code_span(&self, position: usize) -> bool {
165 self.code_spans
166 .iter()
167 .any(|span| position >= span.start && position < span.end)
168 }
169
170 pub fn is_list_item(&self, line_num: usize) -> bool {
172 if line_num == 0 || line_num > self.list_line_map.len() {
173 return false;
174 }
175 self.list_line_map[line_num - 1] }
177
178 pub fn get_list_item(&self, line_num: usize) -> Option<&ListItem> {
180 self.list_items.iter().find(|item| item.line_number == line_num)
181 }
182
183 pub fn get_list_items(&self) -> &[ListItem] {
185 &self.list_items
186 }
187
188 pub fn get_code_blocks(&self) -> &[CodeBlock] {
190 &self.code_blocks
191 }
192
193 pub fn get_code_spans(&self) -> &[Range] {
195 &self.code_spans
196 }
197
198 fn populate_code_blocks(&mut self, content: &str) {
200 let lines: Vec<&str> = content.lines().collect();
201 let mut in_fenced_block = false;
202 let mut fence_marker = String::new();
203 let mut block_start_line = 0;
204 let mut block_language = String::new();
205
206 for (i, line) in lines.iter().enumerate() {
207 if in_fenced_block {
208 self.code_block_line_map[i] = true;
210
211 if line.trim().starts_with(&fence_marker) {
212 let start_pos =
214 lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
215 let end_pos = lines[0..=i].join("\n").len();
216
217 self.code_blocks.push(CodeBlock {
218 range: Range {
219 start: start_pos,
220 end: end_pos,
221 },
222 block_type: CodeBlockType::Fenced,
223 start_line: block_start_line + 1, end_line: i + 1, language: if !block_language.is_empty() {
226 Some(block_language.clone())
227 } else {
228 None
229 },
230 });
231
232 in_fenced_block = false;
233 fence_marker.clear();
234 block_language.clear();
235 }
236 } else if let Some(caps) = CODE_BLOCK_START_REGEX.captures(line) {
237 fence_marker = caps.get(2).map_or("```", |m| m.as_str()).to_string();
239 in_fenced_block = true;
240 block_start_line = i;
241 block_language = caps.get(3).map_or("", |m| m.as_str().trim()).to_string();
242 self.code_block_line_map[i] = true;
243 } else if INDENTED_CODE_BLOCK_REGEX.is_match(line) {
244 let is_unordered_list = UNORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
246 let is_ordered_list = ORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
247 if !is_unordered_list && !is_ordered_list {
248 self.code_block_line_map[i] = true;
250 let start_pos = lines[0..i].join("\n").len() + if i > 0 { 1 } else { 0 };
254 let end_pos = start_pos + line.len();
255 self.code_blocks.push(CodeBlock {
256 range: Range {
257 start: start_pos,
258 end: end_pos,
259 },
260 block_type: CodeBlockType::Indented,
261 start_line: i + 1, end_line: i + 1, language: None,
264 });
265 }
266 }
267 }
268
269 if in_fenced_block {
271 let start_pos = lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
272 let end_pos = content.len();
273
274 self.code_blocks.push(CodeBlock {
275 range: Range {
276 start: start_pos,
277 end: end_pos,
278 },
279 block_type: CodeBlockType::Fenced,
280 start_line: block_start_line + 1, end_line: lines.len(), language: if !block_language.is_empty() {
283 Some(block_language)
284 } else {
285 None
286 },
287 });
288 }
289 }
290
291 fn populate_code_spans(&mut self, content: &str) {
293 let mut i = 0;
295 while i < content.len() {
296 if let Some(m) = CODE_SPAN_REGEX.find_at(content, i) {
297 let backtick_length = m.end() - m.start();
298 let start = m.start();
299
300 if let Some(end_pos) = content[m.end()..].find(&"`".repeat(backtick_length)) {
302 let end = m.end() + end_pos + backtick_length;
303 self.code_spans.push(Range { start, end });
304 i = end;
305 } else {
306 i = m.end();
307 }
308 } else {
309 break;
310 }
311 }
312 }
313
314 fn populate_list_items(&mut self, content: &str) {
316 let lines: Vec<&str> = content.lines().collect();
317 let mut prev_items: Vec<(usize, usize, usize)> = Vec::new(); for (i, line) in lines.iter().enumerate() {
319 if line.trim().is_empty() {
321 continue;
322 }
323 let (blockquote_depth, blockquote_prefix, rest) = Self::parse_blockquote_prefix(line);
325 if let Some(item) = self.parse_list_item(
327 rest,
328 i + 1,
329 &mut prev_items,
330 blockquote_depth,
331 blockquote_prefix.clone(),
332 ) {
333 self.list_items.push(item);
334 self.list_line_map[i] = true;
335 }
336 }
337 }
338
339 fn parse_blockquote_prefix(line: &str) -> (usize, String, &str) {
341 let mut rest = line;
342 let mut prefix = String::new();
343 let mut depth = 0;
344 loop {
345 let trimmed = rest.trim_start();
346 if let Some(after) = trimmed.strip_prefix('>') {
347 let mut chars = after.chars();
349 let mut space_count = 0;
350 if let Some(' ') = chars.next() {
351 space_count = 1;
352 }
353 let (spaces, after_marker) = after.split_at(space_count);
354 prefix.push('>');
355 prefix.push_str(spaces);
356 rest = after_marker;
357 depth += 1;
358 } else {
359 break;
360 }
361 }
362 (depth, prefix, rest)
363 }
364
365 fn calculate_nesting_level(
367 &self,
368 indent: usize,
369 blockquote_depth: usize,
370 prev_items: &mut Vec<(usize, usize, usize)>,
371 ) -> usize {
372 let mut nesting_level = 0;
373
374 if let Some(&(_last_bq, last_indent, last_level)) =
376 prev_items.iter().rev().find(|(bq, _, _)| *bq == blockquote_depth)
377 {
378 use std::cmp::Ordering;
379 match indent.cmp(&last_indent) {
380 Ordering::Greater => {
381 nesting_level = last_level + 1;
383 }
384 Ordering::Equal => {
385 nesting_level = last_level;
387 }
388 Ordering::Less => {
389 let mut found_level = None;
391
392 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
394 if prev_bq == blockquote_depth && prev_indent == indent {
395 found_level = Some(prev_level);
396 break;
397 }
398 }
399
400 if found_level.is_none() && indent > 0 && last_indent > 0 {
403 let diff = (indent as i32 - last_indent as i32).abs();
405 if diff <= 2 && indent <= 8 && last_indent <= 8 {
406 let has_lower_indent = prev_items.iter().rev().take(3).any(|(bq, prev_indent, _)| {
408 *bq == blockquote_depth && *prev_indent < indent.min(last_indent)
409 });
410 if has_lower_indent {
411 found_level = Some(last_level);
412 }
413 }
414 }
415
416 if found_level.is_none() {
418 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
419 if prev_bq == blockquote_depth && prev_indent < indent {
420 found_level = Some(prev_level);
421 break;
422 }
423 }
424 }
425
426 nesting_level = found_level.unwrap_or(0);
427 }
428 }
429 }
430
431 while let Some(&(prev_bq, prev_indent, _)) = prev_items.last() {
433 if prev_bq != blockquote_depth || prev_indent < indent {
434 break;
435 }
436 prev_items.pop();
437 }
438 prev_items.push((blockquote_depth, indent, nesting_level));
439 nesting_level
440 }
441
442 fn parse_list_item(
444 &self,
445 line: &str,
446 line_num: usize,
447 prev_items: &mut Vec<(usize, usize, usize)>,
448 blockquote_depth: usize,
449 blockquote_prefix: String,
450 ) -> Option<ListItem> {
451 match UNORDERED_LIST_REGEX.captures(line) {
452 Ok(Some(captures)) => {
453 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
454 let indentation = Self::calculate_indentation_width_default(&indent_str);
455 let marker = captures.name("marker").unwrap().as_str();
456 let after = captures.name("after").map_or("", |m| m.as_str());
457 let spaces = after.len();
458 let raw_content = captures.name("content").map_or("", |m| m.as_str());
459 let content = raw_content.trim_start().to_string();
460 let marker_type = match marker {
461 "*" => ListMarkerType::Asterisk,
462 "+" => ListMarkerType::Plus,
463 "-" => ListMarkerType::Minus,
464 other => {
465 eprintln!("Warning: Unexpected list marker '{other}', defaulting to dash");
468 ListMarkerType::Minus
469 }
470 };
471 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
472 let parent_line_number = prev_items
474 .iter()
475 .rev()
476 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
477 .map(|(_, _, line_num)| *line_num);
478 return Some(ListItem {
479 line_number: line_num,
480 indentation,
481 indent_str,
482 marker_type,
483 marker: marker.to_string(),
484 content,
485 spaces_after_marker: spaces,
486 nesting_level,
487 parent_line_number,
488 blockquote_depth,
489 blockquote_prefix,
490 });
491 }
492 Ok(None) => {
493 }
495 Err(_) => {}
496 }
497 match ORDERED_LIST_REGEX.captures(line) {
498 Ok(Some(captures)) => {
499 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
500 let indentation = Self::calculate_indentation_width_default(&indent_str);
501 let marker = captures.name("marker").unwrap().as_str();
502 let spaces = captures.name("after").map_or(0, |m| m.as_str().len());
503 let content = captures
504 .name("content")
505 .map_or("", |m| m.as_str())
506 .trim_start()
507 .to_string();
508 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
509 let parent_line_number = prev_items
511 .iter()
512 .rev()
513 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
514 .map(|(_, _, line_num)| *line_num);
515 return Some(ListItem {
516 line_number: line_num,
517 indentation,
518 indent_str,
519 marker_type: ListMarkerType::Ordered,
520 marker: marker.to_string(),
521 content,
522 spaces_after_marker: spaces,
523 nesting_level,
524 parent_line_number,
525 blockquote_depth,
526 blockquote_prefix,
527 });
528 }
529 Ok(None) => {}
530 Err(_) => {}
531 }
532 None
533 }
534}
535
536static ELEMENT_CACHE: LazyLock<Arc<Mutex<Option<ElementCache>>>> = LazyLock::new(|| Arc::new(Mutex::new(None)));
538
539pub fn get_element_cache(content: &str) -> ElementCache {
544 if let Ok(cache_guard) = ELEMENT_CACHE.lock() {
546 if let Some(existing_cache) = &*cache_guard
548 && existing_cache.is_valid_for(content)
549 {
550 return existing_cache.clone();
551 }
552 }
553
554 let new_cache = ElementCache::new(content);
556
557 if let Ok(mut cache_guard) = ELEMENT_CACHE.lock() {
559 *cache_guard = Some(new_cache.clone());
560 }
561
562 new_cache
563}
564
565pub fn reset_element_cache() {
569 if let Ok(mut cache_guard) = ELEMENT_CACHE.lock() {
570 *cache_guard = None;
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 #[test]
579 fn test_code_block_detection() {
580 let content = "Regular text\n\n```rust\nfn main() {\n println!(\"Hello\");\n}\n```\n\nMore text";
581 let cache = ElementCache::new(content);
582
583 assert_eq!(cache.code_blocks.len(), 1);
584 assert_eq!(cache.code_blocks[0].start_line, 3);
585 assert_eq!(cache.code_blocks[0].end_line, 7);
586 assert_eq!(cache.code_blocks[0].block_type, CodeBlockType::Fenced);
587 assert_eq!(cache.code_blocks[0].language, Some("rust".to_string()));
588
589 assert!(!cache.is_in_code_block(1));
590 assert!(!cache.is_in_code_block(2));
591 assert!(cache.is_in_code_block(3));
592 assert!(cache.is_in_code_block(4));
593 assert!(cache.is_in_code_block(5));
594 assert!(cache.is_in_code_block(6));
595 assert!(cache.is_in_code_block(7));
596 assert!(!cache.is_in_code_block(8));
597 assert!(!cache.is_in_code_block(9));
598 }
599
600 #[test]
601 fn test_list_item_detection_simple() {
602 let content =
603 "# Heading\n\n- First item\n - Nested item\n- Second item\n\n1. Ordered item\n 1. Nested ordered\n";
604 let cache = ElementCache::new(content);
605 assert_eq!(cache.list_items.len(), 5);
606 assert_eq!(cache.list_items[0].line_number, 3);
608 assert_eq!(cache.list_items[0].marker, "-");
609 assert_eq!(cache.list_items[0].nesting_level, 0);
610 assert_eq!(cache.list_items[1].line_number, 4);
612 assert_eq!(cache.list_items[1].marker, "-");
613 assert_eq!(cache.list_items[1].nesting_level, 1);
614 assert_eq!(cache.list_items[2].line_number, 5);
616 assert_eq!(cache.list_items[2].marker, "-");
617 assert_eq!(cache.list_items[2].nesting_level, 0);
618 assert_eq!(cache.list_items[3].line_number, 7);
620 assert_eq!(cache.list_items[3].marker, "1.");
621 assert_eq!(cache.list_items[3].nesting_level, 0);
622 assert_eq!(cache.list_items[4].line_number, 8);
624 assert_eq!(cache.list_items[4].marker, "1.");
625 assert_eq!(cache.list_items[4].nesting_level, 1);
626 }
627
628 #[test]
629 fn test_list_item_detection_complex() {
630 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";
631 let cache = ElementCache::new(complex);
632
633 assert_eq!(cache.list_items.len(), 10);
635 assert_eq!(cache.list_items[0].marker, "*");
637 assert_eq!(cache.list_items[0].nesting_level, 0);
638 assert_eq!(cache.list_items[1].marker, "-");
639 assert_eq!(cache.list_items[1].nesting_level, 1);
640 assert_eq!(cache.list_items[2].marker, "+");
641 assert_eq!(cache.list_items[2].nesting_level, 2);
642 assert_eq!(cache.list_items[3].marker, "-");
643 assert_eq!(cache.list_items[3].nesting_level, 1);
644 assert_eq!(cache.list_items[4].marker, "*");
645 assert_eq!(cache.list_items[4].nesting_level, 0);
646 assert_eq!(cache.list_items[5].marker, "*");
647 assert_eq!(cache.list_items[5].nesting_level, 0);
648 assert_eq!(cache.list_items[6].marker, "+");
649 assert_eq!(cache.list_items[6].nesting_level, 1);
650 assert_eq!(cache.list_items[7].marker, "-");
651 assert_eq!(cache.list_items[7].nesting_level, 2);
652 assert_eq!(cache.list_items[8].marker, "*");
653 assert_eq!(cache.list_items[8].nesting_level, 3);
654 assert_eq!(cache.list_items[9].marker, "+");
655 assert_eq!(cache.list_items[9].nesting_level, 4);
656 let expected_nesting = vec![0, 1, 2, 1, 0, 0, 1, 2, 3, 4];
657 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
658 assert_eq!(
659 actual_nesting, expected_nesting,
660 "Nesting levels should match expected values"
661 );
662 }
663
664 #[test]
665 fn test_list_item_detection_edge() {
666 let edge = "* Item 1\n\n - Nested 1\n + Nested 2\n\n* Item 2\n";
667 let cache = ElementCache::new(edge);
668 assert_eq!(cache.list_items.len(), 4);
669
670 let expected_nesting = vec![0, 1, 1, 0];
676 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
677 assert_eq!(
678 actual_nesting, expected_nesting,
679 "Nesting levels should be calculated based on indentation, not reset by blank lines"
680 );
681 }
682
683 #[test]
684 fn test_code_span_detection() {
685 let content = "Here is some `inline code` and here are ``nested `code` spans``";
686 let cache = ElementCache::new(content);
687
688 assert_eq!(cache.code_spans.len(), 2);
690
691 let span1_content = &content[cache.code_spans[0].start..cache.code_spans[0].end];
693 assert_eq!(span1_content, "`inline code`");
694
695 let span2_content = &content[cache.code_spans[1].start..cache.code_spans[1].end];
696 assert_eq!(span2_content, "``nested `code` spans``");
697 }
698
699 #[test]
700 fn test_get_element_cache() {
701 let content1 = "Test content";
702 let content2 = "Different content";
703
704 let cache1 = get_element_cache(content1);
706
707 let cache2 = get_element_cache(content1);
709
710 let cache3 = get_element_cache(content2);
712
713 assert!(cache1.is_valid_for(content1));
715 assert!(cache2.is_valid_for(content1));
716 assert!(cache3.is_valid_for(content2));
717
718 assert!(!cache1.is_valid_for(content2));
720 assert!(!cache3.is_valid_for(content1));
721 }
722
723 #[test]
724 fn test_list_item_detection_deep_nesting_and_edge_cases() {
725 let content = "\
727* Level 1
728 - Level 2
729 + Level 3
730 * Level 4
731 - Level 5
732 + Level 6
733* Sibling 1
734 * Sibling 2
735\n - After blank line, not nested\n\n\t* Tab indented\n * 8 spaces indented\n* After excessive indent\n";
736 let cache = ElementCache::new(content);
737 let _expected_markers = ["*", "-", "+", "*", "-", "+", "*", "*", "-", "*", "*", "*"];
739 let _expected_indents = [0, 4, 8, 0, 4, 8, 0, 4, 8, 12, 16, 20];
740 let expected_content = vec![
741 "Level 1",
742 "Level 2",
743 "Level 3",
744 "Level 4",
745 "Level 5",
746 "Level 6",
747 "Sibling 1",
748 "Sibling 2",
749 "After blank line, not nested",
750 "Tab indented", "8 spaces indented", "After excessive indent",
753 ];
754 let actual_content: Vec<_> = cache.list_items.iter().map(|item| item.content.clone()).collect();
755 assert_eq!(
756 actual_content, expected_content,
757 "List item contents should match expected values"
758 );
759 let expected_nesting = vec![0, 1, 2, 3, 4, 5, 0, 1, 1, 1, 2, 0];
762 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
763 assert_eq!(
764 actual_nesting, expected_nesting,
765 "Nesting levels should match expected values"
766 );
767 assert!(
769 cache
770 .list_items
771 .iter()
772 .any(|item| item.marker == "*" && item.indentation >= 1),
773 "Tab or 8-space indented item not detected"
774 );
775 let after_blank = cache
777 .list_items
778 .iter()
779 .find(|item| item.content.contains("After blank line"));
780 assert!(after_blank.is_some());
781 assert_eq!(
782 after_blank.unwrap().nesting_level,
783 1,
784 "Item after blank line should maintain nesting based on indentation"
785 );
786 }
787
788 #[test]
789 fn test_tab_indentation_calculation() {
790 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";
792 let cache = ElementCache::new(content);
793
794 assert_eq!(cache.list_items.len(), 5);
795
796 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);
805 assert_eq!(cache.list_items[1].nesting_level, 1);
806 assert_eq!(cache.list_items[2].nesting_level, 2);
807 assert_eq!(cache.list_items[3].nesting_level, 1);
808 assert_eq!(cache.list_items[4].nesting_level, 2);
809 }
810
811 #[test]
812 fn test_mixed_tabs_and_spaces_indentation() {
813 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";
815
816 reset_element_cache();
818 let cache = ElementCache::new(content);
819
820 assert_eq!(cache.list_items.len(), 4);
821
822 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);
830 assert_eq!(cache.list_items[1].nesting_level, 1);
831 assert_eq!(cache.list_items[2].nesting_level, 1);
832 assert_eq!(cache.list_items[3].nesting_level, 2);
833 }
834
835 #[test]
836 fn test_tab_width_configuration() {
837 let content = "\t* Single tab\n\t\t* Double tab\n";
839 let cache = ElementCache::new(content);
840
841 assert_eq!(cache.list_items.len(), 2);
842
843 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);
849 assert_eq!(cache.list_items[1].nesting_level, 1);
850 }
851
852 #[test]
853 fn test_tab_expansion_debug() {
854 assert_eq!(ElementCache::calculate_indentation_width_default(""), 0);
856 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 1);
857 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 2);
858 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 4);
859 assert_eq!(ElementCache::calculate_indentation_width_default("\t"), 4);
860 assert_eq!(ElementCache::calculate_indentation_width_default("\t\t"), 8);
861 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);
864 }
866
867 #[test]
868 fn test_mixed_tabs_debug() {
869 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";
871 let cache = ElementCache::new(content);
872
873 println!("Number of list items: {}", cache.list_items.len());
874 for (i, item) in cache.list_items.iter().enumerate() {
875 println!(
876 "Item {}: indent_str={:?}, indentation={}, content={:?}",
877 i, item.indent_str, item.indentation, item.content
878 );
879 }
880
881 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);
885 }
887}