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 fn calculate_indentation_width(indent_str: &str, tab_width: usize) -> usize {
128 let mut width = 0;
129 for ch in indent_str.chars() {
130 if ch == '\t' {
131 width = ((width / tab_width) + 1) * tab_width;
133 } else if ch == ' ' {
134 width += 1;
135 } else {
136 break;
138 }
139 }
140 width
141 }
142
143 fn calculate_indentation_width_default(indent_str: &str) -> usize {
145 Self::calculate_indentation_width(indent_str, 4)
146 }
147
148 pub fn is_in_code_block(&self, line_num: usize) -> bool {
150 if line_num == 0 || line_num > self.code_block_line_map.len() {
151 return false;
152 }
153 self.code_block_line_map[line_num - 1] }
155
156 pub fn is_in_code_span(&self, position: usize) -> bool {
158 self.code_spans
159 .iter()
160 .any(|span| position >= span.start && position < span.end)
161 }
162
163 pub fn is_list_item(&self, line_num: usize) -> bool {
165 if line_num == 0 || line_num > self.list_line_map.len() {
166 return false;
167 }
168 self.list_line_map[line_num - 1] }
170
171 pub fn get_list_item(&self, line_num: usize) -> Option<&ListItem> {
173 self.list_items.iter().find(|item| item.line_number == line_num)
174 }
175
176 pub fn get_list_items(&self) -> &[ListItem] {
178 &self.list_items
179 }
180
181 pub fn get_code_blocks(&self) -> &[CodeBlock] {
183 &self.code_blocks
184 }
185
186 pub fn get_code_spans(&self) -> &[Range] {
188 &self.code_spans
189 }
190
191 fn populate_code_blocks(&mut self, content: &str) {
193 let lines: Vec<&str> = content.lines().collect();
194 let mut in_fenced_block = false;
195 let mut fence_marker = String::new();
196 let mut block_start_line = 0;
197 let mut block_language = String::new();
198
199 for (i, line) in lines.iter().enumerate() {
200 if in_fenced_block {
201 self.code_block_line_map[i] = true;
203
204 if line.trim().starts_with(&fence_marker) {
205 let start_pos =
207 lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
208 let end_pos = lines[0..=i].join("\n").len();
209
210 self.code_blocks.push(CodeBlock {
211 range: Range {
212 start: start_pos,
213 end: end_pos,
214 },
215 block_type: CodeBlockType::Fenced,
216 start_line: block_start_line + 1, end_line: i + 1, language: if !block_language.is_empty() {
219 Some(block_language.clone())
220 } else {
221 None
222 },
223 });
224
225 in_fenced_block = false;
226 fence_marker.clear();
227 block_language.clear();
228 }
229 } else if let Some(caps) = CODE_BLOCK_START_REGEX.captures(line) {
230 fence_marker = caps.get(2).map_or("```", |m| m.as_str()).to_string();
232 in_fenced_block = true;
233 block_start_line = i;
234 block_language = caps.get(3).map_or("", |m| m.as_str().trim()).to_string();
235 self.code_block_line_map[i] = true;
236 } else if INDENTED_CODE_BLOCK_REGEX.is_match(line) {
237 let is_unordered_list = UNORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
239 let is_ordered_list = ORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
240 if !is_unordered_list && !is_ordered_list {
241 self.code_block_line_map[i] = true;
243 let start_pos = lines[0..i].join("\n").len() + if i > 0 { 1 } else { 0 };
247 let end_pos = start_pos + line.len();
248 self.code_blocks.push(CodeBlock {
249 range: Range {
250 start: start_pos,
251 end: end_pos,
252 },
253 block_type: CodeBlockType::Indented,
254 start_line: i + 1, end_line: i + 1, language: None,
257 });
258 }
259 }
260 }
261
262 if in_fenced_block {
264 let start_pos = lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
265 let end_pos = content.len();
266
267 self.code_blocks.push(CodeBlock {
268 range: Range {
269 start: start_pos,
270 end: end_pos,
271 },
272 block_type: CodeBlockType::Fenced,
273 start_line: block_start_line + 1, end_line: lines.len(), language: if !block_language.is_empty() {
276 Some(block_language)
277 } else {
278 None
279 },
280 });
281 }
282 }
283
284 fn populate_code_spans(&mut self, content: &str) {
286 let mut i = 0;
288 while i < content.len() {
289 if let Some(m) = CODE_SPAN_REGEX.find_at(content, i) {
290 let backtick_length = m.end() - m.start();
291 let start = m.start();
292
293 if let Some(end_pos) = content[m.end()..].find(&"`".repeat(backtick_length)) {
295 let end = m.end() + end_pos + backtick_length;
296 self.code_spans.push(Range { start, end });
297 i = end;
298 } else {
299 i = m.end();
300 }
301 } else {
302 break;
303 }
304 }
305 }
306
307 fn populate_list_items(&mut self, content: &str) {
309 let lines: Vec<&str> = content.lines().collect();
310 let mut prev_items: Vec<(usize, usize, usize)> = Vec::new(); for (i, line) in lines.iter().enumerate() {
312 if line.trim().is_empty() {
314 continue;
315 }
316 let (blockquote_depth, blockquote_prefix, rest) = Self::parse_blockquote_prefix(line);
318 if let Some(item) = self.parse_list_item(
320 rest,
321 i + 1,
322 &mut prev_items,
323 blockquote_depth,
324 blockquote_prefix.clone(),
325 ) {
326 self.list_items.push(item);
327 self.list_line_map[i] = true;
328 }
329 }
330 }
331
332 fn parse_blockquote_prefix(line: &str) -> (usize, String, &str) {
334 let mut rest = line;
335 let mut prefix = String::new();
336 let mut depth = 0;
337 loop {
338 let trimmed = rest.trim_start();
339 if let Some(after) = trimmed.strip_prefix('>') {
340 let mut chars = after.chars();
342 let mut space_count = 0;
343 if let Some(' ') = chars.next() {
344 space_count = 1;
345 }
346 let (spaces, after_marker) = after.split_at(space_count);
347 prefix.push('>');
348 prefix.push_str(spaces);
349 rest = after_marker;
350 depth += 1;
351 } else {
352 break;
353 }
354 }
355 (depth, prefix, rest)
356 }
357
358 fn calculate_nesting_level(
360 &self,
361 indent: usize,
362 blockquote_depth: usize,
363 prev_items: &mut Vec<(usize, usize, usize)>,
364 ) -> usize {
365 let mut nesting_level = 0;
366
367 if let Some(&(_last_bq, last_indent, last_level)) =
369 prev_items.iter().rev().find(|(bq, _, _)| *bq == blockquote_depth)
370 {
371 use std::cmp::Ordering;
372 match indent.cmp(&last_indent) {
373 Ordering::Greater => {
374 nesting_level = last_level + 1;
376 }
377 Ordering::Equal => {
378 nesting_level = last_level;
380 }
381 Ordering::Less => {
382 let mut found_level = None;
384
385 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
387 if prev_bq == blockquote_depth && prev_indent == indent {
388 found_level = Some(prev_level);
389 break;
390 }
391 }
392
393 if found_level.is_none() && indent > 0 && last_indent > 0 {
396 let diff = (indent as i32 - last_indent as i32).abs();
398 if diff <= 2 && indent <= 8 && last_indent <= 8 {
399 let has_lower_indent = prev_items.iter().rev().take(3).any(|(bq, prev_indent, _)| {
401 *bq == blockquote_depth && *prev_indent < indent.min(last_indent)
402 });
403 if has_lower_indent {
404 found_level = Some(last_level);
405 }
406 }
407 }
408
409 if found_level.is_none() {
411 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
412 if prev_bq == blockquote_depth && prev_indent < indent {
413 found_level = Some(prev_level);
414 break;
415 }
416 }
417 }
418
419 nesting_level = found_level.unwrap_or(0);
420 }
421 }
422 }
423
424 while let Some(&(prev_bq, prev_indent, _)) = prev_items.last() {
426 if prev_bq != blockquote_depth || prev_indent < indent {
427 break;
428 }
429 prev_items.pop();
430 }
431 prev_items.push((blockquote_depth, indent, nesting_level));
432 nesting_level
433 }
434
435 fn parse_list_item(
437 &self,
438 line: &str,
439 line_num: usize,
440 prev_items: &mut Vec<(usize, usize, usize)>,
441 blockquote_depth: usize,
442 blockquote_prefix: String,
443 ) -> Option<ListItem> {
444 match UNORDERED_LIST_REGEX.captures(line) {
445 Ok(Some(captures)) => {
446 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
447 let indentation = Self::calculate_indentation_width_default(&indent_str);
448 let marker = captures.name("marker").unwrap().as_str();
449 let after = captures.name("after").map_or("", |m| m.as_str());
450 let spaces = after.len();
451 let raw_content = captures.name("content").map_or("", |m| m.as_str());
452 let content = raw_content.trim_start().to_string();
453 let marker_type = match marker {
454 "*" => ListMarkerType::Asterisk,
455 "+" => ListMarkerType::Plus,
456 "-" => ListMarkerType::Minus,
457 other => {
458 eprintln!("Warning: Unexpected list marker '{other}', defaulting to dash");
461 ListMarkerType::Minus
462 }
463 };
464 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
465 let parent_line_number = prev_items
467 .iter()
468 .rev()
469 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
470 .map(|(_, _, line_num)| *line_num);
471 return Some(ListItem {
472 line_number: line_num,
473 indentation,
474 indent_str,
475 marker_type,
476 marker: marker.to_string(),
477 content,
478 spaces_after_marker: spaces,
479 nesting_level,
480 parent_line_number,
481 blockquote_depth,
482 blockquote_prefix,
483 });
484 }
485 Ok(None) => {
486 }
488 Err(_) => {}
489 }
490 match ORDERED_LIST_REGEX.captures(line) {
491 Ok(Some(captures)) => {
492 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
493 let indentation = Self::calculate_indentation_width_default(&indent_str);
494 let marker = captures.name("marker").unwrap().as_str();
495 let spaces = captures.name("after").map_or(0, |m| m.as_str().len());
496 let content = captures
497 .name("content")
498 .map_or("", |m| m.as_str())
499 .trim_start()
500 .to_string();
501 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
502 let parent_line_number = prev_items
504 .iter()
505 .rev()
506 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
507 .map(|(_, _, line_num)| *line_num);
508 return Some(ListItem {
509 line_number: line_num,
510 indentation,
511 indent_str,
512 marker_type: ListMarkerType::Ordered,
513 marker: marker.to_string(),
514 content,
515 spaces_after_marker: spaces,
516 nesting_level,
517 parent_line_number,
518 blockquote_depth,
519 blockquote_prefix,
520 });
521 }
522 Ok(None) => {}
523 Err(_) => {}
524 }
525 None
526 }
527}
528
529static ELEMENT_CACHE: LazyLock<Arc<Mutex<Option<ElementCache>>>> = LazyLock::new(|| Arc::new(Mutex::new(None)));
531
532pub fn get_element_cache(content: &str) -> ElementCache {
537 if let Ok(cache_guard) = ELEMENT_CACHE.lock() {
539 if let Some(existing_cache) = &*cache_guard
541 && existing_cache.is_valid_for(content)
542 {
543 return existing_cache.clone();
544 }
545 }
546
547 let new_cache = ElementCache::new(content);
549
550 if let Ok(mut cache_guard) = ELEMENT_CACHE.lock() {
552 *cache_guard = Some(new_cache.clone());
553 }
554
555 new_cache
556}
557
558pub fn reset_element_cache() {
562 if let Ok(mut cache_guard) = ELEMENT_CACHE.lock() {
563 *cache_guard = None;
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_code_block_detection() {
573 let content = "Regular text\n\n```rust\nfn main() {\n println!(\"Hello\");\n}\n```\n\nMore text";
574 let cache = ElementCache::new(content);
575
576 assert_eq!(cache.code_blocks.len(), 1);
577 assert_eq!(cache.code_blocks[0].start_line, 3);
578 assert_eq!(cache.code_blocks[0].end_line, 7);
579 assert_eq!(cache.code_blocks[0].block_type, CodeBlockType::Fenced);
580 assert_eq!(cache.code_blocks[0].language, Some("rust".to_string()));
581
582 assert!(!cache.is_in_code_block(1));
583 assert!(!cache.is_in_code_block(2));
584 assert!(cache.is_in_code_block(3));
585 assert!(cache.is_in_code_block(4));
586 assert!(cache.is_in_code_block(5));
587 assert!(cache.is_in_code_block(6));
588 assert!(cache.is_in_code_block(7));
589 assert!(!cache.is_in_code_block(8));
590 assert!(!cache.is_in_code_block(9));
591 }
592
593 #[test]
594 fn test_list_item_detection_simple() {
595 let content =
596 "# Heading\n\n- First item\n - Nested item\n- Second item\n\n1. Ordered item\n 1. Nested ordered\n";
597 let cache = ElementCache::new(content);
598 assert_eq!(cache.list_items.len(), 5);
599 assert_eq!(cache.list_items[0].line_number, 3);
601 assert_eq!(cache.list_items[0].marker, "-");
602 assert_eq!(cache.list_items[0].nesting_level, 0);
603 assert_eq!(cache.list_items[1].line_number, 4);
605 assert_eq!(cache.list_items[1].marker, "-");
606 assert_eq!(cache.list_items[1].nesting_level, 1);
607 assert_eq!(cache.list_items[2].line_number, 5);
609 assert_eq!(cache.list_items[2].marker, "-");
610 assert_eq!(cache.list_items[2].nesting_level, 0);
611 assert_eq!(cache.list_items[3].line_number, 7);
613 assert_eq!(cache.list_items[3].marker, "1.");
614 assert_eq!(cache.list_items[3].nesting_level, 0);
615 assert_eq!(cache.list_items[4].line_number, 8);
617 assert_eq!(cache.list_items[4].marker, "1.");
618 assert_eq!(cache.list_items[4].nesting_level, 1);
619 }
620
621 #[test]
622 fn test_list_item_detection_complex() {
623 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";
624 let cache = ElementCache::new(complex);
625
626 assert_eq!(cache.list_items.len(), 10);
628 assert_eq!(cache.list_items[0].marker, "*");
630 assert_eq!(cache.list_items[0].nesting_level, 0);
631 assert_eq!(cache.list_items[1].marker, "-");
632 assert_eq!(cache.list_items[1].nesting_level, 1);
633 assert_eq!(cache.list_items[2].marker, "+");
634 assert_eq!(cache.list_items[2].nesting_level, 2);
635 assert_eq!(cache.list_items[3].marker, "-");
636 assert_eq!(cache.list_items[3].nesting_level, 1);
637 assert_eq!(cache.list_items[4].marker, "*");
638 assert_eq!(cache.list_items[4].nesting_level, 0);
639 assert_eq!(cache.list_items[5].marker, "*");
640 assert_eq!(cache.list_items[5].nesting_level, 0);
641 assert_eq!(cache.list_items[6].marker, "+");
642 assert_eq!(cache.list_items[6].nesting_level, 1);
643 assert_eq!(cache.list_items[7].marker, "-");
644 assert_eq!(cache.list_items[7].nesting_level, 2);
645 assert_eq!(cache.list_items[8].marker, "*");
646 assert_eq!(cache.list_items[8].nesting_level, 3);
647 assert_eq!(cache.list_items[9].marker, "+");
648 assert_eq!(cache.list_items[9].nesting_level, 4);
649 let expected_nesting = vec![0, 1, 2, 1, 0, 0, 1, 2, 3, 4];
650 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
651 assert_eq!(
652 actual_nesting, expected_nesting,
653 "Nesting levels should match expected values"
654 );
655 }
656
657 #[test]
658 fn test_list_item_detection_edge() {
659 let edge = "* Item 1\n\n - Nested 1\n + Nested 2\n\n* Item 2\n";
660 let cache = ElementCache::new(edge);
661 assert_eq!(cache.list_items.len(), 4);
662
663 let expected_nesting = vec![0, 1, 1, 0];
669 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
670 assert_eq!(
671 actual_nesting, expected_nesting,
672 "Nesting levels should be calculated based on indentation, not reset by blank lines"
673 );
674 }
675
676 #[test]
677 fn test_code_span_detection() {
678 let content = "Here is some `inline code` and here are ``nested `code` spans``";
679 let cache = ElementCache::new(content);
680
681 assert_eq!(cache.code_spans.len(), 2);
683
684 let span1_content = &content[cache.code_spans[0].start..cache.code_spans[0].end];
686 assert_eq!(span1_content, "`inline code`");
687
688 let span2_content = &content[cache.code_spans[1].start..cache.code_spans[1].end];
689 assert_eq!(span2_content, "``nested `code` spans``");
690 }
691
692 #[test]
693 fn test_get_element_cache() {
694 let content1 = "Test content";
695 let content2 = "Different content";
696
697 let cache1 = get_element_cache(content1);
699
700 let cache2 = get_element_cache(content1);
702
703 let cache3 = get_element_cache(content2);
705
706 assert!(cache1.is_valid_for(content1));
708 assert!(cache2.is_valid_for(content1));
709 assert!(cache3.is_valid_for(content2));
710
711 assert!(!cache1.is_valid_for(content2));
713 assert!(!cache3.is_valid_for(content1));
714 }
715
716 #[test]
717 fn test_list_item_detection_deep_nesting_and_edge_cases() {
718 let content = "\
720* Level 1
721 - Level 2
722 + Level 3
723 * Level 4
724 - Level 5
725 + Level 6
726* Sibling 1
727 * Sibling 2
728\n - After blank line, not nested\n\n\t* Tab indented\n * 8 spaces indented\n* After excessive indent\n";
729 let cache = ElementCache::new(content);
730 let _expected_markers = ["*", "-", "+", "*", "-", "+", "*", "*", "-", "*", "*", "*"];
732 let _expected_indents = [0, 4, 8, 0, 4, 8, 0, 4, 8, 12, 16, 20];
733 let expected_content = vec![
734 "Level 1",
735 "Level 2",
736 "Level 3",
737 "Level 4",
738 "Level 5",
739 "Level 6",
740 "Sibling 1",
741 "Sibling 2",
742 "After blank line, not nested",
743 "Tab indented", "8 spaces indented", "After excessive indent",
746 ];
747 let actual_content: Vec<_> = cache.list_items.iter().map(|item| item.content.clone()).collect();
748 assert_eq!(
749 actual_content, expected_content,
750 "List item contents should match expected values"
751 );
752 let expected_nesting = vec![0, 1, 2, 3, 4, 5, 0, 1, 1, 1, 2, 0];
755 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
756 assert_eq!(
757 actual_nesting, expected_nesting,
758 "Nesting levels should match expected values"
759 );
760 assert!(
762 cache
763 .list_items
764 .iter()
765 .any(|item| item.marker == "*" && item.indentation >= 1),
766 "Tab or 8-space indented item not detected"
767 );
768 let after_blank = cache
770 .list_items
771 .iter()
772 .find(|item| item.content.contains("After blank line"));
773 assert!(after_blank.is_some());
774 assert_eq!(
775 after_blank.unwrap().nesting_level,
776 1,
777 "Item after blank line should maintain nesting based on indentation"
778 );
779 }
780
781 #[test]
782 fn test_tab_indentation_calculation() {
783 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";
785 let cache = ElementCache::new(content);
786
787 assert_eq!(cache.list_items.len(), 5);
788
789 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);
798 assert_eq!(cache.list_items[1].nesting_level, 1);
799 assert_eq!(cache.list_items[2].nesting_level, 2);
800 assert_eq!(cache.list_items[3].nesting_level, 1);
801 assert_eq!(cache.list_items[4].nesting_level, 2);
802 }
803
804 #[test]
805 fn test_mixed_tabs_and_spaces_indentation() {
806 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";
808
809 reset_element_cache();
811 let cache = ElementCache::new(content);
812
813 assert_eq!(cache.list_items.len(), 4);
814
815 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);
823 assert_eq!(cache.list_items[1].nesting_level, 1);
824 assert_eq!(cache.list_items[2].nesting_level, 1);
825 assert_eq!(cache.list_items[3].nesting_level, 2);
826 }
827
828 #[test]
829 fn test_tab_width_configuration() {
830 let content = "\t* Single tab\n\t\t* Double tab\n";
832 let cache = ElementCache::new(content);
833
834 assert_eq!(cache.list_items.len(), 2);
835
836 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);
842 assert_eq!(cache.list_items[1].nesting_level, 1);
843 }
844
845 #[test]
846 fn test_tab_expansion_debug() {
847 assert_eq!(ElementCache::calculate_indentation_width_default(""), 0);
849 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 1);
850 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 2);
851 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 4);
852 assert_eq!(ElementCache::calculate_indentation_width_default("\t"), 4);
853 assert_eq!(ElementCache::calculate_indentation_width_default("\t\t"), 8);
854 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);
857 }
859
860 #[test]
861 fn test_mixed_tabs_debug() {
862 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";
864 let cache = ElementCache::new(content);
865
866 println!("Number of list items: {}", cache.list_items.len());
867 for (i, item) in cache.list_items.iter().enumerate() {
868 println!(
869 "Item {}: indent_str={:?}, indentation={}, content={:?}",
870 i, item.indent_str, item.indentation, item.content
871 );
872 }
873
874 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);
878 }
880}