1use pulldown_cmark::{CodeBlockKind, Event, Options, 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
39#[derive(Debug, Clone)]
46pub struct DefinitionTextDetail {
47 pub start: usize,
49 pub end: usize,
51 pub definition_start: usize,
54}
55
56pub type LineToListMap = std::collections::HashMap<usize, usize>;
58pub type ListStartValues = std::collections::HashMap<usize, u64>;
60
61pub struct ParseResult {
63 pub code_blocks: Vec<(usize, usize)>,
65 pub code_spans: Vec<(usize, usize)>,
67 pub code_block_details: Vec<CodeBlockDetail>,
69 pub strong_spans: Vec<StrongSpanDetail>,
71 pub line_to_list: LineToListMap,
73 pub list_start_values: ListStartValues,
75 pub html_blocks: Vec<(usize, usize)>,
82 pub definition_items: Vec<(usize, usize)>,
87 pub definition_terms: Vec<(usize, usize)>,
89 pub definition_texts: Vec<DefinitionTextDetail>,
91 pub colon_definitions: Vec<ColonDefinitionDetail>,
97}
98
99#[derive(Debug, Clone)]
102pub struct ColonDefinitionDetail {
103 pub start: usize,
105 pub end: usize,
107 pub definition_start: usize,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum CodeBlockContext {
114 Standalone,
116 Indented,
118 Adjacent,
120}
121
122fn is_inline_tag(tag: &Tag) -> bool {
124 matches!(
125 tag,
126 Tag::Emphasis
127 | Tag::Strong
128 | Tag::Strikethrough
129 | Tag::Superscript
130 | Tag::Subscript
131 | Tag::Link { .. }
132 | Tag::Image { .. }
133 )
134}
135
136fn is_inline_tag_end(tag_end: TagEnd) -> bool {
138 matches!(
139 tag_end,
140 TagEnd::Emphasis
141 | TagEnd::Strong
142 | TagEnd::Strikethrough
143 | TagEnd::Superscript
144 | TagEnd::Subscript
145 | TagEnd::Link
146 | TagEnd::Image
147 )
148}
149
150fn opens_definition(content: &str, start: usize) -> bool {
158 content[start..]
159 .trim_start_matches([' ', '\t'])
160 .strip_prefix(':')
161 .is_some_and(|rest| rest.is_empty() || rest.starts_with([' ', '\t', '\n', '\r']))
162}
163
164fn inline_ranges(content: &str, options: Options) -> Vec<(usize, usize)> {
168 Parser::new_ext(content, options)
169 .into_offset_iter()
170 .filter_map(|(event, range)| match event {
171 Event::Start(tag) if is_inline_tag(&tag) => Some((range.start, range.end)),
172 Event::Code(_) | Event::InlineHtml(_) | Event::InlineMath(_) | Event::DisplayMath(_) => {
173 Some((range.start, range.end))
174 }
175 _ => None,
176 })
177 .collect()
178}
179
180struct InlineRangeCursor<'a> {
185 ranges: &'a [(usize, usize)],
186 next: usize,
187 active: Vec<(usize, usize)>,
188}
189
190impl<'a> InlineRangeCursor<'a> {
191 fn new(ranges: &'a [(usize, usize)]) -> Self {
192 Self {
193 ranges,
194 next: 0,
195 active: Vec::new(),
196 }
197 }
198
199 fn overlapping(&mut self, detail: &ColonDefinitionDetail) -> impl Iterator<Item = (usize, usize)> + '_ {
200 while self.next < self.ranges.len() && self.ranges[self.next].0 <= detail.start {
201 self.active.push(self.ranges[self.next]);
202 self.next += 1;
203 }
204 self.active.retain(|&(_, end)| end > detail.start);
205 let end = detail.end;
206 self.active.iter().copied().chain(
207 self.ranges[self.next..]
208 .iter()
209 .take_while(move |&&(start, _)| start < end)
210 .copied(),
211 )
212 }
213}
214
215pub struct CodeBlockUtils;
217
218impl CodeBlockUtils {
219 pub fn detect_code_blocks(content: &str) -> Vec<(usize, usize)> {
229 Self::detect_code_blocks_and_spans(content).code_blocks
230 }
231
232 pub fn detect_code_blocks_and_spans(content: &str) -> ParseResult {
235 let mut blocks = Vec::new();
236 let mut spans = Vec::new();
237 let mut details = Vec::new();
238 let mut strong_spans = Vec::new();
239 let mut html_blocks = Vec::new();
240 let mut code_block_start: Option<(usize, bool, String)> = None;
241
242 let mut definition_items: Vec<(usize, usize)> = Vec::new();
253 let mut pending_terms: Vec<(usize, usize)> = Vec::new();
254 let mut definition_terms = Vec::new();
255 let mut definition_texts = Vec::new();
256 let mut colon_definitions = Vec::new();
257 let mut block_stack: Vec<Option<usize>> = Vec::new();
258 let mut definition_paragraph: Option<(usize, usize)> = None;
259 let mut tight_run: Option<DefinitionTextDetail> = None;
260
261 let mut line_to_list = LineToListMap::new();
263 let mut list_start_values = ListStartValues::new();
264 let mut list_stack: Vec<(usize, bool, u64)> = Vec::new(); let mut next_list_id: usize = 0;
266
267 let line_starts: Vec<usize> = std::iter::once(0)
269 .chain(content.match_indices('\n').map(|(i, _)| i + 1))
270 .collect();
271
272 let byte_to_line = |byte_offset: usize| -> usize { line_starts.partition_point(|&start| start <= byte_offset) };
273
274 let options = rumdl_parser_options();
275 let parser = Parser::new_ext(content, options).into_offset_iter();
276
277 for (event, range) in parser {
278 match &event {
279 Event::Start(tag) if !is_inline_tag(tag) => {
280 definition_texts.extend(tight_run.take());
281 if let (Tag::Paragraph, Some(Some(definition_start))) = (tag, block_stack.last()) {
282 definition_paragraph = Some((range.start, *definition_start));
283 }
284 let mut definition_start = None;
285 match tag {
286 Tag::DefinitionList => pending_terms.clear(),
287 Tag::DefinitionListTitle => pending_terms.push((range.start, range.end)),
288 Tag::DefinitionListDefinition => {
289 let item_start = pending_terms.first().map_or(range.start, |&(start, _)| start);
292 let item_end = range.start
293 + content[range.clone()]
294 .trim_end_matches([' ', '\t', '\n', '\r', '>'])
295 .len();
296 if opens_definition(content, range.start) {
297 definition_items.push((item_start, item_end));
298 definition_terms.append(&mut pending_terms);
299 definition_start = Some(range.start);
300 } else {
301 colon_definitions.push(ColonDefinitionDetail {
302 start: item_start,
303 end: item_end,
304 definition_start: range.start,
305 });
306 pending_terms.clear();
307 }
308 }
309 _ => {}
310 }
311 block_stack.push(definition_start);
312 }
313 Event::End(tag_end) if !is_inline_tag_end(*tag_end) => {
314 definition_texts.extend(tight_run.take());
315 if let (TagEnd::Paragraph, Some((start, definition_start))) = (tag_end, definition_paragraph.take())
316 {
317 definition_texts.push(DefinitionTextDetail {
318 start,
319 end: range.end,
320 definition_start,
321 });
322 }
323 if let TagEnd::DefinitionList = tag_end {
324 pending_terms.clear();
325 }
326 block_stack.pop();
327 }
328 _ => {
331 if let Some(Some(definition_start)) = block_stack.last() {
332 match &mut tight_run {
333 Some(run) => run.end = run.end.max(range.end),
334 None => {
335 tight_run = Some(DefinitionTextDetail {
336 start: range.start,
337 end: range.end,
338 definition_start: *definition_start,
339 });
340 }
341 }
342 }
343 }
344 }
345 match event {
346 Event::Start(Tag::CodeBlock(kind)) => {
347 let (is_fenced, info_string) = match &kind {
348 CodeBlockKind::Fenced(info) => (true, info.to_string()),
349 CodeBlockKind::Indented => (false, String::new()),
350 };
351 code_block_start = Some((range.start, is_fenced, info_string));
352 }
353 Event::End(TagEnd::CodeBlock) => {
354 if let Some((start, is_fenced, info_string)) = code_block_start.take() {
355 blocks.push((start, range.end));
356 details.push(CodeBlockDetail {
357 start,
358 end: range.end,
359 is_fenced,
360 info_string,
361 });
362 }
363 }
364 Event::Start(Tag::Strong) => {
365 if range.start + 2 <= content.len() {
366 let is_asterisk = &content[range.start..range.start + 2] == "**";
367 strong_spans.push(StrongSpanDetail {
368 start: range.start,
369 end: range.end,
370 is_asterisk,
371 });
372 }
373 }
374 Event::Start(Tag::List(start_num)) => {
375 let is_ordered = start_num.is_some();
376 let start_value = start_num.unwrap_or(1);
377 list_stack.push((next_list_id, is_ordered, start_value));
378 if is_ordered {
379 list_start_values.insert(next_list_id, start_value);
380 }
381 next_list_id += 1;
382 }
383 Event::End(TagEnd::List(_)) => {
384 list_stack.pop();
385 }
386 Event::Start(Tag::Item) => {
387 if let Some(&(list_id, is_ordered, _)) = list_stack.last()
388 && is_ordered
389 {
390 let line_num = byte_to_line(range.start);
391 line_to_list.insert(line_num, list_id);
392 }
393 }
394 Event::Start(Tag::HtmlBlock) => {
395 html_blocks.push((range.start, range.end));
397 }
398 Event::Code(_) => {
399 spans.push((range.start, range.end));
400 }
401 _ => {}
402 }
403 }
404
405 if let Some((start, is_fenced, info_string)) = code_block_start {
408 blocks.push((start, content.len()));
409 details.push(CodeBlockDetail {
410 start,
411 end: content.len(),
412 is_fenced,
413 info_string,
414 });
415 }
416
417 if !colon_definitions.is_empty() {
420 let as_definitions = inline_ranges(content, options);
421 let as_text = inline_ranges(content, options - Options::ENABLE_DEFINITION_LIST);
422 let mut definition_ranges = InlineRangeCursor::new(&as_definitions);
423 let mut text_ranges = InlineRangeCursor::new(&as_text);
424 colon_definitions.retain(|detail| {
425 !definition_ranges
426 .overlapping(detail)
427 .eq(text_ranges.overlapping(detail))
428 });
429 }
430
431 blocks.sort_by_key(|&(start, _)| start);
433 spans.sort_by_key(|&(start, _)| start);
434 details.sort_by_key(|d| d.start);
435 strong_spans.sort_by_key(|s| s.start);
436 html_blocks.sort_by_key(|&(start, _)| start);
437 ParseResult {
438 definition_items,
439 definition_terms,
440 definition_texts,
441 colon_definitions,
442 code_blocks: blocks,
443 code_spans: spans,
444 code_block_details: details,
445 strong_spans,
446 line_to_list,
447 list_start_values,
448 html_blocks,
449 }
450 }
451
452 pub fn is_in_code_block_or_span(blocks: &[(usize, usize)], pos: usize) -> bool {
454 Self::is_in_code_block(blocks, pos)
455 }
456
457 pub fn is_in_code_block(blocks: &[(usize, usize)], pos: usize) -> bool {
463 let idx = blocks.partition_point(|&(start, _)| start <= pos);
465 idx > 0 && pos < blocks[idx - 1].1
468 }
469
470 pub fn analyze_code_block_context(
473 lines: &[crate::lint_context::LineInfo],
474 line_idx: usize,
475 min_continuation_indent: usize,
476 ) -> CodeBlockContext {
477 if let Some(line_info) = lines.get(line_idx) {
478 if line_info.indent >= min_continuation_indent {
480 return CodeBlockContext::Indented;
481 }
482
483 let (prev_blanks, next_blanks) = Self::count_surrounding_blank_lines(lines, line_idx);
485
486 if prev_blanks > 0 || next_blanks > 0 {
489 return CodeBlockContext::Standalone;
490 }
491
492 CodeBlockContext::Adjacent
494 } else {
495 CodeBlockContext::Adjacent
497 }
498 }
499
500 fn count_surrounding_blank_lines(lines: &[crate::lint_context::LineInfo], line_idx: usize) -> (usize, usize) {
502 let mut prev_blanks = 0;
503 let mut next_blanks = 0;
504
505 for i in (0..line_idx).rev() {
507 if let Some(line) = lines.get(i) {
508 if line.is_blank {
509 prev_blanks += 1;
510 } else {
511 break;
512 }
513 } else {
514 break;
515 }
516 }
517
518 for i in (line_idx + 1)..lines.len() {
520 if let Some(line) = lines.get(i) {
521 if line.is_blank {
522 next_blanks += 1;
523 } else {
524 break;
525 }
526 } else {
527 break;
528 }
529 }
530
531 (prev_blanks, next_blanks)
532 }
533
534 pub fn calculate_min_continuation_indent(
537 content: &str,
538 lines: &[crate::lint_context::LineInfo],
539 current_line_idx: usize,
540 ) -> usize {
541 for i in (0..current_line_idx).rev() {
543 if let Some(line_info) = lines.get(i) {
544 if let Some(list_item) = &line_info.list_item {
545 return if list_item.is_ordered {
547 list_item.marker_column + list_item.marker.len() + 1 } else {
549 list_item.marker_column + 2 };
551 }
552
553 if line_info.heading.is_some() || Self::is_structural_separator(line_info.content(content)) {
555 break;
556 }
557 }
558 }
559
560 0 }
562
563 fn is_structural_separator(content: &str) -> bool {
565 let trimmed = content.trim();
566 trimmed.starts_with("---")
567 || trimmed.starts_with("***")
568 || trimmed.starts_with("___")
569 || crate::utils::skip_context::is_table_line(trimmed)
570 || trimmed.starts_with('>') }
572
573 pub fn detect_markdown_code_blocks(content: &str) -> Vec<MarkdownCodeBlock> {
581 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
582
583 let mut blocks = Vec::new();
584 let mut current_block: Option<MarkdownCodeBlockBuilder> = None;
585
586 let options = rumdl_parser_options();
587 let parser = Parser::new_ext(content, options).into_offset_iter();
588
589 for (event, range) in parser {
590 match event {
591 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
592 let language = info.split_whitespace().next().unwrap_or("");
594 if language.eq_ignore_ascii_case("markdown") || language.eq_ignore_ascii_case("md") {
595 let block_start = range.start;
597 let content_start = content[block_start..]
598 .find('\n')
599 .map_or(content.len(), |i| block_start + i + 1);
600
601 current_block = Some(MarkdownCodeBlockBuilder { content_start });
602 }
603 }
604 Event::End(TagEnd::CodeBlock) => {
605 if let Some(builder) = current_block.take() {
606 let block_end = range.end;
608
609 if builder.content_start > block_end || builder.content_start > content.len() {
611 continue;
612 }
613
614 let search_range = &content[builder.content_start..block_end.min(content.len())];
615 let content_end = search_range
616 .rfind('\n')
617 .map_or(builder.content_start, |i| builder.content_start + i);
618
619 if content_end >= builder.content_start {
621 blocks.push(MarkdownCodeBlock {
622 content_start: builder.content_start,
623 content_end,
624 });
625 }
626 }
627 }
628 _ => {}
629 }
630 }
631
632 blocks
633 }
634}
635
636#[derive(Debug, Clone)]
638pub struct MarkdownCodeBlock {
639 pub content_start: usize,
641 pub content_end: usize,
643}
644
645struct MarkdownCodeBlockBuilder {
647 content_start: usize,
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_detect_fenced_code_blocks() {
656 let content = "Some text\n```\ncode here\n```\nMore text";
661 let blocks = CodeBlockUtils::detect_code_blocks(content);
662 assert_eq!(blocks.len(), 1);
664
665 let fenced_block = blocks
667 .iter()
668 .find(|(start, end)| end - start > 10 && content[*start..*end].contains("code here"));
669 assert!(fenced_block.is_some());
670
671 let content = "Some text\n~~~\ncode here\n~~~\nMore text";
673 let blocks = CodeBlockUtils::detect_code_blocks(content);
674 assert_eq!(blocks.len(), 1);
675 assert_eq!(&content[blocks[0].0..blocks[0].1], "~~~\ncode here\n~~~");
676
677 let content = "Text\n```\ncode1\n```\nMiddle\n~~~\ncode2\n~~~\nEnd";
679 let blocks = CodeBlockUtils::detect_code_blocks(content);
680 assert_eq!(blocks.len(), 2);
682 }
683
684 #[test]
685 fn test_detect_code_blocks_with_language() {
686 let content = "Text\n```rust\nfn main() {}\n```\nMore";
688 let blocks = CodeBlockUtils::detect_code_blocks(content);
689 assert_eq!(blocks.len(), 1);
691 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("fn main"));
693 assert!(fenced.is_some());
694 }
695
696 #[test]
697 fn test_unclosed_code_block() {
698 let content = "Text\n```\ncode here\nno closing fence";
700 let blocks = CodeBlockUtils::detect_code_blocks(content);
701 assert_eq!(blocks.len(), 1);
702 assert_eq!(blocks[0].1, content.len());
703 }
704
705 #[test]
706 fn test_indented_code_blocks() {
707 let content = "Paragraph\n\n code line 1\n code line 2\n\nMore text";
709 let blocks = CodeBlockUtils::detect_code_blocks(content);
710 assert_eq!(blocks.len(), 1);
711 assert!(content[blocks[0].0..blocks[0].1].contains("code line 1"));
712 assert!(content[blocks[0].0..blocks[0].1].contains("code line 2"));
713
714 let content = "Paragraph\n\n\tcode with tab\n\tanother line\n\nText";
716 let blocks = CodeBlockUtils::detect_code_blocks(content);
717 assert_eq!(blocks.len(), 1);
718 }
719
720 #[test]
721 fn test_indented_code_requires_blank_line() {
722 let content = "Paragraph\n indented but not code\nMore text";
724 let blocks = CodeBlockUtils::detect_code_blocks(content);
725 assert_eq!(blocks.len(), 0);
726
727 let content = "Paragraph\n\n now it's code\nMore text";
729 let blocks = CodeBlockUtils::detect_code_blocks(content);
730 assert_eq!(blocks.len(), 1);
731 }
732
733 #[test]
734 fn test_indented_content_with_list_markers_is_code_block() {
735 let content = "List:\n\n - Item 1\n - Item 2\n * Item 3\n + Item 4";
740 let blocks = CodeBlockUtils::detect_code_blocks(content);
741 assert_eq!(blocks.len(), 1); let content = "List:\n\n 1. First\n 2. Second";
745 let blocks = CodeBlockUtils::detect_code_blocks(content);
746 assert_eq!(blocks.len(), 1); }
748
749 #[test]
750 fn test_actual_list_items_not_code_blocks() {
751 let content = "- Item 1\n- Item 2\n* Item 3";
753 let blocks = CodeBlockUtils::detect_code_blocks(content);
754 assert_eq!(blocks.len(), 0);
755
756 let content = "- Item 1\n - Nested item\n- Item 2";
758 let blocks = CodeBlockUtils::detect_code_blocks(content);
759 assert_eq!(blocks.len(), 0);
760 }
761
762 #[test]
763 fn test_inline_code_spans_not_detected() {
764 let content = "Text with `inline code` here";
766 let blocks = CodeBlockUtils::detect_code_blocks(content);
767 assert_eq!(blocks.len(), 0); let content = "Text with ``code with ` backtick`` here";
771 let blocks = CodeBlockUtils::detect_code_blocks(content);
772 assert_eq!(blocks.len(), 0); let content = "Has `code1` and `code2` spans";
776 let blocks = CodeBlockUtils::detect_code_blocks(content);
777 assert_eq!(blocks.len(), 0); }
779
780 #[test]
781 fn test_unclosed_code_span() {
782 let content = "Text with `unclosed code span";
784 let blocks = CodeBlockUtils::detect_code_blocks(content);
785 assert_eq!(blocks.len(), 0);
786
787 let content = "Text with ``one style` different close";
789 let blocks = CodeBlockUtils::detect_code_blocks(content);
790 assert_eq!(blocks.len(), 0);
791 }
792
793 #[test]
794 fn test_mixed_code_blocks_and_spans() {
795 let content = "Has `span1` text\n```\nblock\n```\nand `span2`";
796 let blocks = CodeBlockUtils::detect_code_blocks(content);
797 assert_eq!(blocks.len(), 1);
799
800 assert!(blocks.iter().any(|(s, e)| content[*s..*e].contains("block")));
802 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span1`"));
804 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span2`"));
805 }
806
807 #[test]
808 fn test_is_in_code_block_or_span() {
809 let blocks = vec![(10, 20), (30, 40), (50, 60)];
810
811 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 15));
813 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 35));
814 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 55));
815
816 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));
822 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 25));
823 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 65));
824 }
825
826 #[test]
827 fn test_empty_content() {
828 let blocks = CodeBlockUtils::detect_code_blocks("");
829 assert_eq!(blocks.len(), 0);
830 }
831
832 #[test]
833 fn test_code_block_at_start() {
834 let content = "```\ncode\n```\nText after";
835 let blocks = CodeBlockUtils::detect_code_blocks(content);
836 assert_eq!(blocks.len(), 1);
838 assert_eq!(blocks[0].0, 0); }
840
841 #[test]
842 fn test_code_block_at_end() {
843 let content = "Text before\n```\ncode\n```";
844 let blocks = CodeBlockUtils::detect_code_blocks(content);
845 assert_eq!(blocks.len(), 1);
847 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("code"));
849 assert!(fenced.is_some());
850 }
851
852 #[test]
853 fn test_nested_fence_markers() {
854 let content = "Text\n````\n```\nnested\n```\n````\nAfter";
856 let blocks = CodeBlockUtils::detect_code_blocks(content);
857 assert!(!blocks.is_empty());
859 let outer = blocks.iter().find(|(s, e)| content[*s..*e].contains("nested"));
861 assert!(outer.is_some());
862 }
863
864 #[test]
865 fn test_indented_code_with_blank_lines() {
866 let content = "Text\n\n line1\n\n line2\n\nAfter";
868 let blocks = CodeBlockUtils::detect_code_blocks(content);
869 assert!(!blocks.is_empty());
871 let all_content: String = blocks
873 .iter()
874 .map(|(s, e)| &content[*s..*e])
875 .collect::<Vec<_>>()
876 .join("");
877 assert!(all_content.contains("line1") || content[blocks[0].0..blocks[0].1].contains("line1"));
878 }
879
880 #[test]
881 fn test_code_span_with_spaces() {
882 let content = "Text ` code with spaces ` more";
884 let blocks = CodeBlockUtils::detect_code_blocks(content);
885 assert_eq!(blocks.len(), 0); }
887
888 #[test]
889 fn test_fenced_block_with_info_string() {
890 let content = "```rust,no_run,should_panic\ncode\n```";
892 let blocks = CodeBlockUtils::detect_code_blocks(content);
893 assert_eq!(blocks.len(), 1);
895 assert_eq!(blocks[0].0, 0);
896 }
897
898 #[test]
899 fn test_indented_fences_not_code_blocks() {
900 let content = "Text\n ```\n code\n ```\nAfter";
902 let blocks = CodeBlockUtils::detect_code_blocks(content);
903 assert_eq!(blocks.len(), 1);
905 }
906
907 #[test]
909 fn test_backticks_in_info_string_not_code_block() {
910 let content = "```something```\n\n```bash\n# comment\n```";
916 let blocks = CodeBlockUtils::detect_code_blocks(content);
917 assert_eq!(blocks.len(), 1);
919 assert!(content[blocks[0].0..blocks[0].1].contains("# comment"));
921 }
922
923 #[test]
924 fn test_issue_175_reproduction() {
925 let content = "```something```\n\n```bash\n# Have a parrot\necho \"🦜\"\n```";
927 let blocks = CodeBlockUtils::detect_code_blocks(content);
928 assert_eq!(blocks.len(), 1);
930 assert!(content[blocks[0].0..blocks[0].1].contains("Have a parrot"));
931 }
932
933 #[test]
934 fn test_tilde_fence_allows_tildes_in_info_string() {
935 let content = "~~~abc~~~\ncode content\n~~~";
938 let blocks = CodeBlockUtils::detect_code_blocks(content);
939 assert_eq!(blocks.len(), 1);
941 }
942
943 #[test]
944 fn test_nested_longer_fence_contains_shorter() {
945 let content = "````\n```\nnested content\n```\n````";
947 let blocks = CodeBlockUtils::detect_code_blocks(content);
948 assert_eq!(blocks.len(), 1);
949 assert!(content[blocks[0].0..blocks[0].1].contains("nested content"));
950 }
951
952 #[test]
953 fn test_mixed_fence_types() {
954 let content = "~~~\n```\nmixed content\n~~~";
956 let blocks = CodeBlockUtils::detect_code_blocks(content);
957 assert_eq!(blocks.len(), 1);
958 assert!(content[blocks[0].0..blocks[0].1].contains("mixed content"));
959 }
960
961 #[test]
962 fn test_indented_code_in_list_issue_276() {
963 let content = r#"1. First item
9652. Second item with code:
966
967 # This is a code block in a list
968 print("Hello, world!")
969
9704. Third item"#;
971
972 let blocks = CodeBlockUtils::detect_code_blocks(content);
973 assert!(!blocks.is_empty(), "Should detect indented code block inside list");
975
976 let all_content: String = blocks
978 .iter()
979 .map(|(s, e)| &content[*s..*e])
980 .collect::<Vec<_>>()
981 .join("");
982 assert!(
983 all_content.contains("code block in a list") || all_content.contains("print"),
984 "Detected block should contain the code content: {all_content:?}"
985 );
986 }
987
988 #[test]
989 fn test_detect_markdown_code_blocks() {
990 let content = r#"# Example
991
992```markdown
993# Heading
994Content here
995```
996
997```md
998Another heading
999More content
1000```
1001
1002```rust
1003// Not markdown
1004fn main() {}
1005```
1006"#;
1007
1008 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1009
1010 assert_eq!(
1012 blocks.len(),
1013 2,
1014 "Should detect exactly 2 markdown blocks, got {blocks:?}"
1015 );
1016
1017 let first = &blocks[0];
1019 let first_content = &content[first.content_start..first.content_end];
1020 assert!(
1021 first_content.contains("# Heading"),
1022 "First block should contain '# Heading', got: {first_content:?}"
1023 );
1024
1025 let second = &blocks[1];
1027 let second_content = &content[second.content_start..second.content_end];
1028 assert!(
1029 second_content.contains("Another heading"),
1030 "Second block should contain 'Another heading', got: {second_content:?}"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_detect_markdown_code_blocks_empty() {
1036 let content = "# Just a heading\n\nNo code blocks here\n";
1037 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1038 assert_eq!(blocks.len(), 0);
1039 }
1040
1041 #[test]
1042 fn test_detect_markdown_code_blocks_case_insensitive() {
1043 let content = "```MARKDOWN\nContent\n```\n";
1044 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1045 assert_eq!(blocks.len(), 1);
1046 }
1047
1048 #[test]
1049 fn test_detect_markdown_code_blocks_at_eof_no_trailing_newline() {
1050 let content = "# Doc\n\n```markdown\nContent\n```";
1052 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1053 assert_eq!(blocks.len(), 1);
1054 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
1056 assert!(block_content.contains("Content"));
1057 }
1058
1059 #[test]
1060 fn test_detect_markdown_code_blocks_single_line_content() {
1061 let content = "```markdown\nX\n```\n";
1063 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1064 assert_eq!(blocks.len(), 1);
1065 let block_content = &content[blocks[0].content_start..blocks[0].content_end];
1066 assert_eq!(block_content, "X");
1067 }
1068
1069 #[test]
1070 fn test_detect_markdown_code_blocks_empty_content() {
1071 let content = "```markdown\n```\n";
1073 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1074 if !blocks.is_empty() {
1077 assert!(blocks[0].content_start <= blocks[0].content_end);
1079 }
1080 }
1081
1082 #[test]
1083 fn test_detect_markdown_code_blocks_validates_ranges() {
1084 let test_cases = [
1086 "", "```markdown", "```markdown\n", "```\n```", "```markdown\n```", " ```markdown\n X\n ```", ];
1093
1094 for content in test_cases {
1095 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
1097 for block in &blocks {
1099 assert!(
1100 block.content_start <= block.content_end,
1101 "Invalid range in content: {content:?}"
1102 );
1103 assert!(
1104 block.content_end <= content.len(),
1105 "Range exceeds content length in: {content:?}"
1106 );
1107 }
1108 }
1109 }
1110
1111 #[test]
1114 fn test_is_in_code_block_empty_blocks() {
1115 assert!(!CodeBlockUtils::is_in_code_block(&[], 0));
1116 assert!(!CodeBlockUtils::is_in_code_block(&[], 100));
1117 assert!(!CodeBlockUtils::is_in_code_block(&[], usize::MAX));
1118 }
1119
1120 #[test]
1121 fn test_is_in_code_block_single_range() {
1122 let blocks = [(10, 20)];
1123 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
1124 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 9));
1125 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
1126 assert!(CodeBlockUtils::is_in_code_block(&blocks, 15));
1127 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
1128 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 20));
1130 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 21));
1131 }
1132
1133 #[test]
1134 fn test_is_in_code_block_multiple_ranges() {
1135 let blocks = [(5, 10), (20, 30), (50, 60)];
1136 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
1138 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
1139 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
1141 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
1142 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 10));
1144 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 15));
1145 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 19));
1146 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
1148 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
1149 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
1151 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 49));
1152 assert!(CodeBlockUtils::is_in_code_block(&blocks, 50));
1154 assert!(CodeBlockUtils::is_in_code_block(&blocks, 59));
1155 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 60));
1157 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 1000));
1158 }
1159
1160 #[test]
1161 fn test_is_in_code_block_adjacent_ranges() {
1162 let blocks = [(0, 10), (10, 20), (20, 30)];
1164 assert!(CodeBlockUtils::is_in_code_block(&blocks, 0));
1165 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
1166 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
1167 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
1168 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
1169 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
1170 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
1171 }
1172
1173 #[test]
1174 fn test_is_in_code_block_single_byte_range() {
1175 let blocks = [(5, 6)];
1176 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
1177 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
1178 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 6));
1179 }
1180
1181 #[test]
1182 fn test_is_in_code_block_matches_linear_scan() {
1183 let content = "# Heading\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nSome text\n\n```\nmore code\n```\n\nEnd\n";
1186 let blocks = CodeBlockUtils::detect_code_blocks(content);
1187
1188 for pos in 0..content.len() {
1189 let binary = CodeBlockUtils::is_in_code_block(&blocks, pos);
1190 let linear = blocks.iter().any(|&(s, e)| pos >= s && pos < e);
1191 assert_eq!(
1192 binary, linear,
1193 "Mismatch at pos {pos}: binary={binary}, linear={linear}, blocks={blocks:?}"
1194 );
1195 }
1196 }
1197
1198 #[test]
1199 fn test_is_in_code_block_at_range_boundaries() {
1200 let blocks = [(100, 200), (300, 400), (500, 600)];
1202 for &(start, end) in &blocks {
1203 assert!(
1204 !CodeBlockUtils::is_in_code_block(&blocks, start - 1),
1205 "pos={} should be outside",
1206 start - 1
1207 );
1208 assert!(
1209 CodeBlockUtils::is_in_code_block(&blocks, start),
1210 "pos={start} should be inside"
1211 );
1212 assert!(
1213 CodeBlockUtils::is_in_code_block(&blocks, end - 1),
1214 "pos={} should be inside",
1215 end - 1
1216 );
1217 assert!(
1218 !CodeBlockUtils::is_in_code_block(&blocks, end),
1219 "pos={end} should be outside"
1220 );
1221 }
1222 }
1223}