1use fancy_regex::Regex as FancyRegex;
2use regex::Regex;
3use std::sync::LazyLock;
4use std::sync::{Arc, Mutex};
5
6static CODE_BLOCK_START_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(```|~~~)(.*)$").unwrap());
8static INDENTED_CODE_BLOCK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s{4,})(.+)$").unwrap());
9
10static UNORDERED_LIST_REGEX: LazyLock<FancyRegex> = LazyLock::new(|| {
12 FancyRegex::new(r"^(?P<indent>[ \t]*)(?P<marker>[*+-])(?P<after>[ \t]*)(?P<content>.*)$").unwrap()
13});
14static ORDERED_LIST_REGEX: LazyLock<FancyRegex> = LazyLock::new(|| {
15 FancyRegex::new(r"^(?P<indent>[ \t]*)(?P<marker>\d+\.)(?P<after>[ \t]*)(?P<content>.*)$").unwrap()
16});
17
18static CODE_SPAN_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"`+").unwrap());
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Range {
24 pub start: usize,
25 pub end: usize,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum CodeBlockType {
31 Fenced,
32 Indented,
33}
34
35#[derive(Debug, Clone)]
37pub struct CodeBlock {
38 pub range: Range,
39 pub block_type: CodeBlockType,
40 pub start_line: usize,
41 pub end_line: usize,
42 pub language: Option<String>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ListMarkerType {
48 Asterisk,
49 Plus,
50 Minus,
51 Ordered,
52}
53
54#[derive(Debug, Clone)]
56pub struct ListItem {
57 pub line_number: usize, pub indentation: usize,
59 pub indent_str: String, pub marker_type: ListMarkerType,
61 pub marker: String,
62 pub content: String,
63 pub spaces_after_marker: usize,
64 pub nesting_level: usize,
65 pub parent_line_number: Option<usize>,
66 pub blockquote_depth: usize, pub blockquote_prefix: String, }
69
70#[derive(Debug, Default, Clone)]
73pub struct ElementCache {
74 content: Option<String>,
76 line_count: usize,
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 pub fn new(content: &str) -> Self {
93 let mut cache = ElementCache {
94 content: Some(content.to_string()),
95 line_count: content.lines().count(),
96 code_blocks: Vec::new(),
97 code_block_line_map: Vec::new(),
98 code_spans: Vec::new(),
99 list_items: Vec::new(),
100 list_line_map: Vec::new(),
101 };
102
103 cache.code_block_line_map = vec![false; cache.line_count];
105 cache.list_line_map = vec![false; cache.line_count];
106
107 cache.populate_code_blocks();
109 cache.populate_code_spans();
110 cache.populate_list_items();
111
112 cache
113 }
114
115 fn calculate_indentation_width(indent_str: &str, tab_width: usize) -> usize {
118 let mut width = 0;
119 for ch in indent_str.chars() {
120 if ch == '\t' {
121 width = ((width / tab_width) + 1) * tab_width;
123 } else if ch == ' ' {
124 width += 1;
125 } else {
126 break;
128 }
129 }
130 width
131 }
132
133 fn calculate_indentation_width_default(indent_str: &str) -> usize {
135 Self::calculate_indentation_width(indent_str, 4)
136 }
137
138 pub fn is_in_code_block(&self, line_num: usize) -> bool {
140 if line_num == 0 || line_num > self.code_block_line_map.len() {
141 return false;
142 }
143 self.code_block_line_map[line_num - 1] }
145
146 pub fn is_in_code_span(&self, position: usize) -> bool {
148 self.code_spans
149 .iter()
150 .any(|span| position >= span.start && position < span.end)
151 }
152
153 pub fn is_list_item(&self, line_num: usize) -> bool {
155 if line_num == 0 || line_num > self.list_line_map.len() {
156 return false;
157 }
158 self.list_line_map[line_num - 1] }
160
161 pub fn get_list_item(&self, line_num: usize) -> Option<&ListItem> {
163 self.list_items.iter().find(|item| item.line_number == line_num)
164 }
165
166 pub fn get_list_items(&self) -> &[ListItem] {
168 &self.list_items
169 }
170
171 pub fn get_code_blocks(&self) -> &[CodeBlock] {
173 &self.code_blocks
174 }
175
176 pub fn get_code_spans(&self) -> &[Range] {
178 &self.code_spans
179 }
180
181 fn populate_code_blocks(&mut self) {
183 if let Some(content) = &self.content {
184 let lines: Vec<&str> = content.lines().collect();
185 let mut in_fenced_block = false;
186 let mut fence_marker = String::new();
187 let mut block_start_line = 0;
188 let mut block_language = String::new();
189
190 for (i, line) in lines.iter().enumerate() {
191 if in_fenced_block {
192 self.code_block_line_map[i] = true;
194
195 if line.trim().starts_with(&fence_marker) {
196 let start_pos =
198 lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
199 let end_pos = lines[0..=i].join("\n").len();
200
201 self.code_blocks.push(CodeBlock {
202 range: Range {
203 start: start_pos,
204 end: end_pos,
205 },
206 block_type: CodeBlockType::Fenced,
207 start_line: block_start_line + 1, end_line: i + 1, language: if !block_language.is_empty() {
210 Some(block_language.clone())
211 } else {
212 None
213 },
214 });
215
216 in_fenced_block = false;
217 fence_marker.clear();
218 block_language.clear();
219 }
220 } else if let Some(caps) = CODE_BLOCK_START_REGEX.captures(line) {
221 fence_marker = caps.get(2).map_or("```", |m| m.as_str()).to_string();
223 in_fenced_block = true;
224 block_start_line = i;
225 block_language = caps.get(3).map_or("", |m| m.as_str().trim()).to_string();
226 self.code_block_line_map[i] = true;
227 } else if INDENTED_CODE_BLOCK_REGEX.is_match(line) {
228 let is_unordered_list = UNORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
230 let is_ordered_list = ORDERED_LIST_REGEX.is_match(line).unwrap_or(false);
231 if !is_unordered_list && !is_ordered_list {
232 self.code_block_line_map[i] = true;
234 let start_pos = lines[0..i].join("\n").len() + if i > 0 { 1 } else { 0 };
238 let end_pos = start_pos + line.len();
239 self.code_blocks.push(CodeBlock {
240 range: Range {
241 start: start_pos,
242 end: end_pos,
243 },
244 block_type: CodeBlockType::Indented,
245 start_line: i + 1, end_line: i + 1, language: None,
248 });
249 }
250 }
251 }
252
253 if in_fenced_block {
255 let start_pos = lines[0..block_start_line].join("\n").len() + if block_start_line > 0 { 1 } else { 0 };
256 let end_pos = content.len();
257
258 self.code_blocks.push(CodeBlock {
259 range: Range {
260 start: start_pos,
261 end: end_pos,
262 },
263 block_type: CodeBlockType::Fenced,
264 start_line: block_start_line + 1, end_line: lines.len(), language: if !block_language.is_empty() {
267 Some(block_language)
268 } else {
269 None
270 },
271 });
272 }
273 }
274 }
275
276 fn populate_code_spans(&mut self) {
278 if let Some(content) = &self.content {
279 let mut i = 0;
281 while i < content.len() {
282 if let Some(m) = CODE_SPAN_REGEX.find_at(content, i) {
283 let backtick_length = m.end() - m.start();
284 let start = m.start();
285
286 if let Some(end_pos) = content[m.end()..].find(&"`".repeat(backtick_length)) {
288 let end = m.end() + end_pos + backtick_length;
289 self.code_spans.push(Range { start, end });
290 i = end;
291 } else {
292 i = m.end();
293 }
294 } else {
295 break;
296 }
297 }
298 }
299 }
300
301 fn populate_list_items(&mut self) {
303 if let Some(content) = &self.content {
304 let lines: Vec<&str> = content.lines().collect();
305 let mut prev_items: Vec<(usize, usize, usize)> = Vec::new(); for (i, line) in lines.iter().enumerate() {
307 if line.trim().is_empty() {
309 continue;
310 }
311 let (blockquote_depth, blockquote_prefix, rest) = Self::parse_blockquote_prefix(line);
313 if let Some(item) = self.parse_list_item(
315 rest,
316 i + 1,
317 &mut prev_items,
318 blockquote_depth,
319 blockquote_prefix.clone(),
320 ) {
321 self.list_items.push(item);
322 self.list_line_map[i] = true;
323 }
324 }
325 }
326 }
327
328 fn parse_blockquote_prefix(line: &str) -> (usize, String, &str) {
330 let mut rest = line;
331 let mut prefix = String::new();
332 let mut depth = 0;
333 loop {
334 let trimmed = rest.trim_start();
335 if let Some(after) = trimmed.strip_prefix('>') {
336 let mut chars = after.chars();
338 let mut space_count = 0;
339 if let Some(' ') = chars.next() {
340 space_count = 1;
341 }
342 let (spaces, after_marker) = after.split_at(space_count);
343 prefix.push('>');
344 prefix.push_str(spaces);
345 rest = after_marker;
346 depth += 1;
347 } else {
348 break;
349 }
350 }
351 (depth, prefix, rest)
352 }
353
354 fn calculate_nesting_level(
356 &self,
357 indent: usize,
358 blockquote_depth: usize,
359 prev_items: &mut Vec<(usize, usize, usize)>,
360 ) -> usize {
361 let mut nesting_level = 0;
362
363 if let Some(&(_last_bq, last_indent, last_level)) =
365 prev_items.iter().rev().find(|(bq, _, _)| *bq == blockquote_depth)
366 {
367 use std::cmp::Ordering;
368 match indent.cmp(&last_indent) {
369 Ordering::Greater => {
370 nesting_level = last_level + 1;
372 }
373 Ordering::Equal => {
374 nesting_level = last_level;
376 }
377 Ordering::Less => {
378 let mut found_level = None;
380
381 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
383 if prev_bq == blockquote_depth && prev_indent == indent {
384 found_level = Some(prev_level);
385 break;
386 }
387 }
388
389 if found_level.is_none() && indent > 0 && last_indent > 0 {
392 let diff = (indent as i32 - last_indent as i32).abs();
394 if diff <= 2 && indent <= 8 && last_indent <= 8 {
395 let has_lower_indent = prev_items.iter().rev().take(3).any(|(bq, prev_indent, _)| {
397 *bq == blockquote_depth && *prev_indent < indent.min(last_indent)
398 });
399 if has_lower_indent {
400 found_level = Some(last_level);
401 }
402 }
403 }
404
405 if found_level.is_none() {
407 for &(prev_bq, prev_indent, prev_level) in prev_items.iter().rev() {
408 if prev_bq == blockquote_depth && prev_indent < indent {
409 found_level = Some(prev_level);
410 break;
411 }
412 }
413 }
414
415 nesting_level = found_level.unwrap_or(0);
416 }
417 }
418 }
419
420 while let Some(&(prev_bq, prev_indent, _)) = prev_items.last() {
422 if prev_bq != blockquote_depth || prev_indent < indent {
423 break;
424 }
425 prev_items.pop();
426 }
427 prev_items.push((blockquote_depth, indent, nesting_level));
428 nesting_level
429 }
430
431 fn parse_list_item(
433 &self,
434 line: &str,
435 line_num: usize,
436 prev_items: &mut Vec<(usize, usize, usize)>,
437 blockquote_depth: usize,
438 blockquote_prefix: String,
439 ) -> Option<ListItem> {
440 match UNORDERED_LIST_REGEX.captures(line) {
441 Ok(Some(captures)) => {
442 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
443 let indentation = Self::calculate_indentation_width_default(&indent_str);
444 let marker = captures.name("marker").unwrap().as_str();
445 let after = captures.name("after").map_or("", |m| m.as_str());
446 let spaces = after.len();
447 let raw_content = captures.name("content").map_or("", |m| m.as_str());
448 let content = raw_content.trim_start().to_string();
449 let marker_type = match marker {
450 "*" => ListMarkerType::Asterisk,
451 "+" => ListMarkerType::Plus,
452 "-" => ListMarkerType::Minus,
453 other => {
454 eprintln!("Warning: Unexpected list marker '{other}', defaulting to dash");
457 ListMarkerType::Minus
458 }
459 };
460 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
461 let parent_line_number = prev_items
463 .iter()
464 .rev()
465 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
466 .map(|(_, _, line_num)| *line_num);
467 return Some(ListItem {
468 line_number: line_num,
469 indentation,
470 indent_str,
471 marker_type,
472 marker: marker.to_string(),
473 content,
474 spaces_after_marker: spaces,
475 nesting_level,
476 parent_line_number,
477 blockquote_depth,
478 blockquote_prefix,
479 });
480 }
481 Ok(None) => {
482 }
484 Err(_) => {}
485 }
486 match ORDERED_LIST_REGEX.captures(line) {
487 Ok(Some(captures)) => {
488 let indent_str = captures.name("indent").map_or("", |m| m.as_str()).to_string();
489 let indentation = Self::calculate_indentation_width_default(&indent_str);
490 let marker = captures.name("marker").unwrap().as_str();
491 let spaces = captures.name("after").map_or(0, |m| m.as_str().len());
492 let content = captures
493 .name("content")
494 .map_or("", |m| m.as_str())
495 .trim_start()
496 .to_string();
497 let nesting_level = self.calculate_nesting_level(indentation, blockquote_depth, prev_items);
498 let parent_line_number = prev_items
500 .iter()
501 .rev()
502 .find(|(bq, _, level)| *bq == blockquote_depth && *level < nesting_level)
503 .map(|(_, _, line_num)| *line_num);
504 return Some(ListItem {
505 line_number: line_num,
506 indentation,
507 indent_str,
508 marker_type: ListMarkerType::Ordered,
509 marker: marker.to_string(),
510 content,
511 spaces_after_marker: spaces,
512 nesting_level,
513 parent_line_number,
514 blockquote_depth,
515 blockquote_prefix,
516 });
517 }
518 Ok(None) => {}
519 Err(_) => {}
520 }
521 None
522 }
523}
524
525static ELEMENT_CACHE: LazyLock<Arc<Mutex<Option<ElementCache>>>> = LazyLock::new(|| Arc::new(Mutex::new(None)));
527
528pub fn get_element_cache(content: &str) -> ElementCache {
533 if let Ok(cache_guard) = ELEMENT_CACHE.lock() {
535 if let Some(existing_cache) = &*cache_guard
537 && let Some(cached_content) = &existing_cache.content
538 && cached_content == content
539 {
540 return existing_cache.clone();
541 }
542 }
543
544 let new_cache = ElementCache::new(content);
546
547 if let Ok(mut cache_guard) = ELEMENT_CACHE.lock() {
549 *cache_guard = Some(new_cache.clone());
550 }
551
552 new_cache
553}
554
555pub fn reset_element_cache() {
559 if let Ok(mut cache_guard) = ELEMENT_CACHE.lock() {
560 *cache_guard = None;
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn test_code_block_detection() {
570 let content = "Regular text\n\n```rust\nfn main() {\n println!(\"Hello\");\n}\n```\n\nMore text";
571 let cache = ElementCache::new(content);
572
573 assert_eq!(cache.code_blocks.len(), 1);
574 assert_eq!(cache.code_blocks[0].start_line, 3);
575 assert_eq!(cache.code_blocks[0].end_line, 7);
576 assert_eq!(cache.code_blocks[0].block_type, CodeBlockType::Fenced);
577 assert_eq!(cache.code_blocks[0].language, Some("rust".to_string()));
578
579 assert!(!cache.is_in_code_block(1));
580 assert!(!cache.is_in_code_block(2));
581 assert!(cache.is_in_code_block(3));
582 assert!(cache.is_in_code_block(4));
583 assert!(cache.is_in_code_block(5));
584 assert!(cache.is_in_code_block(6));
585 assert!(cache.is_in_code_block(7));
586 assert!(!cache.is_in_code_block(8));
587 assert!(!cache.is_in_code_block(9));
588 }
589
590 #[test]
591 fn test_list_item_detection_simple() {
592 let content =
593 "# Heading\n\n- First item\n - Nested item\n- Second item\n\n1. Ordered item\n 1. Nested ordered\n";
594 let cache = ElementCache::new(content);
595 assert_eq!(cache.list_items.len(), 5);
596 assert_eq!(cache.list_items[0].line_number, 3);
598 assert_eq!(cache.list_items[0].marker, "-");
599 assert_eq!(cache.list_items[0].nesting_level, 0);
600 assert_eq!(cache.list_items[1].line_number, 4);
602 assert_eq!(cache.list_items[1].marker, "-");
603 assert_eq!(cache.list_items[1].nesting_level, 1);
604 assert_eq!(cache.list_items[2].line_number, 5);
606 assert_eq!(cache.list_items[2].marker, "-");
607 assert_eq!(cache.list_items[2].nesting_level, 0);
608 assert_eq!(cache.list_items[3].line_number, 7);
610 assert_eq!(cache.list_items[3].marker, "1.");
611 assert_eq!(cache.list_items[3].nesting_level, 0);
612 assert_eq!(cache.list_items[4].line_number, 8);
614 assert_eq!(cache.list_items[4].marker, "1.");
615 assert_eq!(cache.list_items[4].nesting_level, 1);
616 }
617
618 #[test]
619 fn test_list_item_detection_complex() {
620 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";
621 let cache = ElementCache::new(complex);
622
623 assert_eq!(cache.list_items.len(), 10);
625 assert_eq!(cache.list_items[0].marker, "*");
627 assert_eq!(cache.list_items[0].nesting_level, 0);
628 assert_eq!(cache.list_items[1].marker, "-");
629 assert_eq!(cache.list_items[1].nesting_level, 1);
630 assert_eq!(cache.list_items[2].marker, "+");
631 assert_eq!(cache.list_items[2].nesting_level, 2);
632 assert_eq!(cache.list_items[3].marker, "-");
633 assert_eq!(cache.list_items[3].nesting_level, 1);
634 assert_eq!(cache.list_items[4].marker, "*");
635 assert_eq!(cache.list_items[4].nesting_level, 0);
636 assert_eq!(cache.list_items[5].marker, "*");
637 assert_eq!(cache.list_items[5].nesting_level, 0);
638 assert_eq!(cache.list_items[6].marker, "+");
639 assert_eq!(cache.list_items[6].nesting_level, 1);
640 assert_eq!(cache.list_items[7].marker, "-");
641 assert_eq!(cache.list_items[7].nesting_level, 2);
642 assert_eq!(cache.list_items[8].marker, "*");
643 assert_eq!(cache.list_items[8].nesting_level, 3);
644 assert_eq!(cache.list_items[9].marker, "+");
645 assert_eq!(cache.list_items[9].nesting_level, 4);
646 let expected_nesting = vec![0, 1, 2, 1, 0, 0, 1, 2, 3, 4];
647 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
648 assert_eq!(
649 actual_nesting, expected_nesting,
650 "Nesting levels should match expected values"
651 );
652 }
653
654 #[test]
655 fn test_list_item_detection_edge() {
656 let edge = "* Item 1\n\n - Nested 1\n + Nested 2\n\n* Item 2\n";
657 let cache = ElementCache::new(edge);
658 assert_eq!(cache.list_items.len(), 4);
659
660 let expected_nesting = vec![0, 1, 1, 0];
666 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
667 assert_eq!(
668 actual_nesting, expected_nesting,
669 "Nesting levels should be calculated based on indentation, not reset by blank lines"
670 );
671 }
672
673 #[test]
674 fn test_code_span_detection() {
675 let content = "Here is some `inline code` and here are ``nested `code` spans``";
676 let cache = ElementCache::new(content);
677
678 assert_eq!(cache.code_spans.len(), 2);
680
681 let span1_content = &content[cache.code_spans[0].start..cache.code_spans[0].end];
683 assert_eq!(span1_content, "`inline code`");
684
685 let span2_content = &content[cache.code_spans[1].start..cache.code_spans[1].end];
686 assert_eq!(span2_content, "``nested `code` spans``");
687 }
688
689 #[test]
690 fn test_get_element_cache() {
691 let content1 = "Test content";
692 let content2 = "Different content";
693
694 let cache1 = get_element_cache(content1);
696
697 let cache2 = get_element_cache(content1);
699
700 let cache3 = get_element_cache(content2);
702
703 assert_eq!(cache1.content.as_ref().unwrap(), content1);
704 assert_eq!(cache2.content.as_ref().unwrap(), content1);
705 assert_eq!(cache3.content.as_ref().unwrap(), content2);
706 }
707
708 #[test]
709 fn test_list_item_detection_deep_nesting_and_edge_cases() {
710 let content = "\
712* Level 1
713 - Level 2
714 + Level 3
715 * Level 4
716 - Level 5
717 + Level 6
718* Sibling 1
719 * Sibling 2
720\n - After blank line, not nested\n\n\t* Tab indented\n * 8 spaces indented\n* After excessive indent\n";
721 let cache = ElementCache::new(content);
722 let _expected_markers = ["*", "-", "+", "*", "-", "+", "*", "*", "-", "*", "*", "*"];
724 let _expected_indents = [0, 4, 8, 0, 4, 8, 0, 4, 8, 12, 16, 20];
725 let expected_content = vec![
726 "Level 1",
727 "Level 2",
728 "Level 3",
729 "Level 4",
730 "Level 5",
731 "Level 6",
732 "Sibling 1",
733 "Sibling 2",
734 "After blank line, not nested",
735 "Tab indented", "8 spaces indented", "After excessive indent",
738 ];
739 let actual_content: Vec<_> = cache.list_items.iter().map(|item| item.content.clone()).collect();
740 assert_eq!(
741 actual_content, expected_content,
742 "List item contents should match expected values"
743 );
744 let expected_nesting = vec![0, 1, 2, 3, 4, 5, 0, 1, 1, 1, 2, 0];
747 let actual_nesting: Vec<_> = cache.list_items.iter().map(|item| item.nesting_level).collect();
748 assert_eq!(
749 actual_nesting, expected_nesting,
750 "Nesting levels should match expected values"
751 );
752 assert!(
754 cache
755 .list_items
756 .iter()
757 .any(|item| item.marker == "*" && item.indentation >= 1),
758 "Tab or 8-space indented item not detected"
759 );
760 let after_blank = cache
762 .list_items
763 .iter()
764 .find(|item| item.content.contains("After blank line"));
765 assert!(after_blank.is_some());
766 assert_eq!(
767 after_blank.unwrap().nesting_level,
768 1,
769 "Item after blank line should maintain nesting based on indentation"
770 );
771 }
772
773 #[test]
774 fn test_tab_indentation_calculation() {
775 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";
777 let cache = ElementCache::new(content);
778
779 assert_eq!(cache.list_items.len(), 5);
780
781 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);
790 assert_eq!(cache.list_items[1].nesting_level, 1);
791 assert_eq!(cache.list_items[2].nesting_level, 2);
792 assert_eq!(cache.list_items[3].nesting_level, 1);
793 assert_eq!(cache.list_items[4].nesting_level, 2);
794 }
795
796 #[test]
797 fn test_mixed_tabs_and_spaces_indentation() {
798 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";
800
801 reset_element_cache();
803 let cache = ElementCache::new(content);
804
805 assert_eq!(cache.list_items.len(), 4);
806
807 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);
815 assert_eq!(cache.list_items[1].nesting_level, 1);
816 assert_eq!(cache.list_items[2].nesting_level, 1);
817 assert_eq!(cache.list_items[3].nesting_level, 2);
818 }
819
820 #[test]
821 fn test_tab_width_configuration() {
822 let content = "\t* Single tab\n\t\t* Double tab\n";
824 let cache = ElementCache::new(content);
825
826 assert_eq!(cache.list_items.len(), 2);
827
828 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);
834 assert_eq!(cache.list_items[1].nesting_level, 1);
835 }
836
837 #[test]
838 fn test_tab_expansion_debug() {
839 assert_eq!(ElementCache::calculate_indentation_width_default(""), 0);
841 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 1);
842 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 2);
843 assert_eq!(ElementCache::calculate_indentation_width_default(" "), 4);
844 assert_eq!(ElementCache::calculate_indentation_width_default("\t"), 4);
845 assert_eq!(ElementCache::calculate_indentation_width_default("\t\t"), 8);
846 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);
849 }
851
852 #[test]
853 fn test_mixed_tabs_debug() {
854 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";
856 let cache = ElementCache::new(content);
857
858 println!("Number of list items: {}", cache.list_items.len());
859 for (i, item) in cache.list_items.iter().enumerate() {
860 println!(
861 "Item {}: indent_str={:?}, indentation={}, content={:?}",
862 i, item.indent_str, item.indentation, item.content
863 );
864 }
865
866 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);
870 }
872}