1use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
12
13pub type CodeRanges = (Vec<(usize, usize)>, Vec<(usize, usize)>);
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum CodeBlockContext {
19 Standalone,
21 Indented,
23 Adjacent,
25}
26
27pub struct CodeBlockUtils;
29
30impl CodeBlockUtils {
31 pub fn detect_code_blocks(content: &str) -> Vec<(usize, usize)> {
41 let (blocks, _) = Self::detect_code_blocks_and_spans(content);
42 blocks
43 }
44
45 pub fn detect_code_blocks_and_spans(content: &str) -> CodeRanges {
47 let mut blocks = Vec::new();
48 let mut spans = Vec::new();
49 let mut code_block_start: Option<usize> = None;
50
51 let options = Options::all();
53 let parser = Parser::new_ext(content, options).into_offset_iter();
54
55 for (event, range) in parser {
56 match event {
57 Event::Start(Tag::CodeBlock(_)) => {
58 code_block_start = Some(range.start);
60 }
61 Event::End(TagEnd::CodeBlock) => {
62 if let Some(start) = code_block_start.take() {
64 blocks.push((start, range.end));
65 }
66 }
67 Event::Code(_) => {
68 spans.push((range.start, range.end));
69 }
70 _ => {}
71 }
72 }
73
74 if let Some(start) = code_block_start {
77 blocks.push((start, content.len()));
78 }
79
80 blocks.sort_by_key(|&(start, _)| start);
82 (blocks, spans)
83 }
84
85 pub fn is_in_code_block_or_span(blocks: &[(usize, usize)], pos: usize) -> bool {
87 Self::is_in_code_block(blocks, pos)
88 }
89
90 pub fn is_in_code_block(blocks: &[(usize, usize)], pos: usize) -> bool {
96 let idx = blocks.partition_point(|&(start, _)| start <= pos);
98 idx > 0 && pos < blocks[idx - 1].1
101 }
102
103 pub fn analyze_code_block_context(
106 lines: &[crate::lint_context::LineInfo],
107 line_idx: usize,
108 min_continuation_indent: usize,
109 ) -> CodeBlockContext {
110 if let Some(line_info) = lines.get(line_idx) {
111 if line_info.indent >= min_continuation_indent {
113 return CodeBlockContext::Indented;
114 }
115
116 let (prev_blanks, next_blanks) = Self::count_surrounding_blank_lines(lines, line_idx);
118
119 if prev_blanks > 0 || next_blanks > 0 {
122 return CodeBlockContext::Standalone;
123 }
124
125 CodeBlockContext::Adjacent
127 } else {
128 CodeBlockContext::Adjacent
130 }
131 }
132
133 fn count_surrounding_blank_lines(lines: &[crate::lint_context::LineInfo], line_idx: usize) -> (usize, usize) {
135 let mut prev_blanks = 0;
136 let mut next_blanks = 0;
137
138 for i in (0..line_idx).rev() {
140 if let Some(line) = lines.get(i) {
141 if line.is_blank {
142 prev_blanks += 1;
143 } else {
144 break;
145 }
146 } else {
147 break;
148 }
149 }
150
151 for i in (line_idx + 1)..lines.len() {
153 if let Some(line) = lines.get(i) {
154 if line.is_blank {
155 next_blanks += 1;
156 } else {
157 break;
158 }
159 } else {
160 break;
161 }
162 }
163
164 (prev_blanks, next_blanks)
165 }
166
167 pub fn calculate_min_continuation_indent(
170 content: &str,
171 lines: &[crate::lint_context::LineInfo],
172 current_line_idx: usize,
173 ) -> usize {
174 for i in (0..current_line_idx).rev() {
176 if let Some(line_info) = lines.get(i) {
177 if let Some(list_item) = &line_info.list_item {
178 return if list_item.is_ordered {
180 list_item.marker_column + list_item.marker.len() + 1 } else {
182 list_item.marker_column + 2 };
184 }
185
186 if line_info.heading.is_some() || Self::is_structural_separator(line_info.content(content)) {
188 break;
189 }
190 }
191 }
192
193 0 }
195
196 fn is_structural_separator(content: &str) -> bool {
198 let trimmed = content.trim();
199 trimmed.starts_with("---")
200 || trimmed.starts_with("***")
201 || trimmed.starts_with("___")
202 || crate::utils::skip_context::is_table_line(trimmed)
203 || trimmed.starts_with(">") }
205
206 pub fn detect_markdown_code_blocks(content: &str) -> Vec<MarkdownCodeBlock> {
214 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
215
216 let mut blocks = Vec::new();
217 let mut current_block: Option<MarkdownCodeBlockBuilder> = None;
218
219 let options = Options::all();
220 let parser = Parser::new_ext(content, options).into_offset_iter();
221
222 for (event, range) in parser {
223 match event {
224 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
225 let language = info.split_whitespace().next().unwrap_or("");
227 if language.eq_ignore_ascii_case("markdown") || language.eq_ignore_ascii_case("md") {
228 let block_start = range.start;
230 let content_start = content[block_start..]
231 .find('\n')
232 .map(|i| block_start + i + 1)
233 .unwrap_or(content.len());
234
235 current_block = Some(MarkdownCodeBlockBuilder { content_start });
236 }
237 }
238 Event::End(TagEnd::CodeBlock) => {
239 if let Some(builder) = current_block.take() {
240 let block_end = range.end;
242
243 if builder.content_start > block_end || builder.content_start > content.len() {
245 continue;
246 }
247
248 let search_range = &content[builder.content_start..block_end.min(content.len())];
249 let content_end = search_range
250 .rfind('\n')
251 .map(|i| builder.content_start + i)
252 .unwrap_or(builder.content_start);
253
254 if content_end >= builder.content_start {
256 blocks.push(MarkdownCodeBlock {
257 content_start: builder.content_start,
258 content_end,
259 });
260 }
261 }
262 }
263 _ => {}
264 }
265 }
266
267 blocks
268 }
269}
270
271#[derive(Debug, Clone)]
273pub struct MarkdownCodeBlock {
274 pub content_start: usize,
276 pub content_end: usize,
278}
279
280struct MarkdownCodeBlockBuilder {
282 content_start: usize,
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_detect_fenced_code_blocks() {
291 let content = "Some text\n```\ncode here\n```\nMore text";
296 let blocks = CodeBlockUtils::detect_code_blocks(content);
297 assert_eq!(blocks.len(), 1);
299
300 let fenced_block = blocks
302 .iter()
303 .find(|(start, end)| end - start > 10 && content[*start..*end].contains("code here"));
304 assert!(fenced_block.is_some());
305
306 let content = "Some text\n~~~\ncode here\n~~~\nMore text";
308 let blocks = CodeBlockUtils::detect_code_blocks(content);
309 assert_eq!(blocks.len(), 1);
310 assert_eq!(&content[blocks[0].0..blocks[0].1], "~~~\ncode here\n~~~");
311
312 let content = "Text\n```\ncode1\n```\nMiddle\n~~~\ncode2\n~~~\nEnd";
314 let blocks = CodeBlockUtils::detect_code_blocks(content);
315 assert_eq!(blocks.len(), 2);
317 }
318
319 #[test]
320 fn test_detect_code_blocks_with_language() {
321 let content = "Text\n```rust\nfn main() {}\n```\nMore";
323 let blocks = CodeBlockUtils::detect_code_blocks(content);
324 assert_eq!(blocks.len(), 1);
326 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("fn main"));
328 assert!(fenced.is_some());
329 }
330
331 #[test]
332 fn test_unclosed_code_block() {
333 let content = "Text\n```\ncode here\nno closing fence";
335 let blocks = CodeBlockUtils::detect_code_blocks(content);
336 assert_eq!(blocks.len(), 1);
337 assert_eq!(blocks[0].1, content.len());
338 }
339
340 #[test]
341 fn test_indented_code_blocks() {
342 let content = "Paragraph\n\n code line 1\n code line 2\n\nMore text";
344 let blocks = CodeBlockUtils::detect_code_blocks(content);
345 assert_eq!(blocks.len(), 1);
346 assert!(content[blocks[0].0..blocks[0].1].contains("code line 1"));
347 assert!(content[blocks[0].0..blocks[0].1].contains("code line 2"));
348
349 let content = "Paragraph\n\n\tcode with tab\n\tanother line\n\nText";
351 let blocks = CodeBlockUtils::detect_code_blocks(content);
352 assert_eq!(blocks.len(), 1);
353 }
354
355 #[test]
356 fn test_indented_code_requires_blank_line() {
357 let content = "Paragraph\n indented but not code\nMore text";
359 let blocks = CodeBlockUtils::detect_code_blocks(content);
360 assert_eq!(blocks.len(), 0);
361
362 let content = "Paragraph\n\n now it's code\nMore text";
364 let blocks = CodeBlockUtils::detect_code_blocks(content);
365 assert_eq!(blocks.len(), 1);
366 }
367
368 #[test]
369 fn test_indented_content_with_list_markers_is_code_block() {
370 let content = "List:\n\n - Item 1\n - Item 2\n * Item 3\n + Item 4";
375 let blocks = CodeBlockUtils::detect_code_blocks(content);
376 assert_eq!(blocks.len(), 1); let content = "List:\n\n 1. First\n 2. Second";
380 let blocks = CodeBlockUtils::detect_code_blocks(content);
381 assert_eq!(blocks.len(), 1); }
383
384 #[test]
385 fn test_actual_list_items_not_code_blocks() {
386 let content = "- Item 1\n- Item 2\n* Item 3";
388 let blocks = CodeBlockUtils::detect_code_blocks(content);
389 assert_eq!(blocks.len(), 0);
390
391 let content = "- Item 1\n - Nested item\n- Item 2";
393 let blocks = CodeBlockUtils::detect_code_blocks(content);
394 assert_eq!(blocks.len(), 0);
395 }
396
397 #[test]
398 fn test_inline_code_spans_not_detected() {
399 let content = "Text with `inline code` here";
401 let blocks = CodeBlockUtils::detect_code_blocks(content);
402 assert_eq!(blocks.len(), 0); let content = "Text with ``code with ` backtick`` here";
406 let blocks = CodeBlockUtils::detect_code_blocks(content);
407 assert_eq!(blocks.len(), 0); let content = "Has `code1` and `code2` spans";
411 let blocks = CodeBlockUtils::detect_code_blocks(content);
412 assert_eq!(blocks.len(), 0); }
414
415 #[test]
416 fn test_unclosed_code_span() {
417 let content = "Text with `unclosed code span";
419 let blocks = CodeBlockUtils::detect_code_blocks(content);
420 assert_eq!(blocks.len(), 0);
421
422 let content = "Text with ``one style` different close";
424 let blocks = CodeBlockUtils::detect_code_blocks(content);
425 assert_eq!(blocks.len(), 0);
426 }
427
428 #[test]
429 fn test_mixed_code_blocks_and_spans() {
430 let content = "Has `span1` text\n```\nblock\n```\nand `span2`";
431 let blocks = CodeBlockUtils::detect_code_blocks(content);
432 assert_eq!(blocks.len(), 1);
434
435 assert!(blocks.iter().any(|(s, e)| content[*s..*e].contains("block")));
437 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span1`"));
439 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span2`"));
440 }
441
442 #[test]
443 fn test_is_in_code_block_or_span() {
444 let blocks = vec![(10, 20), (30, 40), (50, 60)];
445
446 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 15));
448 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 35));
449 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 55));
450
451 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));
457 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 25));
458 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 65));
459 }
460
461 #[test]
462 fn test_empty_content() {
463 let blocks = CodeBlockUtils::detect_code_blocks("");
464 assert_eq!(blocks.len(), 0);
465 }
466
467 #[test]
468 fn test_code_block_at_start() {
469 let content = "```\ncode\n```\nText after";
470 let blocks = CodeBlockUtils::detect_code_blocks(content);
471 assert_eq!(blocks.len(), 1);
473 assert_eq!(blocks[0].0, 0); }
475
476 #[test]
477 fn test_code_block_at_end() {
478 let content = "Text before\n```\ncode\n```";
479 let blocks = CodeBlockUtils::detect_code_blocks(content);
480 assert_eq!(blocks.len(), 1);
482 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("code"));
484 assert!(fenced.is_some());
485 }
486
487 #[test]
488 fn test_nested_fence_markers() {
489 let content = "Text\n````\n```\nnested\n```\n````\nAfter";
491 let blocks = CodeBlockUtils::detect_code_blocks(content);
492 assert!(!blocks.is_empty());
494 let outer = blocks.iter().find(|(s, e)| content[*s..*e].contains("nested"));
496 assert!(outer.is_some());
497 }
498
499 #[test]
500 fn test_indented_code_with_blank_lines() {
501 let content = "Text\n\n line1\n\n line2\n\nAfter";
503 let blocks = CodeBlockUtils::detect_code_blocks(content);
504 assert!(!blocks.is_empty());
506 let all_content: String = blocks
508 .iter()
509 .map(|(s, e)| &content[*s..*e])
510 .collect::<Vec<_>>()
511 .join("");
512 assert!(all_content.contains("line1") || content[blocks[0].0..blocks[0].1].contains("line1"));
513 }
514
515 #[test]
516 fn test_code_span_with_spaces() {
517 let content = "Text ` code with spaces ` more";
519 let blocks = CodeBlockUtils::detect_code_blocks(content);
520 assert_eq!(blocks.len(), 0); }
522
523 #[test]
524 fn test_fenced_block_with_info_string() {
525 let content = "```rust,no_run,should_panic\ncode\n```";
527 let blocks = CodeBlockUtils::detect_code_blocks(content);
528 assert_eq!(blocks.len(), 1);
530 assert_eq!(blocks[0].0, 0);
531 }
532
533 #[test]
534 fn test_indented_fences_not_code_blocks() {
535 let content = "Text\n ```\n code\n ```\nAfter";
537 let blocks = CodeBlockUtils::detect_code_blocks(content);
538 assert_eq!(blocks.len(), 1);
540 }
541
542 #[test]
544 fn test_backticks_in_info_string_not_code_block() {
545 let content = "```something```\n\n```bash\n# comment\n```";
551 let blocks = CodeBlockUtils::detect_code_blocks(content);
552 assert_eq!(blocks.len(), 1);
554 assert!(content[blocks[0].0..blocks[0].1].contains("# comment"));
556 }
557
558 #[test]
559 fn test_issue_175_reproduction() {
560 let content = "```something```\n\n```bash\n# Have a parrot\necho \"🦜\"\n```";
562 let blocks = CodeBlockUtils::detect_code_blocks(content);
563 assert_eq!(blocks.len(), 1);
565 assert!(content[blocks[0].0..blocks[0].1].contains("Have a parrot"));
566 }
567
568 #[test]
569 fn test_tilde_fence_allows_tildes_in_info_string() {
570 let content = "~~~abc~~~\ncode content\n~~~";
573 let blocks = CodeBlockUtils::detect_code_blocks(content);
574 assert_eq!(blocks.len(), 1);
576 }
577
578 #[test]
579 fn test_nested_longer_fence_contains_shorter() {
580 let content = "````\n```\nnested content\n```\n````";
582 let blocks = CodeBlockUtils::detect_code_blocks(content);
583 assert_eq!(blocks.len(), 1);
584 assert!(content[blocks[0].0..blocks[0].1].contains("nested content"));
585 }
586
587 #[test]
588 fn test_mixed_fence_types() {
589 let content = "~~~\n```\nmixed content\n~~~";
591 let blocks = CodeBlockUtils::detect_code_blocks(content);
592 assert_eq!(blocks.len(), 1);
593 assert!(content[blocks[0].0..blocks[0].1].contains("mixed content"));
594 }
595
596 #[test]
597 fn test_indented_code_in_list_issue_276() {
598 let content = r#"1. First item
6002. Second item with code:
601
602 # This is a code block in a list
603 print("Hello, world!")
604
6054. Third item"#;
606
607 let blocks = CodeBlockUtils::detect_code_blocks(content);
608 assert!(!blocks.is_empty(), "Should detect indented code block inside list");
610
611 let all_content: String = blocks
613 .iter()
614 .map(|(s, e)| &content[*s..*e])
615 .collect::<Vec<_>>()
616 .join("");
617 assert!(
618 all_content.contains("code block in a list") || all_content.contains("print"),
619 "Detected block should contain the code content: {all_content:?}"
620 );
621 }
622
623 #[test]
624 fn test_detect_markdown_code_blocks() {
625 let content = r#"# Example
626
627```markdown
628# Heading
629Content here
630```
631
632```md
633Another heading
634More content
635```
636
637```rust
638// Not markdown
639fn main() {}
640```
641"#;
642
643 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
644
645 assert_eq!(
647 blocks.len(),
648 2,
649 "Should detect exactly 2 markdown blocks, got {blocks:?}"
650 );
651
652 let first = &blocks[0];
654 let first_content = &content[first.content_start..first.content_end];
655 assert!(
656 first_content.contains("# Heading"),
657 "First block should contain '# Heading', got: {first_content:?}"
658 );
659
660 let second = &blocks[1];
662 let second_content = &content[second.content_start..second.content_end];
663 assert!(
664 second_content.contains("Another heading"),
665 "Second block should contain 'Another heading', got: {second_content:?}"
666 );
667 }
668
669 #[test]
670 fn test_detect_markdown_code_blocks_empty() {
671 let content = "# Just a heading\n\nNo code blocks here\n";
672 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
673 assert_eq!(blocks.len(), 0);
674 }
675
676 #[test]
677 fn test_detect_markdown_code_blocks_case_insensitive() {
678 let content = "```MARKDOWN\nContent\n```\n";
679 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
680 assert_eq!(blocks.len(), 1);
681 }
682
683 #[test]
684 fn test_detect_markdown_code_blocks_at_eof_no_trailing_newline() {
685 let content = "# Doc\n\n```markdown\nContent\n```";
687 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
688 assert_eq!(blocks.len(), 1);
689 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
691 assert!(block_content.contains("Content"));
692 }
693
694 #[test]
695 fn test_detect_markdown_code_blocks_single_line_content() {
696 let content = "```markdown\nX\n```\n";
698 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
699 assert_eq!(blocks.len(), 1);
700 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
701 assert_eq!(block_content, "X");
702 }
703
704 #[test]
705 fn test_detect_markdown_code_blocks_empty_content() {
706 let content = "```markdown\n```\n";
708 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
709 if !blocks.is_empty() {
712 assert!(blocks[0].content_start <= blocks[0].content_end);
714 }
715 }
716
717 #[test]
718 fn test_detect_markdown_code_blocks_validates_ranges() {
719 let test_cases = [
721 "", "```markdown", "```markdown\n", "```\n```", "```markdown\n```", " ```markdown\n X\n ```", ];
728
729 for content in test_cases {
730 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
732 for block in &blocks {
734 assert!(
735 block.content_start <= block.content_end,
736 "Invalid range in content: {content:?}"
737 );
738 assert!(
739 block.content_end <= content.len(),
740 "Range exceeds content length in: {content:?}"
741 );
742 }
743 }
744 }
745
746 #[test]
749 fn test_is_in_code_block_empty_blocks() {
750 assert!(!CodeBlockUtils::is_in_code_block(&[], 0));
751 assert!(!CodeBlockUtils::is_in_code_block(&[], 100));
752 assert!(!CodeBlockUtils::is_in_code_block(&[], usize::MAX));
753 }
754
755 #[test]
756 fn test_is_in_code_block_single_range() {
757 let blocks = [(10, 20)];
758 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
759 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 9));
760 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
761 assert!(CodeBlockUtils::is_in_code_block(&blocks, 15));
762 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
763 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 20));
765 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 21));
766 }
767
768 #[test]
769 fn test_is_in_code_block_multiple_ranges() {
770 let blocks = [(5, 10), (20, 30), (50, 60)];
771 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
773 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
774 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
776 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
777 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 10));
779 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 15));
780 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 19));
781 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
783 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
784 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
786 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 49));
787 assert!(CodeBlockUtils::is_in_code_block(&blocks, 50));
789 assert!(CodeBlockUtils::is_in_code_block(&blocks, 59));
790 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 60));
792 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 1000));
793 }
794
795 #[test]
796 fn test_is_in_code_block_adjacent_ranges() {
797 let blocks = [(0, 10), (10, 20), (20, 30)];
799 assert!(CodeBlockUtils::is_in_code_block(&blocks, 0));
800 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
801 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
802 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
803 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
804 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
805 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
806 }
807
808 #[test]
809 fn test_is_in_code_block_single_byte_range() {
810 let blocks = [(5, 6)];
811 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
812 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
813 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 6));
814 }
815
816 #[test]
817 fn test_is_in_code_block_matches_linear_scan() {
818 let content = "# Heading\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nSome text\n\n```\nmore code\n```\n\nEnd\n";
821 let blocks = CodeBlockUtils::detect_code_blocks(content);
822
823 for pos in 0..content.len() {
824 let binary = CodeBlockUtils::is_in_code_block(&blocks, pos);
825 let linear = blocks.iter().any(|&(s, e)| pos >= s && pos < e);
826 assert_eq!(
827 binary, linear,
828 "Mismatch at pos {pos}: binary={binary}, linear={linear}, blocks={blocks:?}"
829 );
830 }
831 }
832
833 #[test]
834 fn test_is_in_code_block_at_range_boundaries() {
835 let blocks = [(100, 200), (300, 400), (500, 600)];
837 for &(start, end) in &blocks {
838 assert!(
839 !CodeBlockUtils::is_in_code_block(&blocks, start - 1),
840 "pos={} should be outside",
841 start - 1
842 );
843 assert!(
844 CodeBlockUtils::is_in_code_block(&blocks, start),
845 "pos={start} should be inside"
846 );
847 assert!(
848 CodeBlockUtils::is_in_code_block(&blocks, end - 1),
849 "pos={} should be inside",
850 end - 1
851 );
852 assert!(
853 !CodeBlockUtils::is_in_code_block(&blocks, end),
854 "pos={end} should be outside"
855 );
856 }
857 }
858}