1use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
12
13use super::parser_options::rumdl_parser_options;
14
15#[derive(Debug, Clone)]
17pub struct CodeBlockDetail {
18 pub start: usize,
20 pub end: usize,
22 pub is_fenced: bool,
24 pub info_string: String,
26}
27
28#[derive(Debug, Clone)]
30pub struct StrongSpanDetail {
31 pub start: usize,
33 pub end: usize,
35 pub is_asterisk: bool,
37}
38
39pub type LineToListMap = std::collections::HashMap<usize, usize>;
41pub type ListStartValues = std::collections::HashMap<usize, u64>;
43
44pub struct ParseResult {
46 pub code_blocks: Vec<(usize, usize)>,
48 pub code_spans: Vec<(usize, usize)>,
50 pub code_block_details: Vec<CodeBlockDetail>,
52 pub strong_spans: Vec<StrongSpanDetail>,
54 pub line_to_list: LineToListMap,
56 pub list_start_values: ListStartValues,
58 pub html_blocks: Vec<(usize, usize)>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum CodeBlockContext {
70 Standalone,
72 Indented,
74 Adjacent,
76}
77
78pub struct CodeBlockUtils;
80
81impl CodeBlockUtils {
82 pub fn detect_code_blocks(content: &str) -> Vec<(usize, usize)> {
92 Self::detect_code_blocks_and_spans(content).code_blocks
93 }
94
95 pub fn detect_code_blocks_and_spans(content: &str) -> ParseResult {
98 let mut blocks = Vec::new();
99 let mut spans = Vec::new();
100 let mut details = Vec::new();
101 let mut strong_spans = Vec::new();
102 let mut html_blocks = Vec::new();
103 let mut code_block_start: Option<(usize, bool, String)> = None;
104
105 let mut line_to_list = LineToListMap::new();
107 let mut list_start_values = ListStartValues::new();
108 let mut list_stack: Vec<(usize, bool, u64)> = Vec::new(); let mut next_list_id: usize = 0;
110
111 let line_starts: Vec<usize> = std::iter::once(0)
113 .chain(content.match_indices('\n').map(|(i, _)| i + 1))
114 .collect();
115
116 let byte_to_line = |byte_offset: usize| -> usize { line_starts.partition_point(|&start| start <= byte_offset) };
117
118 let options = rumdl_parser_options();
119 let parser = Parser::new_ext(content, options).into_offset_iter();
120
121 for (event, range) in parser {
122 match event {
123 Event::Start(Tag::CodeBlock(kind)) => {
124 let (is_fenced, info_string) = match &kind {
125 CodeBlockKind::Fenced(info) => (true, info.to_string()),
126 CodeBlockKind::Indented => (false, String::new()),
127 };
128 code_block_start = Some((range.start, is_fenced, info_string));
129 }
130 Event::End(TagEnd::CodeBlock) => {
131 if let Some((start, is_fenced, info_string)) = code_block_start.take() {
132 blocks.push((start, range.end));
133 details.push(CodeBlockDetail {
134 start,
135 end: range.end,
136 is_fenced,
137 info_string,
138 });
139 }
140 }
141 Event::Start(Tag::Strong) => {
142 if range.start + 2 <= content.len() {
143 let is_asterisk = &content[range.start..range.start + 2] == "**";
144 strong_spans.push(StrongSpanDetail {
145 start: range.start,
146 end: range.end,
147 is_asterisk,
148 });
149 }
150 }
151 Event::Start(Tag::List(start_num)) => {
152 let is_ordered = start_num.is_some();
153 let start_value = start_num.unwrap_or(1);
154 list_stack.push((next_list_id, is_ordered, start_value));
155 if is_ordered {
156 list_start_values.insert(next_list_id, start_value);
157 }
158 next_list_id += 1;
159 }
160 Event::End(TagEnd::List(_)) => {
161 list_stack.pop();
162 }
163 Event::Start(Tag::Item) => {
164 if let Some(&(list_id, is_ordered, _)) = list_stack.last()
165 && is_ordered
166 {
167 let line_num = byte_to_line(range.start);
168 line_to_list.insert(line_num, list_id);
169 }
170 }
171 Event::Start(Tag::HtmlBlock) => {
172 html_blocks.push((range.start, range.end));
174 }
175 Event::Code(_) => {
176 spans.push((range.start, range.end));
177 }
178 _ => {}
179 }
180 }
181
182 if let Some((start, is_fenced, info_string)) = code_block_start {
185 blocks.push((start, content.len()));
186 details.push(CodeBlockDetail {
187 start,
188 end: content.len(),
189 is_fenced,
190 info_string,
191 });
192 }
193
194 blocks.sort_by_key(|&(start, _)| start);
196 spans.sort_by_key(|&(start, _)| start);
197 details.sort_by_key(|d| d.start);
198 strong_spans.sort_by_key(|s| s.start);
199 html_blocks.sort_by_key(|&(start, _)| start);
200 ParseResult {
201 code_blocks: blocks,
202 code_spans: spans,
203 code_block_details: details,
204 strong_spans,
205 line_to_list,
206 list_start_values,
207 html_blocks,
208 }
209 }
210
211 pub fn is_in_code_block_or_span(blocks: &[(usize, usize)], pos: usize) -> bool {
213 Self::is_in_code_block(blocks, pos)
214 }
215
216 pub fn is_in_code_block(blocks: &[(usize, usize)], pos: usize) -> bool {
222 let idx = blocks.partition_point(|&(start, _)| start <= pos);
224 idx > 0 && pos < blocks[idx - 1].1
227 }
228
229 pub fn analyze_code_block_context(
232 lines: &[crate::lint_context::LineInfo],
233 line_idx: usize,
234 min_continuation_indent: usize,
235 ) -> CodeBlockContext {
236 if let Some(line_info) = lines.get(line_idx) {
237 if line_info.indent >= min_continuation_indent {
239 return CodeBlockContext::Indented;
240 }
241
242 let (prev_blanks, next_blanks) = Self::count_surrounding_blank_lines(lines, line_idx);
244
245 if prev_blanks > 0 || next_blanks > 0 {
248 return CodeBlockContext::Standalone;
249 }
250
251 CodeBlockContext::Adjacent
253 } else {
254 CodeBlockContext::Adjacent
256 }
257 }
258
259 fn count_surrounding_blank_lines(lines: &[crate::lint_context::LineInfo], line_idx: usize) -> (usize, usize) {
261 let mut prev_blanks = 0;
262 let mut next_blanks = 0;
263
264 for i in (0..line_idx).rev() {
266 if let Some(line) = lines.get(i) {
267 if line.is_blank {
268 prev_blanks += 1;
269 } else {
270 break;
271 }
272 } else {
273 break;
274 }
275 }
276
277 for i in (line_idx + 1)..lines.len() {
279 if let Some(line) = lines.get(i) {
280 if line.is_blank {
281 next_blanks += 1;
282 } else {
283 break;
284 }
285 } else {
286 break;
287 }
288 }
289
290 (prev_blanks, next_blanks)
291 }
292
293 pub fn calculate_min_continuation_indent(
296 content: &str,
297 lines: &[crate::lint_context::LineInfo],
298 current_line_idx: usize,
299 ) -> usize {
300 for i in (0..current_line_idx).rev() {
302 if let Some(line_info) = lines.get(i) {
303 if let Some(list_item) = &line_info.list_item {
304 return if list_item.is_ordered {
306 list_item.marker_column + list_item.marker.len() + 1 } else {
308 list_item.marker_column + 2 };
310 }
311
312 if line_info.is_valid_heading() || Self::is_structural_separator(line_info.content(content)) {
314 break;
315 }
316 }
317 }
318
319 0 }
321
322 fn is_structural_separator(content: &str) -> bool {
324 let trimmed = content.trim();
325 trimmed.starts_with("---")
326 || trimmed.starts_with("***")
327 || trimmed.starts_with("___")
328 || crate::utils::skip_context::is_table_line(trimmed)
329 || trimmed.starts_with('>') }
331
332 pub fn detect_markdown_code_blocks(content: &str) -> Vec<MarkdownCodeBlock> {
340 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
341
342 let mut blocks = Vec::new();
343 let mut current_block: Option<MarkdownCodeBlockBuilder> = None;
344
345 let options = rumdl_parser_options();
346 let parser = Parser::new_ext(content, options).into_offset_iter();
347
348 for (event, range) in parser {
349 match event {
350 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
351 let language = info.split_whitespace().next().unwrap_or("");
353 if language.eq_ignore_ascii_case("markdown") || language.eq_ignore_ascii_case("md") {
354 let block_start = range.start;
356 let content_start = content[block_start..]
357 .find('\n')
358 .map_or(content.len(), |i| block_start + i + 1);
359
360 current_block = Some(MarkdownCodeBlockBuilder { content_start });
361 }
362 }
363 Event::End(TagEnd::CodeBlock) => {
364 if let Some(builder) = current_block.take() {
365 let block_end = range.end;
367
368 if builder.content_start > block_end || builder.content_start > content.len() {
370 continue;
371 }
372
373 let search_range = &content[builder.content_start..block_end.min(content.len())];
374 let content_end = search_range
375 .rfind('\n')
376 .map_or(builder.content_start, |i| builder.content_start + i);
377
378 if content_end >= builder.content_start {
380 blocks.push(MarkdownCodeBlock {
381 content_start: builder.content_start,
382 content_end,
383 });
384 }
385 }
386 }
387 _ => {}
388 }
389 }
390
391 blocks
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct MarkdownCodeBlock {
398 pub content_start: usize,
400 pub content_end: usize,
402}
403
404struct MarkdownCodeBlockBuilder {
406 content_start: usize,
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_detect_fenced_code_blocks() {
415 let content = "Some text\n```\ncode here\n```\nMore text";
420 let blocks = CodeBlockUtils::detect_code_blocks(content);
421 assert_eq!(blocks.len(), 1);
423
424 let fenced_block = blocks
426 .iter()
427 .find(|(start, end)| end - start > 10 && content[*start..*end].contains("code here"));
428 assert!(fenced_block.is_some());
429
430 let content = "Some text\n~~~\ncode here\n~~~\nMore text";
432 let blocks = CodeBlockUtils::detect_code_blocks(content);
433 assert_eq!(blocks.len(), 1);
434 assert_eq!(&content[blocks[0].0..blocks[0].1], "~~~\ncode here\n~~~");
435
436 let content = "Text\n```\ncode1\n```\nMiddle\n~~~\ncode2\n~~~\nEnd";
438 let blocks = CodeBlockUtils::detect_code_blocks(content);
439 assert_eq!(blocks.len(), 2);
441 }
442
443 #[test]
444 fn test_detect_code_blocks_with_language() {
445 let content = "Text\n```rust\nfn main() {}\n```\nMore";
447 let blocks = CodeBlockUtils::detect_code_blocks(content);
448 assert_eq!(blocks.len(), 1);
450 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("fn main"));
452 assert!(fenced.is_some());
453 }
454
455 #[test]
456 fn test_unclosed_code_block() {
457 let content = "Text\n```\ncode here\nno closing fence";
459 let blocks = CodeBlockUtils::detect_code_blocks(content);
460 assert_eq!(blocks.len(), 1);
461 assert_eq!(blocks[0].1, content.len());
462 }
463
464 #[test]
465 fn test_indented_code_blocks() {
466 let content = "Paragraph\n\n code line 1\n code line 2\n\nMore text";
468 let blocks = CodeBlockUtils::detect_code_blocks(content);
469 assert_eq!(blocks.len(), 1);
470 assert!(content[blocks[0].0..blocks[0].1].contains("code line 1"));
471 assert!(content[blocks[0].0..blocks[0].1].contains("code line 2"));
472
473 let content = "Paragraph\n\n\tcode with tab\n\tanother line\n\nText";
475 let blocks = CodeBlockUtils::detect_code_blocks(content);
476 assert_eq!(blocks.len(), 1);
477 }
478
479 #[test]
480 fn test_indented_code_requires_blank_line() {
481 let content = "Paragraph\n indented but not code\nMore text";
483 let blocks = CodeBlockUtils::detect_code_blocks(content);
484 assert_eq!(blocks.len(), 0);
485
486 let content = "Paragraph\n\n now it's code\nMore text";
488 let blocks = CodeBlockUtils::detect_code_blocks(content);
489 assert_eq!(blocks.len(), 1);
490 }
491
492 #[test]
493 fn test_indented_content_with_list_markers_is_code_block() {
494 let content = "List:\n\n - Item 1\n - Item 2\n * Item 3\n + Item 4";
499 let blocks = CodeBlockUtils::detect_code_blocks(content);
500 assert_eq!(blocks.len(), 1); let content = "List:\n\n 1. First\n 2. Second";
504 let blocks = CodeBlockUtils::detect_code_blocks(content);
505 assert_eq!(blocks.len(), 1); }
507
508 #[test]
509 fn test_actual_list_items_not_code_blocks() {
510 let content = "- Item 1\n- Item 2\n* Item 3";
512 let blocks = CodeBlockUtils::detect_code_blocks(content);
513 assert_eq!(blocks.len(), 0);
514
515 let content = "- Item 1\n - Nested item\n- Item 2";
517 let blocks = CodeBlockUtils::detect_code_blocks(content);
518 assert_eq!(blocks.len(), 0);
519 }
520
521 #[test]
522 fn test_inline_code_spans_not_detected() {
523 let content = "Text with `inline code` here";
525 let blocks = CodeBlockUtils::detect_code_blocks(content);
526 assert_eq!(blocks.len(), 0); let content = "Text with ``code with ` backtick`` here";
530 let blocks = CodeBlockUtils::detect_code_blocks(content);
531 assert_eq!(blocks.len(), 0); let content = "Has `code1` and `code2` spans";
535 let blocks = CodeBlockUtils::detect_code_blocks(content);
536 assert_eq!(blocks.len(), 0); }
538
539 #[test]
540 fn test_unclosed_code_span() {
541 let content = "Text with `unclosed code span";
543 let blocks = CodeBlockUtils::detect_code_blocks(content);
544 assert_eq!(blocks.len(), 0);
545
546 let content = "Text with ``one style` different close";
548 let blocks = CodeBlockUtils::detect_code_blocks(content);
549 assert_eq!(blocks.len(), 0);
550 }
551
552 #[test]
553 fn test_mixed_code_blocks_and_spans() {
554 let content = "Has `span1` text\n```\nblock\n```\nand `span2`";
555 let blocks = CodeBlockUtils::detect_code_blocks(content);
556 assert_eq!(blocks.len(), 1);
558
559 assert!(blocks.iter().any(|(s, e)| content[*s..*e].contains("block")));
561 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span1`"));
563 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span2`"));
564 }
565
566 #[test]
567 fn test_is_in_code_block_or_span() {
568 let blocks = vec![(10, 20), (30, 40), (50, 60)];
569
570 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 15));
572 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 35));
573 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 55));
574
575 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));
581 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 25));
582 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 65));
583 }
584
585 #[test]
586 fn test_empty_content() {
587 let blocks = CodeBlockUtils::detect_code_blocks("");
588 assert_eq!(blocks.len(), 0);
589 }
590
591 #[test]
592 fn test_code_block_at_start() {
593 let content = "```\ncode\n```\nText after";
594 let blocks = CodeBlockUtils::detect_code_blocks(content);
595 assert_eq!(blocks.len(), 1);
597 assert_eq!(blocks[0].0, 0); }
599
600 #[test]
601 fn test_code_block_at_end() {
602 let content = "Text before\n```\ncode\n```";
603 let blocks = CodeBlockUtils::detect_code_blocks(content);
604 assert_eq!(blocks.len(), 1);
606 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("code"));
608 assert!(fenced.is_some());
609 }
610
611 #[test]
612 fn test_nested_fence_markers() {
613 let content = "Text\n````\n```\nnested\n```\n````\nAfter";
615 let blocks = CodeBlockUtils::detect_code_blocks(content);
616 assert!(!blocks.is_empty());
618 let outer = blocks.iter().find(|(s, e)| content[*s..*e].contains("nested"));
620 assert!(outer.is_some());
621 }
622
623 #[test]
624 fn test_indented_code_with_blank_lines() {
625 let content = "Text\n\n line1\n\n line2\n\nAfter";
627 let blocks = CodeBlockUtils::detect_code_blocks(content);
628 assert!(!blocks.is_empty());
630 let all_content: String = blocks
632 .iter()
633 .map(|(s, e)| &content[*s..*e])
634 .collect::<Vec<_>>()
635 .join("");
636 assert!(all_content.contains("line1") || content[blocks[0].0..blocks[0].1].contains("line1"));
637 }
638
639 #[test]
640 fn test_code_span_with_spaces() {
641 let content = "Text ` code with spaces ` more";
643 let blocks = CodeBlockUtils::detect_code_blocks(content);
644 assert_eq!(blocks.len(), 0); }
646
647 #[test]
648 fn test_fenced_block_with_info_string() {
649 let content = "```rust,no_run,should_panic\ncode\n```";
651 let blocks = CodeBlockUtils::detect_code_blocks(content);
652 assert_eq!(blocks.len(), 1);
654 assert_eq!(blocks[0].0, 0);
655 }
656
657 #[test]
658 fn test_indented_fences_not_code_blocks() {
659 let content = "Text\n ```\n code\n ```\nAfter";
661 let blocks = CodeBlockUtils::detect_code_blocks(content);
662 assert_eq!(blocks.len(), 1);
664 }
665
666 #[test]
668 fn test_backticks_in_info_string_not_code_block() {
669 let content = "```something```\n\n```bash\n# comment\n```";
675 let blocks = CodeBlockUtils::detect_code_blocks(content);
676 assert_eq!(blocks.len(), 1);
678 assert!(content[blocks[0].0..blocks[0].1].contains("# comment"));
680 }
681
682 #[test]
683 fn test_issue_175_reproduction() {
684 let content = "```something```\n\n```bash\n# Have a parrot\necho \"🦜\"\n```";
686 let blocks = CodeBlockUtils::detect_code_blocks(content);
687 assert_eq!(blocks.len(), 1);
689 assert!(content[blocks[0].0..blocks[0].1].contains("Have a parrot"));
690 }
691
692 #[test]
693 fn test_tilde_fence_allows_tildes_in_info_string() {
694 let content = "~~~abc~~~\ncode content\n~~~";
697 let blocks = CodeBlockUtils::detect_code_blocks(content);
698 assert_eq!(blocks.len(), 1);
700 }
701
702 #[test]
703 fn test_nested_longer_fence_contains_shorter() {
704 let content = "````\n```\nnested content\n```\n````";
706 let blocks = CodeBlockUtils::detect_code_blocks(content);
707 assert_eq!(blocks.len(), 1);
708 assert!(content[blocks[0].0..blocks[0].1].contains("nested content"));
709 }
710
711 #[test]
712 fn test_mixed_fence_types() {
713 let content = "~~~\n```\nmixed content\n~~~";
715 let blocks = CodeBlockUtils::detect_code_blocks(content);
716 assert_eq!(blocks.len(), 1);
717 assert!(content[blocks[0].0..blocks[0].1].contains("mixed content"));
718 }
719
720 #[test]
721 fn test_indented_code_in_list_issue_276() {
722 let content = r#"1. First item
7242. Second item with code:
725
726 # This is a code block in a list
727 print("Hello, world!")
728
7294. Third item"#;
730
731 let blocks = CodeBlockUtils::detect_code_blocks(content);
732 assert!(!blocks.is_empty(), "Should detect indented code block inside list");
734
735 let all_content: String = blocks
737 .iter()
738 .map(|(s, e)| &content[*s..*e])
739 .collect::<Vec<_>>()
740 .join("");
741 assert!(
742 all_content.contains("code block in a list") || all_content.contains("print"),
743 "Detected block should contain the code content: {all_content:?}"
744 );
745 }
746
747 #[test]
748 fn test_detect_markdown_code_blocks() {
749 let content = r#"# Example
750
751```markdown
752# Heading
753Content here
754```
755
756```md
757Another heading
758More content
759```
760
761```rust
762// Not markdown
763fn main() {}
764```
765"#;
766
767 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
768
769 assert_eq!(
771 blocks.len(),
772 2,
773 "Should detect exactly 2 markdown blocks, got {blocks:?}"
774 );
775
776 let first = &blocks[0];
778 let first_content = &content[first.content_start..first.content_end];
779 assert!(
780 first_content.contains("# Heading"),
781 "First block should contain '# Heading', got: {first_content:?}"
782 );
783
784 let second = &blocks[1];
786 let second_content = &content[second.content_start..second.content_end];
787 assert!(
788 second_content.contains("Another heading"),
789 "Second block should contain 'Another heading', got: {second_content:?}"
790 );
791 }
792
793 #[test]
794 fn test_detect_markdown_code_blocks_empty() {
795 let content = "# Just a heading\n\nNo code blocks here\n";
796 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
797 assert_eq!(blocks.len(), 0);
798 }
799
800 #[test]
801 fn test_detect_markdown_code_blocks_case_insensitive() {
802 let content = "```MARKDOWN\nContent\n```\n";
803 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
804 assert_eq!(blocks.len(), 1);
805 }
806
807 #[test]
808 fn test_detect_markdown_code_blocks_at_eof_no_trailing_newline() {
809 let content = "# Doc\n\n```markdown\nContent\n```";
811 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
812 assert_eq!(blocks.len(), 1);
813 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
815 assert!(block_content.contains("Content"));
816 }
817
818 #[test]
819 fn test_detect_markdown_code_blocks_single_line_content() {
820 let content = "```markdown\nX\n```\n";
822 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
823 assert_eq!(blocks.len(), 1);
824 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
825 assert_eq!(block_content, "X");
826 }
827
828 #[test]
829 fn test_detect_markdown_code_blocks_empty_content() {
830 let content = "```markdown\n```\n";
832 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
833 if !blocks.is_empty() {
836 assert!(blocks[0].content_start <= blocks[0].content_end);
838 }
839 }
840
841 #[test]
842 fn test_detect_markdown_code_blocks_validates_ranges() {
843 let test_cases = [
845 "", "```markdown", "```markdown\n", "```\n```", "```markdown\n```", " ```markdown\n X\n ```", ];
852
853 for content in test_cases {
854 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
856 for block in &blocks {
858 assert!(
859 block.content_start <= block.content_end,
860 "Invalid range in content: {content:?}"
861 );
862 assert!(
863 block.content_end <= content.len(),
864 "Range exceeds content length in: {content:?}"
865 );
866 }
867 }
868 }
869
870 #[test]
873 fn test_is_in_code_block_empty_blocks() {
874 assert!(!CodeBlockUtils::is_in_code_block(&[], 0));
875 assert!(!CodeBlockUtils::is_in_code_block(&[], 100));
876 assert!(!CodeBlockUtils::is_in_code_block(&[], usize::MAX));
877 }
878
879 #[test]
880 fn test_is_in_code_block_single_range() {
881 let blocks = [(10, 20)];
882 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
883 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 9));
884 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
885 assert!(CodeBlockUtils::is_in_code_block(&blocks, 15));
886 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
887 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 20));
889 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 21));
890 }
891
892 #[test]
893 fn test_is_in_code_block_multiple_ranges() {
894 let blocks = [(5, 10), (20, 30), (50, 60)];
895 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
897 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
898 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
900 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
901 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 10));
903 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 15));
904 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 19));
905 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
907 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
908 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
910 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 49));
911 assert!(CodeBlockUtils::is_in_code_block(&blocks, 50));
913 assert!(CodeBlockUtils::is_in_code_block(&blocks, 59));
914 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 60));
916 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 1000));
917 }
918
919 #[test]
920 fn test_is_in_code_block_adjacent_ranges() {
921 let blocks = [(0, 10), (10, 20), (20, 30)];
923 assert!(CodeBlockUtils::is_in_code_block(&blocks, 0));
924 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
925 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
926 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
927 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
928 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
929 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
930 }
931
932 #[test]
933 fn test_is_in_code_block_single_byte_range() {
934 let blocks = [(5, 6)];
935 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
936 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
937 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 6));
938 }
939
940 #[test]
941 fn test_is_in_code_block_matches_linear_scan() {
942 let content = "# Heading\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nSome text\n\n```\nmore code\n```\n\nEnd\n";
945 let blocks = CodeBlockUtils::detect_code_blocks(content);
946
947 for pos in 0..content.len() {
948 let binary = CodeBlockUtils::is_in_code_block(&blocks, pos);
949 let linear = blocks.iter().any(|&(s, e)| pos >= s && pos < e);
950 assert_eq!(
951 binary, linear,
952 "Mismatch at pos {pos}: binary={binary}, linear={linear}, blocks={blocks:?}"
953 );
954 }
955 }
956
957 #[test]
958 fn test_is_in_code_block_at_range_boundaries() {
959 let blocks = [(100, 200), (300, 400), (500, 600)];
961 for &(start, end) in &blocks {
962 assert!(
963 !CodeBlockUtils::is_in_code_block(&blocks, start - 1),
964 "pos={} should be outside",
965 start - 1
966 );
967 assert!(
968 CodeBlockUtils::is_in_code_block(&blocks, start),
969 "pos={start} should be inside"
970 );
971 assert!(
972 CodeBlockUtils::is_in_code_block(&blocks, end - 1),
973 "pos={} should be inside",
974 end - 1
975 );
976 assert!(
977 !CodeBlockUtils::is_in_code_block(&blocks, end),
978 "pos={end} should be outside"
979 );
980 }
981 }
982}