1use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
12
13use super::parser_options::rumdl_parser_options_for;
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 pub content_column: Option<usize>,
31 pub closed: bool,
36 pub enclosing: Vec<Enclosure>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Enclosure {
48 Quote { end: usize },
50 Item { width: usize, end: usize },
53 Opaque { end: usize },
56}
57
58impl Enclosure {
59 pub fn end(&self) -> usize {
61 match *self {
62 Self::Quote { end } | Self::Item { end, .. } | Self::Opaque { end } => end,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, Default)]
69pub struct StrippedLine {
70 pub pos: usize,
73 pub col: usize,
75 pub pending: usize,
77}
78
79impl StrippedLine {
80 pub fn indent(&self, line: &str) -> (usize, Option<usize>) {
83 let mut col = self.col + self.pending;
84 let mut pos = self.pos;
85 for c in line[pos..].chars() {
86 match c {
87 ' ' => col += 1,
88 '\t' => col += 4 - col % 4,
89 _ => return (col - self.col, Some(pos)),
90 }
91 pos += c.len_utf8();
92 }
93 (col - self.col, None)
94 }
95
96 fn take_whitespace(&mut self, line: &str, max: usize) -> usize {
97 let mut taken = 0;
98 while taken < max {
99 if self.pending > 0 {
100 let n = self.pending.min(max - taken);
101 self.pending -= n;
102 self.col += n;
103 taken += n;
104 continue;
105 }
106 match line[self.pos..].chars().next() {
107 Some(' ') => {
108 self.pos += 1;
109 self.col += 1;
110 taken += 1;
111 }
112 Some('\t') => {
113 let width = 4 - self.col % 4;
114 let n = width.min(max - taken);
115 self.pos += 1;
116 self.col += n;
117 self.pending = width - n;
118 taken += n;
119 }
120 _ => break,
121 }
122 }
123 taken
124 }
125
126 fn rest_is_blank(&self, line: &str) -> bool {
127 line[self.pos..].chars().all(|c| c == ' ' || c == '\t')
128 }
129
130 fn take_container(&mut self, line: &str, enclosure: Enclosure) -> bool {
133 match enclosure {
134 Enclosure::Quote { .. } => {
135 self.take_whitespace(line, 3);
136 if self.pending > 0 || !line[self.pos..].starts_with('>') {
137 return false;
138 }
139 self.pos += 1;
140 self.col += 1;
141 self.take_whitespace(line, 1);
142 true
143 }
144 Enclosure::Item { width, .. } => self.take_whitespace(line, width) >= width || self.rest_is_blank(line),
145 Enclosure::Opaque { .. } => false,
146 }
147 }
148}
149
150pub fn strip_enclosure(line: &str, enclosing: &[Enclosure]) -> Option<StrippedLine> {
158 let mut at = StrippedLine {
159 pos: 0,
160 col: 0,
161 pending: 0,
162 };
163 enclosing
164 .iter()
165 .all(|&enclosure| at.take_container(line, enclosure))
166 .then_some(at)
167}
168
169pub fn continued_depth(line: &str, enclosing: &[Enclosure], item_runs: &[usize]) -> usize {
176 let blank_from = line.trim_end_matches([' ', '\t']).len();
177 let mut at = StrippedLine {
178 pos: 0,
179 col: 0,
180 pending: 0,
181 };
182 for (i, &enclosure) in enclosing.iter().enumerate() {
183 if at.pos >= blank_from {
184 return i + item_runs[i];
185 }
186 if !at.take_container(line, enclosure) {
187 return i;
188 }
189 }
190 enclosing.len()
191}
192
193pub fn item_runs(enclosing: &[Enclosure]) -> Vec<usize> {
196 let mut runs = vec![0; enclosing.len()];
197 for i in (0..enclosing.len()).rev() {
198 if matches!(enclosing[i], Enclosure::Item { .. }) {
199 runs[i] = 1 + runs.get(i + 1).copied().unwrap_or(0);
200 }
201 }
202 runs
203}
204
205fn item_content_start(content: &str, marker: usize) -> Option<(StrippedLine, usize)> {
214 let rest = content.get(marker..)?;
215 let line_start = line_start_of(content, marker);
216 let marker_len = match rest.as_bytes().first()? {
217 b'-' | b'+' | b'*' => 1,
218 b'0'..=b'9' => {
219 let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
220 match rest.as_bytes().get(digits)? {
221 b'.' | b')' => digits + 1,
222 _ => return None,
223 }
224 }
225 _ => return None,
226 };
227 let marker_end = marker + marker_len;
228 let marker_col = visual_column(content, marker_end);
229 let line_end = content[marker_end..]
230 .find('\n')
231 .map_or(content.len(), |i| marker_end + i);
232 let after = content[marker_end..line_end].trim_end_matches('\r');
233 let mut col = marker_col;
234 let mut text = None;
235 for (i, c) in after.char_indices() {
236 match c {
237 ' ' => col += 1,
238 '\t' => col += 4 - col % 4,
239 _ => {
240 text = Some(i);
241 break;
242 }
243 }
244 }
245 let mut at = StrippedLine {
246 pos: marker_end - line_start,
247 col: marker_col,
248 pending: 0,
249 };
250 match text {
251 Some(i) if (1..=4).contains(&(col - marker_col)) => {
252 at.pos += i;
253 at.col = col;
254 }
255 _ => {
258 if at.take_whitespace(&content[line_start..line_end], 1) == 0 {
259 at.col += 1;
260 }
261 }
262 }
263 Some((at, line_start))
264}
265
266fn fenced_block_is_closed(content: &str, start: usize, end: usize, literal_lines: usize) -> bool {
276 let bytes = content.as_bytes();
277 let end = end.min(bytes.len());
278 let body_end = if end > start && bytes[end - 1] == b'\n' {
280 end - 1
281 } else {
282 end
283 };
284 let body = &bytes[start.min(body_end)..body_end];
285 let span_lines = body.split(|&b| b == b'\n').count();
287 let line_start = body
288 .iter()
289 .rposition(|&b| b == b'\n')
290 .map_or(start, |at| start + at + 1);
291 let last_line = content.get(line_start..body_end).unwrap_or("");
292 span_lines == literal_lines + 2 && last_line.trim_start_matches(['>', ' ', '\t']).starts_with(['`', '~'])
293}
294
295fn line_start_of(content: &str, pos: usize) -> usize {
298 let before = &content.as_bytes()[..pos.min(content.len())];
299 before.iter().rposition(|&b| b == b'\n').map_or(0, |i| i + 1)
300}
301
302fn visual_column(content: &str, pos: usize) -> usize {
305 let line_start = line_start_of(content, pos);
306 content[line_start..pos]
307 .chars()
308 .fold(0, |col, c| if c == '\t' { col + 4 - col % 4 } else { col + 1 })
309}
310
311#[derive(Debug, Clone)]
313pub struct StrongSpanDetail {
314 pub start: usize,
316 pub end: usize,
318 pub is_asterisk: bool,
320}
321
322#[derive(Debug, Clone)]
329pub struct DefinitionTextDetail {
330 pub start: usize,
332 pub end: usize,
334 pub definition_start: usize,
337}
338
339pub type LineToListMap = std::collections::HashMap<usize, usize>;
341pub type ListStartValues = std::collections::HashMap<usize, u64>;
343
344#[derive(Debug, Default)]
346pub struct ParseResult {
347 pub code_blocks: Vec<(usize, usize)>,
349 pub code_spans: Vec<(usize, usize)>,
351 pub code_block_details: Vec<CodeBlockDetail>,
353 pub strong_spans: Vec<StrongSpanDetail>,
355 pub line_to_list: LineToListMap,
357 pub list_start_values: ListStartValues,
359 pub html_blocks: Vec<(usize, usize)>,
366 pub definition_items: Vec<(usize, usize)>,
371 pub definition_terms: Vec<(usize, usize)>,
373 pub definition_texts: Vec<DefinitionTextDetail>,
375 pub colon_definitions: Vec<ColonDefinitionDetail>,
381 pub table_ranges: Vec<(usize, usize)>,
383 pub top_level_blocks: Vec<(usize, usize)>,
386 pub rejected_definition_ranges: Vec<(usize, usize)>,
390 pub list_items: Vec<(usize, usize, usize)>,
393}
394
395#[derive(Debug, Clone)]
398pub struct ColonDefinitionDetail {
399 pub start: usize,
401 pub end: usize,
403 pub definition_start: usize,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
409pub enum CodeBlockContext {
410 Standalone,
412 Indented,
414 Adjacent,
416}
417
418fn is_inline_tag(tag: &Tag) -> bool {
420 matches!(
421 tag,
422 Tag::Emphasis
423 | Tag::Strong
424 | Tag::Strikethrough
425 | Tag::Superscript
426 | Tag::Subscript
427 | Tag::Link { .. }
428 | Tag::Image { .. }
429 )
430}
431
432fn is_inline_tag_end(tag_end: TagEnd) -> bool {
434 matches!(
435 tag_end,
436 TagEnd::Emphasis
437 | TagEnd::Strong
438 | TagEnd::Strikethrough
439 | TagEnd::Superscript
440 | TagEnd::Subscript
441 | TagEnd::Link
442 | TagEnd::Image
443 )
444}
445
446fn opens_definition(content: &str, start: usize) -> bool {
454 let Some(rest) = content.get(start..) else {
457 return false;
458 };
459 rest.trim_start_matches([' ', '\t'])
460 .strip_prefix(':')
461 .is_some_and(|rest| rest.is_empty() || rest.starts_with([' ', '\t', '\n', '\r']))
462}
463
464fn indented_block_columns(content: &str, options: Options) -> Vec<(usize, usize, usize)> {
470 let mut blocks = Vec::new();
471 let mut open: Option<(usize, usize)> = None;
472 let mut awaiting_first_event = false;
473 for (event, range) in Parser::new_ext(content, options).into_offset_iter() {
474 if std::mem::take(&mut awaiting_first_event)
475 && let Event::Text(text) = &event
476 && range.is_empty()
477 && let Some((_, column)) = &mut open
478 {
479 *column = column.saturating_sub(text.len());
480 }
481 match event {
482 Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => {
483 open = Some((range.start, visual_column(content, range.start).saturating_sub(4)));
484 awaiting_first_event = true;
485 }
486 Event::End(TagEnd::CodeBlock) => {
487 if let Some((start, column)) = open.take() {
488 blocks.push((start, range.end, column));
489 }
490 }
491 _ => {}
492 }
493 }
494 blocks
495}
496
497fn inline_ranges(content: &str, options: Options) -> Vec<(usize, usize)> {
501 Parser::new_ext(content, options)
502 .into_offset_iter()
503 .filter_map(|(event, range)| match event {
504 Event::Start(tag) if is_inline_tag(&tag) => Some((range.start, range.end)),
505 Event::Code(_) | Event::InlineHtml(_) | Event::InlineMath(_) | Event::DisplayMath(_) => {
506 Some((range.start, range.end))
507 }
508 _ => None,
509 })
510 .collect()
511}
512
513struct InlineRangeCursor<'a> {
518 ranges: &'a [(usize, usize)],
519 next: usize,
520 active: Vec<(usize, usize)>,
521}
522
523impl<'a> InlineRangeCursor<'a> {
524 fn new(ranges: &'a [(usize, usize)]) -> Self {
525 Self {
526 ranges,
527 next: 0,
528 active: Vec::new(),
529 }
530 }
531
532 fn overlapping(&mut self, detail: &ColonDefinitionDetail) -> impl Iterator<Item = (usize, usize)> + '_ {
533 while self.next < self.ranges.len() && self.ranges[self.next].0 <= detail.start {
534 self.active.push(self.ranges[self.next]);
535 self.next += 1;
536 }
537 self.active.retain(|&(_, end)| end > detail.start);
538 let end = detail.end;
539 self.active.iter().copied().chain(
540 self.ranges[self.next..]
541 .iter()
542 .take_while(move |&&(start, _)| start < end)
543 .copied(),
544 )
545 }
546}
547
548pub struct CodeBlockUtils;
550
551impl CodeBlockUtils {
552 pub fn detect_code_blocks(content: &str) -> Vec<(usize, usize)> {
562 Self::detect_code_blocks_and_spans(content).code_blocks
563 }
564
565 pub fn detect_code_blocks_and_spans(content: &str) -> ParseResult {
568 let line_starts: Vec<usize> = std::iter::once(0)
569 .chain(content.match_indices('\n').map(|(i, _)| i + 1))
570 .collect();
571 let options = rumdl_parser_options_for(content);
572 Self::collect_code_blocks_and_spans(
573 content,
574 &line_starts,
575 options,
576 Parser::new_ext(content, options).into_offset_iter(),
577 )
578 }
579
580 pub(crate) fn collect_code_blocks_and_spans<'a>(
584 content: &str,
585 line_starts: &[usize],
586 options: Options,
587 events: impl Iterator<Item = (Event<'a>, std::ops::Range<usize>)>,
588 ) -> ParseResult {
589 let mut blocks = Vec::new();
590 let mut spans = Vec::new();
591 let mut details = Vec::new();
592 let mut strong_spans = Vec::new();
593 let mut html_blocks = Vec::new();
594 let mut code_block_start: Option<(usize, bool, String, Option<usize>)> = None;
595 let mut literal_newlines = 0usize;
598 let mut literal_partial_line = false;
599 let mut awaiting_first_code_event = false;
604 let mut definition_rejected: Vec<bool> = Vec::new();
609 let mut text_reading = Vec::new();
610
611 let mut definition_items: Vec<(usize, usize)> = Vec::new();
622 let mut pending_terms: Vec<(usize, usize)> = Vec::new();
623 let mut definition_terms = Vec::new();
624 let mut definition_texts = Vec::new();
625 let mut colon_definitions = Vec::new();
626 let mut table_ranges: Vec<(usize, usize)> = Vec::new();
627 let mut top_level_blocks: Vec<(usize, usize)> = Vec::new();
628 let mut rejected_definition_ranges: Vec<(usize, usize)> = Vec::new();
629 let mut list_item_spans: Vec<(usize, usize, Option<usize>)> = Vec::new();
630 let mut open_items: Vec<usize> = Vec::new();
631 let mut open_containers: Vec<(Enclosure, usize, StrippedLine)> = Vec::new();
635 let mut code_block_enclosing: Vec<Enclosure> = Vec::new();
636 let mut block_stack: Vec<Option<usize>> = Vec::new();
637 let mut definition_paragraph: Option<(usize, usize)> = None;
638 let mut tight_run: Option<DefinitionTextDetail> = None;
639
640 let mut line_to_list = LineToListMap::new();
642 let mut list_start_values = ListStartValues::new();
643 let mut list_stack: Vec<(usize, bool, u64)> = Vec::new(); let mut next_list_id: usize = 0;
645
646 let byte_to_line = |byte_offset: usize| -> usize { line_starts.partition_point(|&start| start <= byte_offset) };
647
648 for (event, range) in events {
649 if let Some(&item) = open_items.last()
650 && list_item_spans[item].2.is_none()
651 && !matches!(event, Event::End(_))
652 {
653 list_item_spans[item].2 = Some(range.start);
654 }
655 if matches!(event, Event::Start(Tag::Item)) {
656 open_items.push(list_item_spans.len());
657 list_item_spans.push((range.start, range.end, None));
658 } else if matches!(event, Event::End(TagEnd::Item)) {
659 open_items.pop();
660 }
661 match &event {
662 Event::Start(Tag::BlockQuote(_)) => {
663 let line_start = line_start_of(content, range.start);
664 open_containers.push((Enclosure::Quote { end: range.end }, line_start, StrippedLine::default()));
665 }
666 Event::Start(Tag::Item) => {
667 let entry = item_content_start(content, range.start)
668 .and_then(|(content_start, line_start)| {
669 let prefix = &content[line_start..range.start];
670 let same_line = open_containers.iter().rposition(|&(parent, parent_line, _)| {
674 matches!(parent, Enclosure::Item { .. }) && parent_line == line_start
675 });
676 let (mut at, rest) = match same_line {
677 Some(i) => (open_containers[i].2, &open_containers[i + 1..]),
678 None => (StrippedLine::default(), &open_containers[..]),
679 };
680 if !rest.iter().all(|&(parent, _, _)| at.take_container(prefix, parent)) {
681 return None;
682 }
683 let width = content_start.col.checked_sub(at.col)?;
684 Some((Enclosure::Item { width, end: range.end }, line_start, content_start))
685 })
686 .unwrap_or((Enclosure::Opaque { end: range.end }, 0, StrippedLine::default()));
687 open_containers.push(entry);
688 }
689 Event::Start(Tag::DefinitionListDefinition | Tag::FootnoteDefinition(_)) => {
690 open_containers.push((Enclosure::Opaque { end: range.end }, 0, StrippedLine::default()));
691 }
692 Event::End(
693 TagEnd::BlockQuote(_)
694 | TagEnd::Item
695 | TagEnd::DefinitionListDefinition
696 | TagEnd::FootnoteDefinition,
697 ) => {
698 open_containers.pop();
699 }
700 _ => {}
701 }
702 match &event {
703 Event::Start(tag) if !is_inline_tag(tag) => {
704 definition_texts.extend(tight_run.take());
705 if block_stack.is_empty() {
706 top_level_blocks.push((range.start, range.end));
707 }
708 if matches!(tag, Tag::Table(_)) {
709 table_ranges.push((range.start, range.end));
710 }
711 if let (Tag::Paragraph, Some(Some(definition_start))) = (tag, block_stack.last()) {
712 definition_paragraph = Some((range.start, *definition_start));
713 }
714 let mut definition_start = None;
715 match tag {
716 Tag::DefinitionList => pending_terms.clear(),
717 Tag::DefinitionListTitle => pending_terms.push((range.start, range.end)),
718 Tag::DefinitionListDefinition => {
719 let item_start = pending_terms.first().map_or(range.start, |&(start, _)| start);
722 let item_end = content.as_bytes()[range.clone()]
726 .iter()
727 .rposition(|byte| !matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | b'>'))
728 .map_or(range.start, |index| range.start + index + 1);
729 let accepted = opens_definition(content, range.start);
730 definition_rejected.push(!accepted);
731 if accepted {
732 definition_items.push((item_start, item_end));
733 definition_terms.append(&mut pending_terms);
734 definition_start = Some(range.start);
735 } else {
736 rejected_definition_ranges.push((item_start, item_end));
737 colon_definitions.push(ColonDefinitionDetail {
738 start: item_start,
739 end: item_end,
740 definition_start: range.start,
741 });
742 pending_terms.clear();
743 }
744 }
745 _ => {}
746 }
747 block_stack.push(definition_start);
748 }
749 Event::End(tag_end) if !is_inline_tag_end(*tag_end) => {
750 definition_texts.extend(tight_run.take());
751 if let (TagEnd::Paragraph, Some((start, definition_start))) = (tag_end, definition_paragraph.take())
752 {
753 definition_texts.push(DefinitionTextDetail {
754 start,
755 end: range.end,
756 definition_start,
757 });
758 }
759 if let TagEnd::DefinitionListDefinition = tag_end {
760 definition_rejected.pop();
761 }
762 if let TagEnd::DefinitionList = tag_end {
763 pending_terms.clear();
764 }
765 block_stack.pop();
766 }
767 _ => {
770 if let Some(Some(definition_start)) = block_stack.last() {
771 match &mut tight_run {
772 Some(run) => run.end = run.end.max(range.end),
773 None => {
774 tight_run = Some(DefinitionTextDetail {
775 start: range.start,
776 end: range.end,
777 definition_start: *definition_start,
778 });
779 }
780 }
781 }
782 }
783 }
784 if std::mem::take(&mut awaiting_first_code_event)
785 && let Event::Text(text) = &event
786 && range.is_empty()
787 && let Some((_, _, _, Some(column))) = &mut code_block_start
788 {
789 *column = column.saturating_sub(text.len());
790 }
791 if code_block_start.is_some()
792 && let Event::Text(text) = &event
793 && !text.is_empty()
794 {
795 literal_newlines += text.matches('\n').count();
796 literal_partial_line = !text.ends_with('\n');
797 }
798 match event {
799 Event::Start(Tag::CodeBlock(kind)) => {
800 literal_newlines = 0;
801 literal_partial_line = false;
802 let (is_fenced, info_string) = match &kind {
803 CodeBlockKind::Fenced(info) => (true, info.to_string()),
804 CodeBlockKind::Indented => (false, String::new()),
805 };
806 let mut content_column =
807 (!is_fenced).then(|| visual_column(content, range.start).saturating_sub(4));
808 if !is_fenced && definition_rejected.contains(&true) {
809 if !block_stack.iter().any(Option::is_some) {
812 text_reading.push(range.start);
813 }
814 content_column = None;
815 }
816 awaiting_first_code_event = !is_fenced;
817 code_block_enclosing = if is_fenced {
818 open_containers.iter().map(|container| container.0).collect()
819 } else {
820 Vec::new()
821 };
822 code_block_start = Some((range.start, is_fenced, info_string, content_column));
823 }
824 Event::End(TagEnd::CodeBlock) => {
825 if let Some((start, is_fenced, info_string, content_column)) = code_block_start.take() {
826 blocks.push((start, range.end));
827 let literal_lines = literal_newlines + usize::from(literal_partial_line);
828 details.push(CodeBlockDetail {
829 start,
830 end: range.end,
831 is_fenced,
832 info_string,
833 content_column,
834 closed: is_fenced && fenced_block_is_closed(content, start, range.end, literal_lines),
835 enclosing: std::mem::take(&mut code_block_enclosing),
836 });
837 }
838 }
839 Event::Start(Tag::Strong) => {
840 if range.start + 2 <= content.len() {
841 let is_asterisk = &content[range.start..range.start + 2] == "**";
842 strong_spans.push(StrongSpanDetail {
843 start: range.start,
844 end: range.end,
845 is_asterisk,
846 });
847 }
848 }
849 Event::Start(Tag::List(start_num)) => {
850 let is_ordered = start_num.is_some();
851 let start_value = start_num.unwrap_or(1);
852 list_stack.push((next_list_id, is_ordered, start_value));
853 if is_ordered {
854 list_start_values.insert(next_list_id, start_value);
855 }
856 next_list_id += 1;
857 }
858 Event::End(TagEnd::List(_)) => {
859 list_stack.pop();
860 }
861 Event::Start(Tag::Item) => {
862 if let Some(&(list_id, is_ordered, _)) = list_stack.last()
863 && is_ordered
864 {
865 let line_num = byte_to_line(range.start);
866 line_to_list.insert(line_num, list_id);
867 }
868 }
869 Event::Start(Tag::HtmlBlock) => {
870 html_blocks.push((range.start, range.end));
872 }
873 Event::Code(_) => {
874 spans.push((range.start, range.end));
875 }
876 _ => {}
877 }
878 }
879
880 if let Some((start, is_fenced, info_string, content_column)) = code_block_start {
883 blocks.push((start, content.len()));
884 details.push(CodeBlockDetail {
885 start,
886 end: content.len(),
887 is_fenced,
888 info_string,
889 content_column,
890 closed: false,
891 enclosing: std::mem::take(&mut code_block_enclosing),
892 });
893 }
894
895 if !text_reading.is_empty() {
896 let text_blocks = indented_block_columns(content, options - Options::ENABLE_DEFINITION_LIST);
897 for detail in details.iter_mut().filter(|d| text_reading.contains(&d.start)) {
898 let lines = (byte_to_line(detail.start), byte_to_line(detail.end.saturating_sub(1)));
899 detail.content_column = text_blocks
900 .iter()
901 .find(|&&(start, end, _)| (byte_to_line(start), byte_to_line(end.saturating_sub(1))) == lines)
902 .map(|&(_, _, column)| column);
903 }
904 }
905
906 if !colon_definitions.is_empty() {
909 let as_definitions = inline_ranges(content, options);
910 let as_text = inline_ranges(content, options - Options::ENABLE_DEFINITION_LIST);
911 let mut definition_ranges = InlineRangeCursor::new(&as_definitions);
912 let mut text_ranges = InlineRangeCursor::new(&as_text);
913 colon_definitions.retain(|detail| {
914 !definition_ranges
915 .overlapping(detail)
916 .eq(text_ranges.overlapping(detail))
917 });
918 }
919
920 blocks.sort_by_key(|&(start, _)| start);
922 spans.sort_by_key(|&(start, _)| start);
923 details.sort_by_key(|d| d.start);
924 strong_spans.sort_by_key(|s| s.start);
925 html_blocks.sort_by_key(|&(start, _)| start);
926 ParseResult {
927 definition_items,
928 definition_terms,
929 definition_texts,
930 colon_definitions,
931 code_blocks: blocks,
932 code_spans: spans,
933 code_block_details: details,
934 strong_spans,
935 line_to_list,
936 list_start_values,
937 html_blocks,
938 table_ranges,
939 top_level_blocks,
940 rejected_definition_ranges,
941 list_items: list_item_spans
942 .into_iter()
943 .filter_map(|(start, end, content)| content.map(|content| (start, end, content)))
944 .collect(),
945 }
946 }
947
948 pub fn is_in_code_block_or_span(blocks: &[(usize, usize)], pos: usize) -> bool {
950 Self::is_in_code_block(blocks, pos)
951 }
952
953 pub fn is_in_code_block(blocks: &[(usize, usize)], pos: usize) -> bool {
959 let idx = blocks.partition_point(|&(start, _)| start <= pos);
961 idx > 0 && pos < blocks[idx - 1].1
964 }
965
966 pub fn analyze_code_block_context(
969 lines: &[crate::lint_context::LineInfo],
970 line_idx: usize,
971 min_continuation_indent: usize,
972 ) -> CodeBlockContext {
973 if let Some(line_info) = lines.get(line_idx) {
974 if line_info.indent >= min_continuation_indent {
976 return CodeBlockContext::Indented;
977 }
978
979 let (prev_blanks, next_blanks) = Self::count_surrounding_blank_lines(lines, line_idx);
981
982 if prev_blanks > 0 || next_blanks > 0 {
985 return CodeBlockContext::Standalone;
986 }
987
988 CodeBlockContext::Adjacent
990 } else {
991 CodeBlockContext::Adjacent
993 }
994 }
995
996 fn count_surrounding_blank_lines(lines: &[crate::lint_context::LineInfo], line_idx: usize) -> (usize, usize) {
998 let mut prev_blanks = 0;
999 let mut next_blanks = 0;
1000
1001 for i in (0..line_idx).rev() {
1003 if let Some(line) = lines.get(i) {
1004 if line.is_blank {
1005 prev_blanks += 1;
1006 } else {
1007 break;
1008 }
1009 } else {
1010 break;
1011 }
1012 }
1013
1014 for i in (line_idx + 1)..lines.len() {
1016 if let Some(line) = lines.get(i) {
1017 if line.is_blank {
1018 next_blanks += 1;
1019 } else {
1020 break;
1021 }
1022 } else {
1023 break;
1024 }
1025 }
1026
1027 (prev_blanks, next_blanks)
1028 }
1029
1030 pub fn calculate_min_continuation_indent(
1033 content: &str,
1034 lines: &[crate::lint_context::LineInfo],
1035 current_line_idx: usize,
1036 ) -> usize {
1037 for i in (0..current_line_idx).rev() {
1039 if let Some(line_info) = lines.get(i)
1040 && let Some(indent) = Self::continuation_indent_boundary(content, line_info)
1041 {
1042 return indent;
1043 }
1044 }
1045 0
1046 }
1047
1048 pub(crate) fn continuation_indent_boundary(
1052 content: &str,
1053 line_info: &crate::lint_context::LineInfo,
1054 ) -> Option<usize> {
1055 if let Some(list_item) = &line_info.list_item {
1056 Some(if list_item.is_ordered {
1057 list_item.marker_column + list_item.marker.len() + 1
1058 } else {
1059 list_item.marker_column + 2
1060 })
1061 } else if line_info.heading.is_some() || Self::is_structural_separator(line_info.content(content)) {
1062 Some(0)
1063 } else {
1064 None
1065 }
1066 }
1067
1068 fn is_structural_separator(content: &str) -> bool {
1070 let trimmed = content.trim();
1071 trimmed.starts_with("---")
1072 || trimmed.starts_with("***")
1073 || trimmed.starts_with("___")
1074 || crate::utils::skip_context::is_table_line(trimmed)
1075 || trimmed.starts_with('>') }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 #[test]
1082 fn test_continued_depth_is_the_longest_prefix_strip_enclosure_accepts() {
1083 use super::{Enclosure, continued_depth, item_runs, strip_enclosure};
1084 let quote = Enclosure::Quote { end: 0 };
1085 let item = |width| Enclosure::Item { width, end: 0 };
1086 let stacks = [
1087 vec![quote, item(2), item(2), quote, item(3)],
1088 vec![item(2), quote, quote, item(4)],
1089 vec![item(2), item(2), item(2)],
1090 vec![quote, Enclosure::Opaque { end: 0 }, item(2)],
1091 ];
1092 let lines = [
1093 "",
1094 " ",
1095 "\t",
1096 ">",
1097 "> ",
1098 ">\t",
1099 "> ",
1100 "> > x",
1101 "> x",
1102 " > > x",
1103 " >",
1104 " > ",
1105 " x",
1106 " x",
1107 " x",
1108 "\tx",
1109 " \t x",
1110 "lazy",
1111 "> > > x",
1112 "> > x",
1113 ];
1114 for stack in &stacks {
1115 let runs = item_runs(stack);
1116 for line in lines {
1117 let expected = (0..=stack.len())
1118 .rev()
1119 .find(|&k| strip_enclosure(line, &stack[..k]).is_some())
1120 .unwrap();
1121 assert_eq!(
1122 continued_depth(line, stack, &runs),
1123 expected,
1124 "line {line:?} in {stack:?}"
1125 );
1126 }
1127 }
1128 }
1129
1130 #[test]
1131 fn test_items_opening_on_one_line_through_a_quote_record_their_widths() {
1132 use super::Enclosure;
1133 let quote = Enclosure::Quote { end: 0 };
1134 let item = |width| Enclosure::Item { width, end: 0 };
1135 let cases = [
1136 ("- > - ```md\n > x\n > ```\n", vec![item(2), quote, item(2)]),
1137 (
1138 "> - > - ```md\n> > x\n> > ```\n",
1139 vec![quote, item(2), quote, item(2)],
1140 ),
1141 ("1. > - ```md\n > x\n > ```\n", vec![item(3), quote, item(2)]),
1142 ("- > 10. ```md\n > x\n > ```\n", vec![item(2), quote, item(4)]),
1143 ("- >\t- ```md\n >\t x\n >\t ```\n", vec![item(2), quote, item(2)]),
1144 (
1145 "- > - - ```md\n > x\n > ```\n",
1146 vec![item(2), quote, item(2), item(2)],
1147 ),
1148 (
1149 "- > > - ```md\n > > x\n > > ```\n",
1150 vec![item(2), quote, quote, item(2)],
1151 ),
1152 ];
1153 for (content, expected) in cases {
1154 let parsed = CodeBlockUtils::detect_code_blocks_and_spans(content);
1155 let enclosing: Vec<Enclosure> = parsed.code_block_details[0]
1156 .enclosing
1157 .iter()
1158 .map(|&enclosure| match enclosure {
1159 Enclosure::Quote { .. } => quote,
1160 Enclosure::Item { width, .. } => item(width),
1161 Enclosure::Opaque { .. } => Enclosure::Opaque { end: 0 },
1162 })
1163 .collect();
1164 assert_eq!(enclosing, expected, "{content:?}");
1165 }
1166 }
1167
1168 #[test]
1169 fn test_table_ranges_and_top_level_blocks_are_recorded() {
1170 let content = "para\n\n|a|b|\n|-|-|\n|1|2|\n\n- item\n\n |c|d|\n |-|-|\n";
1171 let parsed = CodeBlockUtils::detect_code_blocks_and_spans(content);
1172 let text = |&(start, end): &(usize, usize)| &content[start..end];
1173
1174 assert_eq!(parsed.table_ranges.len(), 2);
1175 assert_eq!(text(&parsed.table_ranges[0]), "|a|b|\n|-|-|\n|1|2|\n");
1176 assert!(text(&parsed.table_ranges[1]).starts_with("|c|d|"));
1177
1178 let top: Vec<&str> = parsed.top_level_blocks.iter().map(text).collect();
1181 assert_eq!(top.len(), 3, "{top:?}");
1182 assert_eq!(top[0], "para\n");
1183 assert!(top[2].starts_with("- item"));
1184 }
1185
1186 #[test]
1187 fn test_list_items_record_where_their_content_starts() {
1188 let content = "- - x\n\n- y\n-\n- \n z\n";
1189 let parsed = CodeBlockUtils::detect_code_blocks_and_spans(content);
1190 let starts: Vec<(&str, &str)> = parsed
1191 .list_items
1192 .iter()
1193 .map(|&(start, _, content_start)| (&content[start..=start], &content[content_start..=content_start]))
1194 .collect();
1195 assert_eq!(starts, [("-", "-"), ("-", "x"), ("-", "y"), ("-", "z")]);
1197 }
1198
1199 #[test]
1200 fn test_item_offset_inside_a_character_is_not_read_as_a_marker() {
1201 let content = "ยก\n:ยก\n\t* ยก";
1204 assert!(!content.is_char_boundary(5));
1205 assert!(item_content_start(content, 5).is_none());
1206
1207 let parsed = CodeBlockUtils::detect_code_blocks_and_spans(content);
1208 assert_eq!(parsed.list_items.len(), 1);
1209 }
1210
1211 #[test]
1212 fn test_no_table_ranges_without_a_table() {
1213 let parsed = CodeBlockUtils::detect_code_blocks_and_spans("a|b\n|-|\n\n |a|b|\n |-|-|\n");
1214 assert!(parsed.table_ranges.is_empty());
1215 }
1216
1217 use super::*;
1218
1219 #[test]
1220 fn test_detect_fenced_code_blocks() {
1221 let content = "Some text\n```\ncode here\n```\nMore text";
1226 let blocks = CodeBlockUtils::detect_code_blocks(content);
1227 assert_eq!(blocks.len(), 1);
1229
1230 let fenced_block = blocks
1232 .iter()
1233 .find(|(start, end)| end - start > 10 && content[*start..*end].contains("code here"));
1234 assert!(fenced_block.is_some());
1235
1236 let content = "Some text\n~~~\ncode here\n~~~\nMore text";
1238 let blocks = CodeBlockUtils::detect_code_blocks(content);
1239 assert_eq!(blocks.len(), 1);
1240 assert_eq!(&content[blocks[0].0..blocks[0].1], "~~~\ncode here\n~~~");
1241
1242 let content = "Text\n```\ncode1\n```\nMiddle\n~~~\ncode2\n~~~\nEnd";
1244 let blocks = CodeBlockUtils::detect_code_blocks(content);
1245 assert_eq!(blocks.len(), 2);
1247 }
1248
1249 #[test]
1250 fn test_detect_code_blocks_with_language() {
1251 let content = "Text\n```rust\nfn main() {}\n```\nMore";
1253 let blocks = CodeBlockUtils::detect_code_blocks(content);
1254 assert_eq!(blocks.len(), 1);
1256 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("fn main"));
1258 assert!(fenced.is_some());
1259 }
1260
1261 #[test]
1262 fn test_unclosed_code_block() {
1263 let content = "Text\n```\ncode here\nno closing fence";
1265 let blocks = CodeBlockUtils::detect_code_blocks(content);
1266 assert_eq!(blocks.len(), 1);
1267 assert_eq!(blocks[0].1, content.len());
1268 }
1269
1270 #[test]
1271 fn test_indented_code_blocks() {
1272 let content = "Paragraph\n\n code line 1\n code line 2\n\nMore text";
1274 let blocks = CodeBlockUtils::detect_code_blocks(content);
1275 assert_eq!(blocks.len(), 1);
1276 assert!(content[blocks[0].0..blocks[0].1].contains("code line 1"));
1277 assert!(content[blocks[0].0..blocks[0].1].contains("code line 2"));
1278
1279 let content = "Paragraph\n\n\tcode with tab\n\tanother line\n\nText";
1281 let blocks = CodeBlockUtils::detect_code_blocks(content);
1282 assert_eq!(blocks.len(), 1);
1283 }
1284
1285 #[test]
1286 fn test_indented_code_requires_blank_line() {
1287 let content = "Paragraph\n indented but not code\nMore text";
1289 let blocks = CodeBlockUtils::detect_code_blocks(content);
1290 assert_eq!(blocks.len(), 0);
1291
1292 let content = "Paragraph\n\n now it's code\nMore text";
1294 let blocks = CodeBlockUtils::detect_code_blocks(content);
1295 assert_eq!(blocks.len(), 1);
1296 }
1297
1298 #[test]
1299 fn test_indented_content_with_list_markers_is_code_block() {
1300 let content = "List:\n\n - Item 1\n - Item 2\n * Item 3\n + Item 4";
1305 let blocks = CodeBlockUtils::detect_code_blocks(content);
1306 assert_eq!(blocks.len(), 1); let content = "List:\n\n 1. First\n 2. Second";
1310 let blocks = CodeBlockUtils::detect_code_blocks(content);
1311 assert_eq!(blocks.len(), 1); }
1313
1314 #[test]
1315 fn test_actual_list_items_not_code_blocks() {
1316 let content = "- Item 1\n- Item 2\n* Item 3";
1318 let blocks = CodeBlockUtils::detect_code_blocks(content);
1319 assert_eq!(blocks.len(), 0);
1320
1321 let content = "- Item 1\n - Nested item\n- Item 2";
1323 let blocks = CodeBlockUtils::detect_code_blocks(content);
1324 assert_eq!(blocks.len(), 0);
1325 }
1326
1327 #[test]
1328 fn test_inline_code_spans_not_detected() {
1329 let content = "Text with `inline code` here";
1331 let blocks = CodeBlockUtils::detect_code_blocks(content);
1332 assert_eq!(blocks.len(), 0); let content = "Text with ``code with ` backtick`` here";
1336 let blocks = CodeBlockUtils::detect_code_blocks(content);
1337 assert_eq!(blocks.len(), 0); let content = "Has `code1` and `code2` spans";
1341 let blocks = CodeBlockUtils::detect_code_blocks(content);
1342 assert_eq!(blocks.len(), 0); }
1344
1345 #[test]
1346 fn test_unclosed_code_span() {
1347 let content = "Text with `unclosed code span";
1349 let blocks = CodeBlockUtils::detect_code_blocks(content);
1350 assert_eq!(blocks.len(), 0);
1351
1352 let content = "Text with ``one style` different close";
1354 let blocks = CodeBlockUtils::detect_code_blocks(content);
1355 assert_eq!(blocks.len(), 0);
1356 }
1357
1358 #[test]
1359 fn test_mixed_code_blocks_and_spans() {
1360 let content = "Has `span1` text\n```\nblock\n```\nand `span2`";
1361 let blocks = CodeBlockUtils::detect_code_blocks(content);
1362 assert_eq!(blocks.len(), 1);
1364
1365 assert!(blocks.iter().any(|(s, e)| content[*s..*e].contains("block")));
1367 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span1`"));
1369 assert!(!blocks.iter().any(|(s, e)| &content[*s..*e] == "`span2`"));
1370 }
1371
1372 #[test]
1373 fn test_is_in_code_block_or_span() {
1374 let blocks = vec![(10, 20), (30, 40), (50, 60)];
1375
1376 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 15));
1378 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 35));
1379 assert!(CodeBlockUtils::is_in_code_block_or_span(&blocks, 55));
1380
1381 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));
1387 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 25));
1388 assert!(!CodeBlockUtils::is_in_code_block_or_span(&blocks, 65));
1389 }
1390
1391 #[test]
1392 fn test_empty_content() {
1393 let blocks = CodeBlockUtils::detect_code_blocks("");
1394 assert_eq!(blocks.len(), 0);
1395 }
1396
1397 #[test]
1398 fn test_code_block_at_start() {
1399 let content = "```\ncode\n```\nText after";
1400 let blocks = CodeBlockUtils::detect_code_blocks(content);
1401 assert_eq!(blocks.len(), 1);
1403 assert_eq!(blocks[0].0, 0); }
1405
1406 #[test]
1407 fn test_code_block_at_end() {
1408 let content = "Text before\n```\ncode\n```";
1409 let blocks = CodeBlockUtils::detect_code_blocks(content);
1410 assert_eq!(blocks.len(), 1);
1412 let fenced = blocks.iter().find(|(s, e)| content[*s..*e].contains("code"));
1414 assert!(fenced.is_some());
1415 }
1416
1417 #[test]
1418 fn test_nested_fence_markers() {
1419 let content = "Text\n````\n```\nnested\n```\n````\nAfter";
1421 let blocks = CodeBlockUtils::detect_code_blocks(content);
1422 assert!(!blocks.is_empty());
1424 let outer = blocks.iter().find(|(s, e)| content[*s..*e].contains("nested"));
1426 assert!(outer.is_some());
1427 }
1428
1429 #[test]
1430 fn test_indented_code_with_blank_lines() {
1431 let content = "Text\n\n line1\n\n line2\n\nAfter";
1433 let blocks = CodeBlockUtils::detect_code_blocks(content);
1434 assert!(!blocks.is_empty());
1436 let all_content: String = blocks
1438 .iter()
1439 .map(|(s, e)| &content[*s..*e])
1440 .collect::<Vec<_>>()
1441 .join("");
1442 assert!(all_content.contains("line1") || content[blocks[0].0..blocks[0].1].contains("line1"));
1443 }
1444
1445 #[test]
1446 fn test_code_span_with_spaces() {
1447 let content = "Text ` code with spaces ` more";
1449 let blocks = CodeBlockUtils::detect_code_blocks(content);
1450 assert_eq!(blocks.len(), 0); }
1452
1453 #[test]
1454 fn test_fenced_block_with_info_string() {
1455 let content = "```rust,no_run,should_panic\ncode\n```";
1457 let blocks = CodeBlockUtils::detect_code_blocks(content);
1458 assert_eq!(blocks.len(), 1);
1460 assert_eq!(blocks[0].0, 0);
1461 }
1462
1463 #[test]
1464 fn test_indented_fences_not_code_blocks() {
1465 let content = "Text\n ```\n code\n ```\nAfter";
1467 let blocks = CodeBlockUtils::detect_code_blocks(content);
1468 assert_eq!(blocks.len(), 1);
1470 }
1471
1472 #[test]
1474 fn test_backticks_in_info_string_not_code_block() {
1475 let content = "```something```\n\n```bash\n# comment\n```";
1481 let blocks = CodeBlockUtils::detect_code_blocks(content);
1482 assert_eq!(blocks.len(), 1);
1484 assert!(content[blocks[0].0..blocks[0].1].contains("# comment"));
1486 }
1487
1488 #[test]
1489 fn test_issue_175_reproduction() {
1490 let content = "```something```\n\n```bash\n# Have a parrot\necho \"๐ฆ\"\n```";
1492 let blocks = CodeBlockUtils::detect_code_blocks(content);
1493 assert_eq!(blocks.len(), 1);
1495 assert!(content[blocks[0].0..blocks[0].1].contains("Have a parrot"));
1496 }
1497
1498 #[test]
1499 fn test_tilde_fence_allows_tildes_in_info_string() {
1500 let content = "~~~abc~~~\ncode content\n~~~";
1503 let blocks = CodeBlockUtils::detect_code_blocks(content);
1504 assert_eq!(blocks.len(), 1);
1506 }
1507
1508 #[test]
1509 fn test_nested_longer_fence_contains_shorter() {
1510 let content = "````\n```\nnested content\n```\n````";
1512 let blocks = CodeBlockUtils::detect_code_blocks(content);
1513 assert_eq!(blocks.len(), 1);
1514 assert!(content[blocks[0].0..blocks[0].1].contains("nested content"));
1515 }
1516
1517 #[test]
1518 fn test_mixed_fence_types() {
1519 let content = "~~~\n```\nmixed content\n~~~";
1521 let blocks = CodeBlockUtils::detect_code_blocks(content);
1522 assert_eq!(blocks.len(), 1);
1523 assert!(content[blocks[0].0..blocks[0].1].contains("mixed content"));
1524 }
1525
1526 #[test]
1527 fn test_indented_block_content_column_is_its_containers() {
1528 let cases = [
1529 ("Text\n\n code\n", 0),
1530 ("- a\n\n code\n", 2),
1531 ("1. a\n\n code\n", 3),
1532 ("1. a\n\nb\n\n code\n", 0),
1534 ("1. a\nb\nc\nd\ne\nf\ng\n\n code\n", 3),
1536 ("> quoted\n", 2),
1537 ("- a\n\n\t code\n", 2),
1538 ("1. a\n\n\t\tcode\n", 3),
1540 (">\t\tcode\n", 2),
1541 ];
1542 for (content, expected) in cases {
1543 let result = CodeBlockUtils::detect_code_blocks_and_spans(content);
1544 let columns: Vec<_> = result.code_block_details.iter().map(|d| d.content_column).collect();
1545 assert_eq!(columns, [Some(expected)], "content column for {content:?}");
1546 }
1547
1548 let fenced = CodeBlockUtils::detect_code_blocks_and_spans("- a\n\n ```\n code\n ```\n");
1549 let columns: Vec<_> = fenced.code_block_details.iter().map(|d| d.content_column).collect();
1550 assert_eq!(columns, [None]);
1551 }
1552
1553 #[test]
1554 fn test_indented_block_in_rejected_definition_takes_its_columns_from_the_text_reading() {
1555 let cases = [
1556 ("term\n:warning:\n\n x\n", Some(0)),
1558 ("- a\n\n term\n :warning:\n\n x\n", Some(2)),
1559 ("term\n: def\n\n x\n", Some(2)),
1561 ("term\n: def\n\n t2\n :warning:\n\n x\n", None),
1564 ];
1565 for (content, expected) in cases {
1566 let result = CodeBlockUtils::detect_code_blocks_and_spans(content);
1567 let columns: Vec<_> = result.code_block_details.iter().map(|d| d.content_column).collect();
1568 assert_eq!(columns, [expected], "content column for {content:?}");
1569 }
1570 }
1571
1572 #[test]
1573 fn tab_indented_definition_markers_after_utf8_keep_valid_ranges() {
1574 for newline in ["\n", "\r", "\r\n"] {
1575 for term in ["", "a", "ยก", "๐ฆ"] {
1576 for text in ["b", "\u{e000}", "๐ฆ"] {
1577 for tail in ["\t:", "\t: next", "\t: *next*", "\t: `next`"] {
1578 let source = format!("{term}{newline}:{text}{newline}{tail}");
1579 let result = CodeBlockUtils::detect_code_blocks_and_spans(&source);
1580 assert_eq!(
1584 result.definition_items.len(),
1585 usize::from(!term.is_empty()),
1586 "{source:?}"
1587 );
1588 assert_eq!(
1589 result.definition_texts.len(),
1590 usize::from(!term.is_empty() && tail != "\t:"),
1591 "{source:?}"
1592 );
1593 let parent_colon = term.len() + newline.len();
1594 let nested_colon = source.len() - tail.len() + 1;
1595 for &(start, end) in &result.definition_items {
1596 assert_eq!((start, end), (parent_colon + 1, source.len()), "{source:?}");
1597 assert!(source.get(start..end).is_some(), "{source:?}: {start}..{end}");
1598 }
1599 for detail in &result.definition_texts {
1600 assert!(source.get(detail.start..detail.end).is_some(), "{source:?}: {detail:?}");
1601 assert_eq!(detail.definition_start, nested_colon, "{source:?}");
1602 }
1603 if !term.is_empty() {
1604 assert!(
1605 result
1606 .rejected_definition_ranges
1607 .iter()
1608 .any(|&(start, end)| start <= parent_colon && parent_colon < end),
1609 "{source:?}"
1610 );
1611 }
1612 for &(start, end) in &result.rejected_definition_ranges {
1613 assert!(source.get(start..end).is_some(), "{source:?}: {start}..{end}");
1614 }
1615 let ctx = crate::lint_context::LintContext::new(
1616 &source,
1617 crate::config::MarkdownFlavor::Standard,
1618 None,
1619 );
1620 assert_eq!(ctx.content, source);
1621 }
1622 }
1623 }
1624 }
1625 assert!(!super::opens_definition("ยก", 1));
1626 assert!(!super::opens_definition(": text", usize::MAX));
1627 assert!(super::opens_definition("\t: text", 0));
1628 }
1629
1630 #[test]
1631 fn test_indented_code_in_list_issue_276() {
1632 let content = r#"1. First item
16342. Second item with code:
1635
1636 # This is a code block in a list
1637 print("Hello, world!")
1638
16394. Third item"#;
1640
1641 let blocks = CodeBlockUtils::detect_code_blocks(content);
1642 assert!(!blocks.is_empty(), "Should detect indented code block inside list");
1644
1645 let all_content: String = blocks
1647 .iter()
1648 .map(|(s, e)| &content[*s..*e])
1649 .collect::<Vec<_>>()
1650 .join("");
1651 assert!(
1652 all_content.contains("code block in a list") || all_content.contains("print"),
1653 "Detected block should contain the code content: {all_content:?}"
1654 );
1655 }
1656
1657 #[test]
1660 fn test_is_in_code_block_empty_blocks() {
1661 assert!(!CodeBlockUtils::is_in_code_block(&[], 0));
1662 assert!(!CodeBlockUtils::is_in_code_block(&[], 100));
1663 assert!(!CodeBlockUtils::is_in_code_block(&[], usize::MAX));
1664 }
1665
1666 #[test]
1667 fn test_is_in_code_block_single_range() {
1668 let blocks = [(10, 20)];
1669 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
1670 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 9));
1671 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
1672 assert!(CodeBlockUtils::is_in_code_block(&blocks, 15));
1673 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
1674 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 20));
1676 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 21));
1677 }
1678
1679 #[test]
1680 fn test_is_in_code_block_multiple_ranges() {
1681 let blocks = [(5, 10), (20, 30), (50, 60)];
1682 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 0));
1684 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
1685 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
1687 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
1688 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 10));
1690 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 15));
1691 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 19));
1692 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
1694 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
1695 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
1697 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 49));
1698 assert!(CodeBlockUtils::is_in_code_block(&blocks, 50));
1700 assert!(CodeBlockUtils::is_in_code_block(&blocks, 59));
1701 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 60));
1703 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 1000));
1704 }
1705
1706 #[test]
1707 fn test_is_in_code_block_adjacent_ranges() {
1708 let blocks = [(0, 10), (10, 20), (20, 30)];
1710 assert!(CodeBlockUtils::is_in_code_block(&blocks, 0));
1711 assert!(CodeBlockUtils::is_in_code_block(&blocks, 9));
1712 assert!(CodeBlockUtils::is_in_code_block(&blocks, 10));
1713 assert!(CodeBlockUtils::is_in_code_block(&blocks, 19));
1714 assert!(CodeBlockUtils::is_in_code_block(&blocks, 20));
1715 assert!(CodeBlockUtils::is_in_code_block(&blocks, 29));
1716 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 30));
1717 }
1718
1719 #[test]
1720 fn test_is_in_code_block_single_byte_range() {
1721 let blocks = [(5, 6)];
1722 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 4));
1723 assert!(CodeBlockUtils::is_in_code_block(&blocks, 5));
1724 assert!(!CodeBlockUtils::is_in_code_block(&blocks, 6));
1725 }
1726
1727 #[test]
1728 fn test_is_in_code_block_matches_linear_scan() {
1729 let content = "# Heading\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nSome text\n\n```\nmore code\n```\n\nEnd\n";
1732 let blocks = CodeBlockUtils::detect_code_blocks(content);
1733
1734 for pos in 0..content.len() {
1735 let binary = CodeBlockUtils::is_in_code_block(&blocks, pos);
1736 let linear = blocks.iter().any(|&(s, e)| pos >= s && pos < e);
1737 assert_eq!(
1738 binary, linear,
1739 "Mismatch at pos {pos}: binary={binary}, linear={linear}, blocks={blocks:?}"
1740 );
1741 }
1742 }
1743
1744 #[test]
1745 fn test_is_in_code_block_at_range_boundaries() {
1746 let blocks = [(100, 200), (300, 400), (500, 600)];
1748 for &(start, end) in &blocks {
1749 assert!(
1750 !CodeBlockUtils::is_in_code_block(&blocks, start - 1),
1751 "pos={} should be outside",
1752 start - 1
1753 );
1754 assert!(
1755 CodeBlockUtils::is_in_code_block(&blocks, start),
1756 "pos={start} should be inside"
1757 );
1758 assert!(
1759 CodeBlockUtils::is_in_code_block(&blocks, end - 1),
1760 "pos={} should be inside",
1761 end - 1
1762 );
1763 assert!(
1764 !CodeBlockUtils::is_in_code_block(&blocks, end),
1765 "pos={end} should be outside"
1766 );
1767 }
1768 }
1769}
1770
1771#[cfg(test)]
1775mod closed_fence_tests {
1776 use super::*;
1777
1778 fn closed_of(content: &str) -> Vec<bool> {
1779 CodeBlockUtils::detect_code_blocks_and_spans(content)
1780 .code_block_details
1781 .iter()
1782 .filter(|detail| detail.is_fenced)
1783 .map(|detail| detail.closed)
1784 .collect()
1785 }
1786
1787 #[test]
1788 fn a_block_ended_by_a_closing_fence_is_closed() {
1789 for content in [
1790 "```\nx\n```\n",
1791 "```\nx\n```",
1792 "```\n```\n",
1793 "~~~\nx\n~~~\n",
1794 "````\nx\n`````\n",
1795 "```\nx\n``` \n",
1796 "> ```\n> x\n> ```\nfoo\n",
1797 "> ```\n> ```\nfoo\n",
1798 "-\n ```\n x\n ```\n",
1799 "- ```\n x\n\n ```\n",
1800 "```\n\n\n```\n",
1801 " ```\nx\n ```\n",
1802 ] {
1803 assert_eq!(closed_of(content), vec![true], "{content:?}");
1804 }
1805 }
1806
1807 #[test]
1808 fn a_block_the_document_or_container_ends_is_not_closed() {
1809 for content in [
1810 "```\nx\n",
1811 "```\nx",
1812 "```",
1813 "```\n",
1814 "> ```\nfoo\n",
1815 "> ```\n> x\nfoo\n",
1816 "> ```\n> ~~~\nfoo\n",
1818 "```\n~~~\n",
1819 "> ````\n> ```\nfoo\n",
1820 "> ```\n> ```\nfoo\n",
1821 "```\n ```\n",
1822 "> ```\n> \u{a0}```\nfoo\n",
1823 "> ```\n> ```x\nfoo\n",
1824 "> ```\n> ```\u{a0}\nfoo\n",
1825 "> ```\n> ",
1828 "```\n ",
1829 "> ```\n> ~~~\n> ",
1830 "- ```\nfoo\n",
1831 ] {
1832 assert_eq!(closed_of(content), vec![false], "{content:?}");
1833 }
1834 }
1835}