1use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
12
13pub type CodeRanges = (Vec<(usize, usize)>, Vec<(usize, usize)>);
15
16#[derive(Debug, Clone)]
18pub struct CodeBlockDetail {
19 pub start: usize,
21 pub end: usize,
23 pub is_fenced: bool,
25 pub info_string: String,
27}
28
29#[derive(Debug, Clone)]
31pub struct StrongSpanDetail {
32 pub start: usize,
34 pub end: usize,
36 pub is_asterisk: bool,
38}
39
40pub type LineToListMap = std::collections::HashMap<usize, usize>;
42pub type ListStartValues = std::collections::HashMap<usize, u64>;
44
45pub struct ParseResult {
47 pub code_blocks: Vec<(usize, usize)>,
49 pub code_spans: Vec<(usize, usize)>,
51 pub code_block_details: Vec<CodeBlockDetail>,
53 pub strong_spans: Vec<StrongSpanDetail>,
55 pub line_to_list: LineToListMap,
57 pub list_start_values: ListStartValues,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum CodeBlockContext {
64 Standalone,
66 Indented,
68 Adjacent,
70}
71
72pub struct CodeBlockUtils;
74
75impl CodeBlockUtils {
76 pub fn detect_code_blocks(content: &str) -> Vec<(usize, usize)> {
86 Self::detect_code_blocks_and_spans(content).code_blocks
87 }
88
89 pub fn detect_code_blocks_and_spans(content: &str) -> ParseResult {
92 let mut blocks = Vec::new();
93 let mut spans = Vec::new();
94 let mut details = Vec::new();
95 let mut strong_spans = Vec::new();
96 let mut code_block_start: Option<(usize, bool, String)> = None;
97
98 let mut line_to_list = LineToListMap::new();
100 let mut list_start_values = ListStartValues::new();
101 let mut list_stack: Vec<(usize, bool, u64)> = Vec::new(); let mut next_list_id: usize = 0;
103
104 let line_starts: Vec<usize> = std::iter::once(0)
106 .chain(content.match_indices('\n').map(|(i, _)| i + 1))
107 .collect();
108
109 let byte_to_line = |byte_offset: usize| -> usize { line_starts.partition_point(|&start| start <= byte_offset) };
110
111 let options = Options::all();
113 let parser = Parser::new_ext(content, options).into_offset_iter();
114
115 for (event, range) in parser {
116 match event {
117 Event::Start(Tag::CodeBlock(kind)) => {
118 let (is_fenced, info_string) = match &kind {
119 CodeBlockKind::Fenced(info) => (true, info.to_string()),
120 CodeBlockKind::Indented => (false, String::new()),
121 };
122 code_block_start = Some((range.start, is_fenced, info_string));
123 }
124 Event::End(TagEnd::CodeBlock) => {
125 if let Some((start, is_fenced, info_string)) = code_block_start.take() {
126 blocks.push((start, range.end));
127 details.push(CodeBlockDetail {
128 start,
129 end: range.end,
130 is_fenced,
131 info_string,
132 });
133 }
134 }
135 Event::Start(Tag::Strong) => {
136 if range.start + 2 <= content.len() {
137 let is_asterisk = &content[range.start..range.start + 2] == "**";
138 strong_spans.push(StrongSpanDetail {
139 start: range.start,
140 end: range.end,
141 is_asterisk,
142 });
143 }
144 }
145 Event::Start(Tag::List(start_num)) => {
146 let is_ordered = start_num.is_some();
147 let start_value = start_num.unwrap_or(1);
148 list_stack.push((next_list_id, is_ordered, start_value));
149 if is_ordered {
150 list_start_values.insert(next_list_id, start_value);
151 }
152 next_list_id += 1;
153 }
154 Event::End(TagEnd::List(_)) => {
155 list_stack.pop();
156 }
157 Event::Start(Tag::Item) => {
158 if let Some(&(list_id, is_ordered, _)) = list_stack.last()
159 && is_ordered
160 {
161 let line_num = byte_to_line(range.start);
162 line_to_list.insert(line_num, list_id);
163 }
164 }
165 Event::Code(_) => {
166 spans.push((range.start, range.end));
167 }
168 _ => {}
169 }
170 }
171
172 if let Some((start, is_fenced, info_string)) = code_block_start {
175 blocks.push((start, content.len()));
176 details.push(CodeBlockDetail {
177 start,
178 end: content.len(),
179 is_fenced,
180 info_string,
181 });
182 }
183
184 blocks.sort_by_key(|&(start, _)| start);
186 spans.sort_by_key(|&(start, _)| start);
187 details.sort_by_key(|d| d.start);
188 strong_spans.sort_by_key(|s| s.start);
189 ParseResult {
190 code_blocks: blocks,
191 code_spans: spans,
192 code_block_details: details,
193 strong_spans,
194 line_to_list,
195 list_start_values,
196 }
197 }
198
199 pub fn is_in_code_block_or_span(blocks: &[(usize, usize)], pos: usize) -> bool {
201 Self::is_in_code_block(blocks, pos)
202 }
203
204 pub fn is_in_code_block(blocks: &[(usize, usize)], pos: usize) -> bool {
210 let idx = blocks.partition_point(|&(start, _)| start <= pos);
212 idx > 0 && pos < blocks[idx - 1].1
215 }
216
217 pub fn analyze_code_block_context(
220 lines: &[crate::lint_context::LineInfo],
221 line_idx: usize,
222 min_continuation_indent: usize,
223 ) -> CodeBlockContext {
224 if let Some(line_info) = lines.get(line_idx) {
225 if line_info.indent >= min_continuation_indent {
227 return CodeBlockContext::Indented;
228 }
229
230 let (prev_blanks, next_blanks) = Self::count_surrounding_blank_lines(lines, line_idx);
232
233 if prev_blanks > 0 || next_blanks > 0 {
236 return CodeBlockContext::Standalone;
237 }
238
239 CodeBlockContext::Adjacent
241 } else {
242 CodeBlockContext::Adjacent
244 }
245 }
246
247 fn count_surrounding_blank_lines(lines: &[crate::lint_context::LineInfo], line_idx: usize) -> (usize, usize) {
249 let mut prev_blanks = 0;
250 let mut next_blanks = 0;
251
252 for i in (0..line_idx).rev() {
254 if let Some(line) = lines.get(i) {
255 if line.is_blank {
256 prev_blanks += 1;
257 } else {
258 break;
259 }
260 } else {
261 break;
262 }
263 }
264
265 for i in (line_idx + 1)..lines.len() {
267 if let Some(line) = lines.get(i) {
268 if line.is_blank {
269 next_blanks += 1;
270 } else {
271 break;
272 }
273 } else {
274 break;
275 }
276 }
277
278 (prev_blanks, next_blanks)
279 }
280
281 pub fn calculate_min_continuation_indent(
284 content: &str,
285 lines: &[crate::lint_context::LineInfo],
286 current_line_idx: usize,
287 ) -> usize {
288 for i in (0..current_line_idx).rev() {
290 if let Some(line_info) = lines.get(i) {
291 if let Some(list_item) = &line_info.list_item {
292 return if list_item.is_ordered {
294 list_item.marker_column + list_item.marker.len() + 1 } else {
296 list_item.marker_column + 2 };
298 }
299
300 if line_info.heading.is_some() || Self::is_structural_separator(line_info.content(content)) {
302 break;
303 }
304 }
305 }
306
307 0 }
309
310 fn is_structural_separator(content: &str) -> bool {
312 let trimmed = content.trim();
313 trimmed.starts_with("---")
314 || trimmed.starts_with("***")
315 || trimmed.starts_with("___")
316 || crate::utils::skip_context::is_table_line(trimmed)
317 || trimmed.starts_with(">") }
319
320 pub fn detect_markdown_code_blocks(content: &str) -> Vec<MarkdownCodeBlock> {
328 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
329
330 let mut blocks = Vec::new();
331 let mut current_block: Option<MarkdownCodeBlockBuilder> = None;
332
333 let options = Options::all();
334 let parser = Parser::new_ext(content, options).into_offset_iter();
335
336 for (event, range) in parser {
337 match event {
338 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
339 let language = info.split_whitespace().next().unwrap_or("");
341 if language.eq_ignore_ascii_case("markdown") || language.eq_ignore_ascii_case("md") {
342 let block_start = range.start;
344 let content_start = content[block_start..]
345 .find('\n')
346 .map(|i| block_start + i + 1)
347 .unwrap_or(content.len());
348
349 current_block = Some(MarkdownCodeBlockBuilder { content_start });
350 }
351 }
352 Event::End(TagEnd::CodeBlock) => {
353 if let Some(builder) = current_block.take() {
354 let block_end = range.end;
356
357 if builder.content_start > block_end || builder.content_start > content.len() {
359 continue;
360 }
361
362 let search_range = &content[builder.content_start..block_end.min(content.len())];
363 let content_end = search_range
364 .rfind('\n')
365 .map(|i| builder.content_start + i)
366 .unwrap_or(builder.content_start);
367
368 if content_end >= builder.content_start {
370 blocks.push(MarkdownCodeBlock {
371 content_start: builder.content_start,
372 content_end,
373 });
374 }
375 }
376 }
377 _ => {}
378 }
379 }
380
381 blocks
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct MarkdownCodeBlock {
388 pub content_start: usize,
390 pub content_end: usize,
392}
393
394struct MarkdownCodeBlockBuilder {
396 content_start: usize,
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_detect_fenced_code_blocks() {
405 let content = "Some text\n```\ncode here\n```\nMore text";
410 let blocks = CodeBlockUtils::detect_code_blocks(content);
411 assert_eq!(blocks.len(), 1);
413
414 let fenced_block = blocks
416 .iter()
417 .find(|(start, end)| end - start > 10 && content[*start..*end].contains("code here"));
418 assert!(fenced_block.is_some());
419
420 let content = "Some text\n~~~\ncode here\n~~~\nMore text";
422 let blocks = CodeBlockUtils::detect_code_blocks(content);
423 assert_eq!(blocks.len(), 1);
424 assert_eq!(&content[blocks[0].0..blocks[0].1], "~~~\ncode here\n~~~");
425
426 let content = "Text\n```\ncode1\n```\nMiddle\n~~~\ncode2\n~~~\nEnd";
428 let blocks = CodeBlockUtils::detect_code_blocks(content);
429 assert_eq!(blocks.len(), 2);
431 }
432
433 #[test]
434 fn test_detect_code_blocks_with_language() {
435 let content = "Text\n```rust\nfn main() {}\n```\nMore";
437 let blocks = CodeBlockUtils::detect_code_blocks(content);
438 assert_eq!(blocks.len(), 1);
440 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("fn main"));
442 assert!(fenced.is_some());
443 }
444
445 #[test]
446 fn test_unclosed_code_block() {
447 let content = "Text\n```\ncode here\nno closing fence";
449 let blocks = CodeBlockUtils::detect_code_blocks(content);
450 assert_eq!(blocks.len(), 1);
451 assert_eq!(blocks[0].1, content.len());
452 }
453
454 #[test]
455 fn test_indented_code_blocks() {
456 let content = "Paragraph\n\n code line 1\n code line 2\n\nMore text";
458 let blocks = CodeBlockUtils::detect_code_blocks(content);
459 assert_eq!(blocks.len(), 1);
460 assert!(content[blocks[0].0..blocks[0].1].contains("code line 1"));
461 assert!(content[blocks[0].0..blocks[0].1].contains("code line 2"));
462
463 let content = "Paragraph\n\n\tcode with tab\n\tanother line\n\nText";
465 let blocks = CodeBlockUtils::detect_code_blocks(content);
466 assert_eq!(blocks.len(), 1);
467 }
468
469 #[test]
470 fn test_indented_code_requires_blank_line() {
471 let content = "Paragraph\n indented but not code\nMore text";
473 let blocks = CodeBlockUtils::detect_code_blocks(content);
474 assert_eq!(blocks.len(), 0);
475
476 let content = "Paragraph\n\n now it's code\nMore text";
478 let blocks = CodeBlockUtils::detect_code_blocks(content);
479 assert_eq!(blocks.len(), 1);
480 }
481
482 #[test]
483 fn test_indented_content_with_list_markers_is_code_block() {
484 let content = "List:\n\n - Item 1\n - Item 2\n * Item 3\n + Item 4";
489 let blocks = CodeBlockUtils::detect_code_blocks(content);
490 assert_eq!(blocks.len(), 1); let content = "List:\n\n 1. First\n 2. Second";
494 let blocks = CodeBlockUtils::detect_code_blocks(content);
495 assert_eq!(blocks.len(), 1); }
497
498 #[test]
499 fn test_actual_list_items_not_code_blocks() {
500 let content = "- Item 1\n- Item 2\n* Item 3";
502 let blocks = CodeBlockUtils::detect_code_blocks(content);
503 assert_eq!(blocks.len(), 0);
504
505 let content = "- Item 1\n - Nested item\n- Item 2";
507 let blocks = CodeBlockUtils::detect_code_blocks(content);
508 assert_eq!(blocks.len(), 0);
509 }
510
511 #[test]
512 fn test_inline_code_spans_not_detected() {
513 let content = "Text with `inline code` here";
515 let blocks = CodeBlockUtils::detect_code_blocks(content);
516 assert_eq!(blocks.len(), 0); let content = "Text with ``code with ` backtick`` here";
520 let blocks = CodeBlockUtils::detect_code_blocks(content);
521 assert_eq!(blocks.len(), 0); let content = "Has `code1` and `code2` spans";
525 let blocks = CodeBlockUtils::detect_code_blocks(content);
526 assert_eq!(blocks.len(), 0); }
528
529 #[test]
530 fn test_unclosed_code_span() {
531 let content = "Text with `unclosed code span";
533 let blocks = CodeBlockUtils::detect_code_blocks(content);
534 assert_eq!(blocks.len(), 0);
535
536 let content = "Text with ``one style` different close";
538 let blocks = CodeBlockUtils::detect_code_blocks(content);
539 assert_eq!(blocks.len(), 0);
540 }
541
542 #[test]
543 fn test_mixed_code_blocks_and_spans() {
544 let content = "Has `span1` text\n```\nblock\n```\nand `span2`";
545 let blocks = CodeBlockUtils::detect_code_blocks(content);
546 assert_eq!(blocks.len(), 1);
548
549 assert!(blocks.iter().any(|(s, e)| content[*s..*e].contains("block")));
551 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span1`"));
553 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span2`"));
554 }
555
556 #[test]
557 fn test_is_in_code_block_or_span() {
558 let blocks = vec![(10, 20), (30, 40), (50, 60)];
559
560 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 15));
562 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 35));
563 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 55));
564
565 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 10)); assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 20)); assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 5));
571 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 25));
572 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 65));
573 }
574
575 #[test]
576 fn test_empty_content() {
577 let blocks = CodeBlockUtils::detect_code_blocks("");
578 assert_eq!(blocks.len(), 0);
579 }
580
581 #[test]
582 fn test_code_block_at_start() {
583 let content = "```\ncode\n```\nText after";
584 let blocks = CodeBlockUtils::detect_code_blocks(content);
585 assert_eq!(blocks.len(), 1);
587 assert_eq!(blocks[0].0, 0); }
589
590 #[test]
591 fn test_code_block_at_end() {
592 let content = "Text before\n```\ncode\n```";
593 let blocks = CodeBlockUtils::detect_code_blocks(content);
594 assert_eq!(blocks.len(), 1);
596 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("code"));
598 assert!(fenced.is_some());
599 }
600
601 #[test]
602 fn test_nested_fence_markers() {
603 let content = "Text\n````\n```\nnested\n```\n````\nAfter";
605 let blocks = CodeBlockUtils::detect_code_blocks(content);
606 assert!(!blocks.is_empty());
608 let outer = blocks.iter().find(|(s, e)| content[*s..*e].contains("nested"));
610 assert!(outer.is_some());
611 }
612
613 #[test]
614 fn test_indented_code_with_blank_lines() {
615 let content = "Text\n\n line1\n\n line2\n\nAfter";
617 let blocks = CodeBlockUtils::detect_code_blocks(content);
618 assert!(!blocks.is_empty());
620 let all_content: String = blocks
622 .iter()
623 .map(|(s, e)| &content[*s..*e])
624 .collect::<Vec<_>>()
625 .join("");
626 assert!(all_content.contains("line1") || content[blocks[0].0..blocks[0].1].contains("line1"));
627 }
628
629 #[test]
630 fn test_code_span_with_spaces() {
631 let content = "Text ` code with spaces ` more";
633 let blocks = CodeBlockUtils::detect_code_blocks(content);
634 assert_eq!(blocks.len(), 0); }
636
637 #[test]
638 fn test_fenced_block_with_info_string() {
639 let content = "```rust,no_run,should_panic\ncode\n```";
641 let blocks = CodeBlockUtils::detect_code_blocks(content);
642 assert_eq!(blocks.len(), 1);
644 assert_eq!(blocks[0].0, 0);
645 }
646
647 #[test]
648 fn test_indented_fences_not_code_blocks() {
649 let content = "Text\n ```\n code\n ```\nAfter";
651 let blocks = CodeBlockUtils::detect_code_blocks(content);
652 assert_eq!(blocks.len(), 1);
654 }
655
656 #[test]
658 fn test_backticks_in_info_string_not_code_block() {
659 let content = "```something```\n\n```bash\n# comment\n```";
665 let blocks = CodeBlockUtils::detect_code_blocks(content);
666 assert_eq!(blocks.len(), 1);
668 assert!(content[blocks[0].0..blocks[0].1].contains("# comment"));
670 }
671
672 #[test]
673 fn test_issue_175_reproduction() {
674 let content = "```something```\n\n```bash\n# Have a parrot\necho \"🦜\"\n```";
676 let blocks = CodeBlockUtils::detect_code_blocks(content);
677 assert_eq!(blocks.len(), 1);
679 assert!(content[blocks[0].0..blocks[0].1].contains("Have a parrot"));
680 }
681
682 #[test]
683 fn test_tilde_fence_allows_tildes_in_info_string() {
684 let content = "~~~abc~~~\ncode content\n~~~";
687 let blocks = CodeBlockUtils::detect_code_blocks(content);
688 assert_eq!(blocks.len(), 1);
690 }
691
692 #[test]
693 fn test_nested_longer_fence_contains_shorter() {
694 let content = "````\n```\nnested content\n```\n````";
696 let blocks = CodeBlockUtils::detect_code_blocks(content);
697 assert_eq!(blocks.len(), 1);
698 assert!(content[blocks[0].0..blocks[0].1].contains("nested content"));
699 }
700
701 #[test]
702 fn test_mixed_fence_types() {
703 let content = "~~~\n```\nmixed content\n~~~";
705 let blocks = CodeBlockUtils::detect_code_blocks(content);
706 assert_eq!(blocks.len(), 1);
707 assert!(content[blocks[0].0..blocks[0].1].contains("mixed content"));
708 }
709
710 #[test]
711 fn test_indented_code_in_list_issue_276() {
712 let content = r#"1. First item
7142. Second item with code:
715
716 # This is a code block in a list
717 print("Hello, world!")
718
7194. Third item"#;
720
721 let blocks = CodeBlockUtils::detect_code_blocks(content);
722 assert!(!blocks.is_empty(), "Should detect indented code block inside list");
724
725 let all_content: String = blocks
727 .iter()
728 .map(|(s, e)| &content[*s..*e])
729 .collect::<Vec<_>>()
730 .join("");
731 assert!(
732 all_content.contains("code block in a list") || all_content.contains("print"),
733 "Detected block should contain the code content: {all_content:?}"
734 );
735 }
736
737 #[test]
738 fn test_detect_markdown_code_blocks() {
739 let content = r#"# Example
740
741```markdown
742# Heading
743Content here
744```
745
746```md
747Another heading
748More content
749```
750
751```rust
752// Not markdown
753fn main() {}
754```
755"#;
756
757 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
758
759 assert_eq!(
761 blocks.len(),
762 2,
763 "Should detect exactly 2 markdown blocks, got {blocks:?}"
764 );
765
766 let first = &blocks[0];
768 let first_content = &content[first.content_start..first.content_end];
769 assert!(
770 first_content.contains("# Heading"),
771 "First block should contain '# Heading', got: {first_content:?}"
772 );
773
774 let second = &blocks[1];
776 let second_content = &content[second.content_start..second.content_end];
777 assert!(
778 second_content.contains("Another heading"),
779 "Second block should contain 'Another heading', got: {second_content:?}"
780 );
781 }
782
783 #[test]
784 fn test_detect_markdown_code_blocks_empty() {
785 let content = "# Just a heading\n\nNo code blocks here\n";
786 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
787 assert_eq!(blocks.len(), 0);
788 }
789
790 #[test]
791 fn test_detect_markdown_code_blocks_case_insensitive() {
792 let content = "```MARKDOWN\nContent\n```\n";
793 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
794 assert_eq!(blocks.len(), 1);
795 }
796
797 #[test]
798 fn test_detect_markdown_code_blocks_at_eof_no_trailing_newline() {
799 let content = "# Doc\n\n```markdown\nContent\n```";
801 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
802 assert_eq!(blocks.len(), 1);
803 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
805 assert!(block_content.contains("Content"));
806 }
807
808 #[test]
809 fn test_detect_markdown_code_blocks_single_line_content() {
810 let content = "```markdown\nX\n```\n";
812 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
813 assert_eq!(blocks.len(), 1);
814 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
815 assert_eq!(block_content, "X");
816 }
817
818 #[test]
819 fn test_detect_markdown_code_blocks_empty_content() {
820 let content = "```markdown\n```\n";
822 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
823 if !blocks.is_empty() {
826 assert!(blocks[0].content_start <= blocks[0].content_end);
828 }
829 }
830
831 #[test]
832 fn test_detect_markdown_code_blocks_validates_ranges() {
833 let test_cases = [
835 "", "```markdown", "```markdown\n", "```\n```", "```markdown\n```", " ```markdown\n X\n ```", ];
842
843 for content in test_cases {
844 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
846 for block in &blocks {
848 assert!(
849 block.content_start <= block.content_end,
850 "Invalid range in content: {content:?}"
851 );
852 assert!(
853 block.content_end <= content.len(),
854 "Range exceeds content length in: {content:?}"
855 );
856 }
857 }
858 }
859
860 #[test]
863 fn test_is_in_code_block_empty_blocks() {
864 assert!(!CodeBlockUtils::is_in_code_block(&[], 0));
865 assert!(!CodeBlockUtils::is_in_code_block(&[], 100));
866 assert!(!CodeBlockUtils::is_in_code_block(&[], usize::MAX));
867 }
868
869 #[test]
870 fn test_is_in_code_block_single_range() {
871 let blocks = [(10, 20)];
872 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
873 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 9));
874 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
875 assert!(CodeBlockUtils::is_in_code_block(&blocks, 15));
876 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
877 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 20));
879 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 21));
880 }
881
882 #[test]
883 fn test_is_in_code_block_multiple_ranges() {
884 let blocks = [(5, 10), (20, 30), (50, 60)];
885 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
887 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
888 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
890 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
891 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 10));
893 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 15));
894 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 19));
895 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
897 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
898 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
900 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 49));
901 assert!(CodeBlockUtils::is_in_code_block(&blocks, 50));
903 assert!(CodeBlockUtils::is_in_code_block(&blocks, 59));
904 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 60));
906 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 1000));
907 }
908
909 #[test]
910 fn test_is_in_code_block_adjacent_ranges() {
911 let blocks = [(0, 10), (10, 20), (20, 30)];
913 assert!(CodeBlockUtils::is_in_code_block(&blocks, 0));
914 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
915 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
916 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
917 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
918 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
919 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
920 }
921
922 #[test]
923 fn test_is_in_code_block_single_byte_range() {
924 let blocks = [(5, 6)];
925 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
926 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
927 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 6));
928 }
929
930 #[test]
931 fn test_is_in_code_block_matches_linear_scan() {
932 let content = "# Heading\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nSome text\n\n```\nmore code\n```\n\nEnd\n";
935 let blocks = CodeBlockUtils::detect_code_blocks(content);
936
937 for pos in 0..content.len() {
938 let binary = CodeBlockUtils::is_in_code_block(&blocks, pos);
939 let linear = blocks.iter().any(|&(s, e)| pos >= s && pos < e);
940 assert_eq!(
941 binary, linear,
942 "Mismatch at pos {pos}: binary={binary}, linear={linear}, blocks={blocks:?}"
943 );
944 }
945 }
946
947 #[test]
948 fn test_is_in_code_block_at_range_boundaries() {
949 let blocks = [(100, 200), (300, 400), (500, 600)];
951 for &(start, end) in &blocks {
952 assert!(
953 !CodeBlockUtils::is_in_code_block(&blocks, start - 1),
954 "pos={} should be outside",
955 start - 1
956 );
957 assert!(
958 CodeBlockUtils::is_in_code_block(&blocks, start),
959 "pos={start} should be inside"
960 );
961 assert!(
962 CodeBlockUtils::is_in_code_block(&blocks, end - 1),
963 "pos={} should be inside",
964 end - 1
965 );
966 assert!(
967 !CodeBlockUtils::is_in_code_block(&blocks, end),
968 "pos={end} should be outside"
969 );
970 }
971 }
972}