1pub mod types;
2pub(crate) use heading_detection::is_paragraph_text_line;
3pub(crate) use link_parser::{image_pattern, link_pattern};
4pub use types::*;
5
6mod bracket_math;
7mod element_parsers;
8mod flavor_detection;
9mod heading_detection;
10mod line_computation;
11mod link_parser;
12mod list_blocks;
13mod mdx;
14#[cfg(test)]
15mod tests;
16
17use crate::config::MarkdownFlavor;
18use crate::inline_config::InlineConfig;
19use crate::rules::front_matter_utils::FrontMatterUtils;
20use crate::utils::code_block_utils::{CodeBlockDetail, CodeBlockUtils};
21use crate::utils::range_utils::byte_to_char_count;
22use std::collections::HashMap;
23use std::ops::Range;
24use std::path::{Path, PathBuf};
25
26#[derive(Debug, Clone)]
29pub struct LinkTargetPolicy {
30 supplied_paths: Arc<std::collections::HashSet<PathBuf>>,
31 allow_disk_fallback: bool,
32}
33
34impl LinkTargetPolicy {
35 pub fn open_world<I, P>(paths: I) -> Self
38 where
39 I: IntoIterator<Item = P>,
40 P: AsRef<Path>,
41 {
42 Self::from_paths(paths, true)
43 }
44
45 pub fn closed_world<I, P>(paths: I) -> Self
47 where
48 I: IntoIterator<Item = P>,
49 P: AsRef<Path>,
50 {
51 Self::from_paths(paths, false)
52 }
53
54 fn from_paths<I, P>(paths: I, allow_disk_fallback: bool) -> Self
55 where
56 I: IntoIterator<Item = P>,
57 P: AsRef<Path>,
58 {
59 let mut roots = Vec::new();
60 if let Ok(cwd) = std::env::current_dir() {
61 if let Ok(canonical_cwd) = cwd.canonicalize()
62 && canonical_cwd != cwd
63 {
64 roots.push(canonical_cwd);
65 }
66 roots.push(cwd);
67 }
68 Self::from_paths_with_roots(paths, allow_disk_fallback, roots)
69 }
70
71 fn from_paths_with_roots<I, P, R, Q>(paths: I, allow_disk_fallback: bool, roots: R) -> Self
72 where
73 I: IntoIterator<Item = P>,
74 P: AsRef<Path>,
75 R: IntoIterator<Item = Q>,
76 Q: AsRef<Path>,
77 {
78 let roots: Vec<PathBuf> = roots
79 .into_iter()
80 .map(|root| crate::workspace_index::normalize_relative_path(root.as_ref()))
81 .collect();
82 let mut supplied_paths = std::collections::HashSet::new();
83 for path in paths {
84 let path = path.as_ref();
85 supplied_paths.insert(crate::workspace_index::normalize_relative_path(path));
86
87 if path.is_relative() {
88 for root in &roots {
89 supplied_paths.insert(crate::workspace_index::normalize_relative_path(&root.join(path)));
90 }
91 } else {
92 for source_root in &roots {
93 if let Ok(relative) = path.strip_prefix(source_root) {
94 for root in &roots {
95 supplied_paths
96 .insert(crate::workspace_index::normalize_relative_path(&root.join(relative)));
97 }
98 }
99 }
100 }
101 }
102 Self {
103 supplied_paths: Arc::new(supplied_paths),
104 allow_disk_fallback,
105 }
106 }
107
108 pub fn contains(&self, path: &Path) -> bool {
109 self.supplied_paths
110 .contains(&crate::workspace_index::normalize_relative_path(path))
111 }
112
113 pub(crate) fn resolve_supplied(&self, path: &Path) -> Option<PathBuf> {
114 let normalized = crate::workspace_index::normalize_relative_path(path);
115 if self.supplied_paths.contains(&normalized) {
116 return Some(normalized);
117 }
118
119 if path.extension().is_none() {
120 for extension in ["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"] {
121 let candidate = crate::workspace_index::normalize_relative_path(&path.with_extension(extension));
122 if self.supplied_paths.contains(&candidate) {
123 return Some(candidate);
124 }
125 }
126 }
127
128 None
129 }
130
131 pub fn contains_with_markdown_extension(&self, path: &Path) -> bool {
132 self.resolve_supplied(path).is_some()
133 }
134
135 pub fn allow_disk_fallback(&self) -> bool {
136 self.allow_disk_fallback
137 }
138}
139
140#[cfg(not(target_arch = "wasm32"))]
142macro_rules! profile_section {
143 ($name:expr, $profile:expr, $code:expr) => {{
144 let start = std::time::Instant::now();
145 let result = $code;
146 if $profile {
147 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
148 }
149 result
150 }};
151}
152
153fn build_commonmark_ordered_lists(
154 lines: &[LineInfo],
155 line_to_list: &crate::utils::code_block_utils::LineToListMap,
156 list_start_values: &crate::utils::code_block_utils::ListStartValues,
157) -> Vec<CommonMarkOrderedListInfo> {
158 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
159
160 for (&line_num, &list_id) in line_to_list {
161 let is_ordered_item = line_num
162 .checked_sub(1)
163 .and_then(|index| lines.get(index))
164 .and_then(|line| line.list_item.as_deref())
165 .is_some_and(|item| item.is_ordered);
166 if is_ordered_item {
167 grouped_lines.entry(list_id).or_default().push(line_num);
168 }
169 }
170
171 let mut lists: Vec<_> = grouped_lines
172 .into_iter()
173 .map(|(list_id, mut item_lines)| {
174 item_lines.sort_unstable();
175 CommonMarkOrderedListInfo {
176 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
177 item_lines,
178 }
179 })
180 .collect();
181 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
182 lists
183}
184
185#[cfg(target_arch = "wasm32")]
186macro_rules! profile_section {
187 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
188}
189
190pub(super) struct SkipByteRanges<'a> {
193 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
194 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
195 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
196 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
197}
198
199use std::sync::{Arc, OnceLock};
200
201pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
203
204pub(super) type ByteRanges = Vec<(usize, usize)>;
206
207pub struct LintContext<'a> {
208 pub content: &'a str,
209 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
211 pub code_blocks: Vec<(usize, usize)>, pub code_block_details: Vec<CodeBlockDetail>, pub strong_spans: Vec<crate::utils::code_block_utils::StrongSpanDetail>, line_to_list: crate::utils::code_block_utils::LineToListMap, list_start_values: crate::utils::code_block_utils::ListStartValues, commonmark_ordered_lists_cache: OnceLock<Vec<CommonMarkOrderedListInfo>>, pub lines: Vec<LineInfo>, blockquote_headings: Vec<Option<Box<HeadingInfo>>>, links: Vec<ParsedLink<'a>>, images: Vec<ParsedImage<'a>>, broken_links: Vec<BrokenLinkInfo>, footnote_refs: Vec<FootnoteRef>, reference_defs: Vec<ReferenceDef>, reference_defs_map: HashMap<String, usize>, code_spans_cache: OnceLock<Arc<Vec<CodeSpan>>>, math_spans_cache: OnceLock<Arc<Vec<MathSpan>>>, bracket_math_cache: OnceLock<bracket_math::BracketDisplayMathLines>,
228 math_byte_ranges_cache: OnceLock<Vec<(usize, usize)>>, pub list_blocks: Vec<ListBlock>, pub char_frequency: CharFrequency, html_tags_cache: OnceLock<Arc<Vec<HtmlTag>>>, jsx_component_tags_cache: OnceLock<Arc<Vec<HtmlTag>>>, emphasis_spans_cache: OnceLock<Arc<Vec<EmphasisSpan>>>, bare_urls_cache: OnceLock<Arc<Vec<BareUrl>>>, has_mixed_list_nesting_cache: OnceLock<bool>, html_comment_ranges: Vec<crate::utils::skip_context::ByteRange>, pub table_blocks: Vec<crate::utils::table_utils::TableBlock>, line_index: crate::utils::range_utils::LineIndex<'a>, jinja_ranges: Vec<(usize, usize)>, pub flavor: MarkdownFlavor, source_file: Option<PathBuf>, link_target_policy: Option<LinkTargetPolicy>, jsx_expression_ranges: Vec<(usize, usize)>, mdx_comment_ranges: Vec<(usize, usize)>, citation_ranges: Vec<crate::utils::skip_context::ByteRange>, pandoc_div_ranges: Vec<crate::utils::skip_context::ByteRange>, colon_fence_details: Vec<CodeBlockDetail>, inline_footnote_ranges: Vec<crate::utils::skip_context::ByteRange>, pandoc_header_slugs: std::collections::HashSet<String>, example_list_marker_ranges: Vec<crate::utils::skip_context::ByteRange>, example_reference_ranges: Vec<crate::utils::skip_context::ByteRange>, sub_super_ranges: Vec<crate::utils::skip_context::ByteRange>, inline_code_attr_ranges: Vec<crate::utils::skip_context::ByteRange>, bracketed_span_ranges: Vec<crate::utils::skip_context::ByteRange>, line_block_ranges: Vec<crate::utils::skip_context::ByteRange>, pipe_table_caption_ranges: Vec<crate::utils::skip_context::ByteRange>, pandoc_metadata_ranges: Vec<crate::utils::skip_context::ByteRange>, grid_table_ranges: Vec<crate::utils::skip_context::ByteRange>, multi_line_table_ranges: Vec<crate::utils::skip_context::ByteRange>, shortcode_ranges: Vec<(usize, usize)>, link_title_ranges: Vec<(usize, usize)>, code_span_byte_ranges: Vec<(usize, usize)>, inline_config: InlineConfig, obsidian_comment_ranges: Vec<(usize, usize)>, unterminated_html_comment: Option<usize>, unterminated_obsidian_comment: Option<usize>, lazy_cont_lines_cache: OnceLock<Arc<Vec<LazyContLine>>>, myst_directive_ranges: Vec<(usize, usize)>, myst_comment_ranges: Vec<(usize, usize)>, myst_role_ranges: Vec<(usize, usize)>, front_matter_end: usize, }
273
274pub struct CodeRanges {
276 pub blocks: Vec<(usize, usize)>,
278 pub spans: Vec<(usize, usize)>,
280}
281
282pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
293 let ctx = LintContext::new(content, flavor, None);
294 CodeRanges {
295 spans: code_span_byte_ranges(&ctx.code_spans()),
296 blocks: ctx.code_blocks,
297 }
298}
299
300pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
302 code_spans
303 .iter()
304 .map(|span| (span.byte_offset, span.byte_end))
305 .collect()
306}
307
308impl<'a> LintContext<'a> {
309 pub fn source_file(&self) -> Option<&Path> {
314 self.source_file.as_deref()
315 }
316
317 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
318 self.link_target_policy.as_ref()
319 }
320
321 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
322 self.link_target_policy = Some(policy);
323 self
324 }
325
326 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
327 #[cfg(not(target_arch = "wasm32"))]
328 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
329
330 let line_offsets = profile_section!("Line offsets", profile, {
331 let mut offsets = vec![0];
332 for (i, c) in content.char_indices() {
333 if c == '\n' {
334 offsets.push(i + 1);
335 }
336 }
337 offsets
338 });
339
340 let content_lines: Vec<&str> = content.lines().collect();
342
343 #[allow(clippy::disallowed_methods)]
347 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
348
349 let parse_result = profile_section!(
351 "Code blocks",
352 profile,
353 CodeBlockUtils::detect_code_blocks_and_spans(content)
354 );
355 let mut code_blocks = parse_result.code_blocks;
356 let mut code_span_ranges = parse_result.code_spans;
357 let code_block_details = parse_result.code_block_details;
358 let strong_spans = parse_result.strong_spans;
359 let line_to_list = parse_result.line_to_list;
360 let list_start_values = parse_result.list_start_values;
361 let html_blocks = parse_result.html_blocks;
362
363 let containers = profile_section!(
366 "Container lines",
367 profile,
368 flavor_detection::detect_container_lines(&content_lines, flavor)
369 );
370
371 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
380 .iter()
381 .flat_map(|detail| {
382 if detail.is_fenced {
383 return vec![(detail.start, detail.end)];
384 }
385 let start_line = line_offsets
386 .partition_point(|&offset| offset <= detail.start)
387 .saturating_sub(1);
388 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
389 containers
390 .code_line_spans_in(start_line..end_line)
391 .into_iter()
392 .map(|span| {
393 let start = line_offsets[span.start].max(detail.start);
394 let end = line_offsets
395 .get(span.end)
396 .copied()
397 .unwrap_or(content.len())
398 .min(detail.end);
399 (start, end)
400 })
401 .collect()
402 })
403 .collect();
404 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
410 let html_comment_scan = profile_section!(
411 "HTML comment ranges",
412 profile,
413 crate::utils::skip_context::scan_html_comments(
414 content,
415 &code_span_ranges,
416 &comment_code_block_ranges,
417 body_start
418 )
419 );
420 let mut html_comment_ranges = html_comment_scan.ranges;
421 let unterminated_html_comment = html_comment_scan.unterminated;
422
423 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
427 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
428 Vec::new()
429 } else {
430 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
431 }
432 });
433
434 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
436 if flavor.is_pandoc_compatible() {
437 crate::utils::pandoc::detect_div_block_ranges(content)
438 } else {
439 Vec::new()
440 }
441 });
442
443 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
445 if flavor == MarkdownFlavor::MkDocs {
446 crate::utils::pymdown_blocks::detect_block_ranges(content)
447 } else {
448 Vec::new()
449 }
450 });
451
452 let skip_ranges = SkipByteRanges {
455 html_comment_ranges: &html_comment_ranges,
456 autodoc_ranges: &autodoc_ranges,
457 pandoc_div_ranges: &pandoc_div_ranges,
458 pymdown_block_ranges: &pymdown_block_ranges,
459 };
460 let (mut lines, emphasis_spans) = profile_section!(
461 "Basic line info",
462 profile,
463 line_computation::compute_basic_line_info(
464 content,
465 &content_lines,
466 &line_offsets,
467 &code_blocks,
468 flavor,
469 &skip_ranges,
470 front_matter_end,
471 )
472 );
473
474 profile_section!(
476 "HTML blocks",
477 profile,
478 heading_detection::detect_html_blocks(content, &mut lines)
479 );
480
481 profile_section!(
483 "ESM blocks",
484 profile,
485 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
486 );
487
488 profile_section!(
490 "JSX block detection",
491 profile,
492 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
493 );
494
495 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
497 "JSX/MDX detection",
498 profile,
499 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
500 );
501
502 profile_section!(
507 "Markdown-in-HTML blocks",
508 profile,
509 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
510 );
511
512 let mdx_context = if flavor == MarkdownFlavor::MDX {
513 mdx::MdxContext::parse(content, &lines)
514 } else {
515 None
516 };
517 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
518 mdx.apply_lines(&mut lines);
519 code_blocks.clone_from(&mdx.code_blocks);
520 code_span_ranges.clone_from(&mdx.code_spans);
521 (mdx.expressions.clone(), mdx.comments.clone())
522 } else {
523 (jsx_expression_ranges, mdx_comment_ranges)
524 };
525
526 profile_section!(
528 "MkDocs constructs",
529 profile,
530 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
531 );
532
533 profile_section!(
538 "Footnote definitions",
539 profile,
540 detect_footnote_definitions(content, &mut lines, &line_offsets)
541 );
542
543 {
546 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
547 for &(start, end) in &code_blocks {
548 let start_line = line_offsets
549 .partition_point(|&offset| offset <= start)
550 .saturating_sub(1);
551 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
552
553 let mut sub_start: Option<usize> = None;
554 for (i, &offset) in line_offsets[start_line..end_line]
555 .iter()
556 .enumerate()
557 .map(|(j, o)| (j + start_line, o))
558 {
559 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
560 if is_real_code && sub_start.is_none() {
561 let byte_start = if i == start_line { start } else { offset };
562 sub_start = Some(byte_start);
563 } else if !is_real_code && sub_start.is_some() {
564 new_code_blocks.push((sub_start.unwrap(), offset));
565 sub_start = None;
566 }
567 }
568 if let Some(s) = sub_start {
569 new_code_blocks.push((s, end));
570 }
571 }
572 code_blocks = new_code_blocks;
573 }
574
575 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
583 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
584 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
585 for &(start, end) in &code_blocks {
586 let start_line = line_offsets
587 .partition_point(|&offset| offset <= start)
588 .saturating_sub(1);
589 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
590
591 let mut sub_start: Option<usize> = None;
593 for (i, &offset) in line_offsets[start_line..end_line]
594 .iter()
595 .enumerate()
596 .map(|(j, o)| (j + start_line, o))
597 {
598 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
599 if is_real_code && sub_start.is_none() {
600 let byte_start = if i == start_line { start } else { offset };
601 sub_start = Some(byte_start);
602 } else if !is_real_code && sub_start.is_some() {
603 new_code_blocks.push((sub_start.unwrap(), offset));
604 sub_start = None;
605 }
606 }
607 if let Some(s) = sub_start {
608 new_code_blocks.push((s, end));
609 }
610 }
611 code_blocks = new_code_blocks;
612 }
613
614 if flavor.supports_jsx() {
618 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
619 for &(start, end) in &code_blocks {
620 let start_line = line_offsets
621 .partition_point(|&offset| offset <= start)
622 .saturating_sub(1);
623 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
624
625 let mut sub_start: Option<usize> = None;
626 for (i, &offset) in line_offsets[start_line..end_line]
627 .iter()
628 .enumerate()
629 .map(|(j, o)| (j + start_line, o))
630 {
631 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
632 if is_real_code && sub_start.is_none() {
633 let byte_start = if i == start_line { start } else { offset };
634 sub_start = Some(byte_start);
635 } else if !is_real_code && sub_start.is_some() {
636 new_code_blocks.push((sub_start.unwrap(), offset));
637 sub_start = None;
638 }
639 }
640 if let Some(s) = sub_start {
641 new_code_blocks.push((s, end));
642 }
643 }
644 code_blocks = new_code_blocks;
645
646 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
653 let mut run: Option<(usize, usize)> = None;
654 for line in &lines {
655 if line.in_jsx_block && line.in_code_block {
656 let line_end = line.byte_offset + line.byte_len;
657 match &mut run {
658 Some((_, end)) => *end = line_end,
659 None => run = Some((line.byte_offset, line_end)),
660 }
661 } else if let Some(r) = run.take() {
662 jsx_fence_ranges.push(r);
663 }
664 }
665 if let Some(r) = run.take() {
666 jsx_fence_ranges.push(r);
667 }
668 if !jsx_fence_ranges.is_empty() {
669 code_blocks.extend(jsx_fence_ranges);
670 code_blocks.sort_by_key(|&(start, _)| start);
671 }
672 }
673
674 let colon_fence_details = profile_section!(
677 "Azure colon fence detection",
678 profile,
679 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
680 );
681 if !colon_fence_details.is_empty() {
682 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
683 code_blocks.sort_by_key(|&(start, _)| start);
684 }
685
686 let myst_directive_ranges = profile_section!(
689 "MyST colon directives",
690 profile,
691 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
692 );
693
694 let myst_comment_ranges = profile_section!(
696 "MyST comments",
697 profile,
698 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
699 );
700
701 profile_section!(
704 "MyST backtick directives",
705 profile,
706 flavor_detection::detect_myst_backtick_directives(
707 content,
708 &mut lines,
709 flavor,
710 &code_block_details,
711 &line_offsets
712 )
713 );
714
715 if flavor.supports_myst_directives() {
718 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
719 for &(start, end) in &code_blocks {
720 let start_line = line_offsets
721 .partition_point(|&offset| offset <= start)
722 .saturating_sub(1);
723 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
724
725 let mut sub_start: Option<usize> = None;
726 for (i, &offset) in line_offsets[start_line..end_line]
727 .iter()
728 .enumerate()
729 .map(|(j, o)| (j + start_line, o))
730 {
731 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
732 if is_real_code && sub_start.is_none() {
733 let byte_start = if i == start_line { start } else { offset };
734 sub_start = Some(byte_start);
735 } else if !is_real_code && sub_start.is_some() {
736 new_code_blocks.push((sub_start.unwrap(), offset));
737 sub_start = None;
738 }
739 }
740 if let Some(s) = sub_start {
741 new_code_blocks.push((s, end));
742 }
743 }
744 code_blocks = new_code_blocks;
745 }
746
747 profile_section!(
749 "Kramdown constructs",
750 profile,
751 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
752 );
753
754 for line in &mut lines {
759 if line.in_kramdown_extension_block {
760 line.list_item = None;
761 line.is_horizontal_rule = false;
762 line.blockquote = None;
763 line.is_kramdown_block_ial = false;
764 }
765 }
766
767 let obsidian_comment_scan = profile_section!(
769 "Obsidian comments",
770 profile,
771 flavor_detection::detect_obsidian_comments(
772 content,
773 &mut lines,
774 flavor,
775 &code_span_ranges,
776 &html_comment_ranges,
777 body_start
778 )
779 );
780 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
781 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
782
783 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
788 unterminated_html_comment,
789 &obsidian_comment_ranges,
790 content,
791 &code_span_ranges,
792 &comment_code_block_ranges,
793 body_start,
794 );
795
796 if let Some(range) = unterminated_html_comment.and_then(|opener| {
809 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
810 .or_else(|| container_comment_range(opener, &containers, &lines, content))
811 }) {
812 html_comment_ranges.push(range);
815
816 for line in &mut lines {
822 let text = line.content(content);
823 let content_start = line.byte_offset + line.indent;
824 let content_end = line.byte_offset + text.trim_end().len();
825 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
826 &html_comment_ranges,
827 content_start,
828 content_end,
829 );
830 line.in_obsidian_comment = false;
831 }
832
833 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
844 content,
845 &mut lines,
846 flavor,
847 &code_span_ranges,
848 &html_comment_ranges,
849 body_start,
850 );
851 obsidian_comment_ranges = obsidian_rescan.ranges;
852 unterminated_obsidian_comment = obsidian_rescan.unterminated;
853 }
854
855 let myst_role_ranges = profile_section!(
857 "MyST roles",
858 profile,
859 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
860 );
861
862 let mut pulldown_result = profile_section!(
866 "Links, images & link ranges",
867 profile,
868 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
869 );
870
871 if let Some(mdx) = &mdx_context {
872 let (links, images) = mdx.links_and_images(content, &lines);
873 pulldown_result.link_byte_ranges = links.iter().map(|link| (link.byte_offset, link.byte_end)).collect();
874 pulldown_result.link_found_positions = links.iter().map(|link| link.byte_offset).collect();
875 pulldown_result.image_found_positions = images.iter().map(|image| image.byte_offset).collect();
876 pulldown_result.links = links;
877 pulldown_result.images = images;
878 pulldown_result.footnote_refs = mdx.footnote_refs();
879 pulldown_result
880 .broken_links
881 .retain(|link| mdx.contains_text(link.span.start, link.span.end));
882 }
883
884 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
886 let mut blockquote_headings = profile_section!(
887 "Headings & blockquotes",
888 profile,
889 heading_detection::detect_headings_and_blockquotes(
890 &content_lines,
891 &mut lines,
892 flavor,
893 &html_comment_ranges,
894 &html_blocks,
895 &code_blocks,
896 &code_span_ranges,
897 &pulldown_result.link_byte_ranges,
898 front_matter_end,
899 mdx_flow_lines.as_deref(),
900 )
901 );
902
903 for line in &mut lines {
905 if line.in_kramdown_extension_block {
906 line.heading = None;
907 }
908 }
909 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
910 if line.in_kramdown_extension_block {
911 *heading = None;
912 }
913 }
914
915 for line in &mut lines {
926 if line.is_horizontal_rule
927 && (line.in_code_block
928 || line.in_html_block
929 || line.in_html_comment
930 || line.in_math_block
931 || line.in_mdx_comment
932 || line.in_obsidian_comment)
933 {
934 line.is_horizontal_rule = false;
935 }
936 }
937
938 let mut code_spans = profile_section!(
940 "Code spans",
941 profile,
942 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
943 );
944
945 if flavor == MarkdownFlavor::MkDocs {
949 let extra = profile_section!(
950 "MkDocs code spans",
951 profile,
952 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
953 );
954 if !extra.is_empty() {
955 code_spans.extend(extra);
956 code_spans.sort_by_key(|span| span.byte_offset);
957 }
958 }
959
960 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
965 let extra = profile_section!(
966 "MDX JSX code spans",
967 profile,
968 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
969 );
970 if !extra.is_empty() {
971 code_spans.extend(extra);
972 code_spans.sort_by_key(|span| span.byte_offset);
973 }
974 }
975
976 for span in &code_spans {
979 if span.end_line > span.line {
980 for line_num in (span.line + 1)..=span.end_line {
982 if let Some(line_info) = lines.get_mut(line_num - 1) {
983 line_info.in_code_span_continuation = true;
984 }
985 }
986 }
987 }
988
989 let (links, images, broken_links, footnote_refs) = profile_section!(
991 "Links & images finalize",
992 profile,
993 link_parser::finalize_links_and_images(
994 content,
995 &lines,
996 flavor,
997 &link_parser::LinkExclusions {
998 code_blocks: &code_blocks,
999 code_spans: &code_spans,
1000 html_comment_ranges: &html_comment_ranges,
1001 mdx: mdx_context.as_ref(),
1002 },
1003 pulldown_result,
1004 )
1005 );
1006
1007 let reference_defs = profile_section!("Reference defs", profile, {
1008 if let Some(mdx) = &mdx_context {
1009 mdx.reference_defs(content)
1010 } else {
1011 link_parser::parse_reference_defs(content, &lines)
1012 }
1013 });
1014
1015 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1016
1017 let char_frequency = profile_section!(
1019 "Char frequency",
1020 profile,
1021 line_computation::compute_char_frequency(content)
1022 );
1023
1024 let table_blocks = profile_section!(
1026 "Table blocks",
1027 profile,
1028 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
1029 content,
1030 &code_blocks,
1031 &code_spans,
1032 &html_comment_ranges,
1033 flavor,
1034 )
1035 );
1036
1037 let links = links
1040 .into_iter()
1041 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1042 .collect::<Vec<_>>();
1043 let images = images
1044 .into_iter()
1045 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1046 .collect::<Vec<_>>();
1047 let broken_links = broken_links
1048 .into_iter()
1049 .filter(|bl| {
1050 let line_idx = line_offsets
1052 .partition_point(|&offset| offset <= bl.span.start)
1053 .saturating_sub(1);
1054 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1055 })
1056 .collect::<Vec<_>>();
1057 let footnote_refs = footnote_refs
1058 .into_iter()
1059 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1060 .collect::<Vec<_>>();
1061 let reference_defs = reference_defs
1062 .into_iter()
1063 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1064 .collect::<Vec<_>>();
1065 let list_blocks = list_blocks
1066 .into_iter()
1067 .filter(|block| {
1068 !lines
1069 .get(block.start_line - 1)
1070 .is_some_and(|l| l.in_kramdown_extension_block)
1071 })
1072 .collect::<Vec<_>>();
1073 let table_blocks = table_blocks
1074 .into_iter()
1075 .filter(|block| {
1076 !lines
1078 .get(block.start_line)
1079 .is_some_and(|l| l.in_kramdown_extension_block)
1080 })
1081 .collect::<Vec<_>>();
1082 let emphasis_spans = emphasis_spans
1083 .into_iter()
1084 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1085 .collect::<Vec<_>>();
1086
1087 for block in &list_blocks {
1091 for line_num in block.start_line..=block.end_line {
1093 if let Some(li) = lines.get_mut(line_num - 1) {
1094 li.in_list_block = true;
1095 }
1096 }
1097 }
1098 for block in &table_blocks {
1099 for idx in block.start_line..=block.end_line {
1101 if let Some(li) = lines.get_mut(idx) {
1102 li.in_table_block = true;
1103 }
1104 }
1105 }
1106
1107 let reference_defs_map: HashMap<String, usize> = reference_defs
1109 .iter()
1110 .enumerate()
1111 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1112 .collect();
1113
1114 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1116 .iter()
1117 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1118 (Some(start), Some(end)) => Some((start, end)),
1119 _ => None,
1120 })
1121 .collect();
1122
1123 let line_index = profile_section!(
1125 "Line index",
1126 profile,
1127 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
1128 content,
1129 line_offsets.clone(),
1130 &code_blocks,
1131 )
1132 );
1133
1134 let jinja_ranges = profile_section!(
1136 "Jinja ranges",
1137 profile,
1138 crate::utils::jinja_utils::find_jinja_ranges(content)
1139 );
1140
1141 let citation_ranges = profile_section!("Citation ranges", profile, {
1143 if flavor.is_pandoc_compatible() {
1144 crate::utils::pandoc::find_citation_ranges(content)
1145 } else {
1146 Vec::new()
1147 }
1148 });
1149
1150 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1152 if flavor.is_pandoc_compatible() {
1153 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1154 } else {
1155 Vec::new()
1156 }
1157 });
1158
1159 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1161 if flavor.is_pandoc_compatible() {
1162 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1163 } else {
1164 std::collections::HashSet::new()
1165 }
1166 });
1167
1168 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1170 if flavor.is_pandoc_compatible() {
1171 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1172 } else {
1173 Vec::new()
1174 }
1175 });
1176
1177 let example_reference_ranges = profile_section!("Example references", profile, {
1179 if flavor.is_pandoc_compatible() {
1180 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1181 } else {
1182 Vec::new()
1183 }
1184 });
1185
1186 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1188 if flavor.is_pandoc_compatible() {
1189 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1190 } else {
1191 Vec::new()
1192 }
1193 });
1194
1195 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1197 if flavor.is_pandoc_compatible() {
1198 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1199 } else {
1200 Vec::new()
1201 }
1202 });
1203
1204 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1206 if flavor.is_pandoc_compatible() {
1207 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1208 } else {
1209 Vec::new()
1210 }
1211 });
1212
1213 let line_block_ranges = profile_section!("Line block ranges", profile, {
1215 if flavor.is_pandoc_compatible() {
1216 crate::utils::pandoc::detect_line_block_ranges(content)
1217 } else {
1218 Vec::new()
1219 }
1220 });
1221
1222 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1224 if flavor.is_pandoc_compatible() {
1225 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1226 } else {
1227 Vec::new()
1228 }
1229 });
1230
1231 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1233 if flavor.is_pandoc_compatible() {
1234 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1235 } else {
1236 Vec::new()
1237 }
1238 });
1239
1240 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1242 if flavor.is_pandoc_compatible() {
1243 crate::utils::pandoc::detect_grid_table_ranges(content)
1244 } else {
1245 Vec::new()
1246 }
1247 });
1248
1249 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1251 if flavor.is_pandoc_compatible() {
1252 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1253 } else {
1254 Vec::new()
1255 }
1256 });
1257
1258 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1260 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
1261 let mut ranges = Vec::new();
1262 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
1263 ranges.push((mat.start(), mat.end()));
1264 }
1265 ranges
1266 });
1267
1268 let inline_config =
1269 InlineConfig::from_content_with_code_blocks(content, &code_blocks, &code_span_byte_ranges(&code_spans));
1270 Self {
1271 content,
1272 content_lines,
1273 line_offsets,
1274 code_blocks,
1275 code_block_details,
1276 strong_spans,
1277 line_to_list,
1278 list_start_values,
1279 commonmark_ordered_lists_cache: OnceLock::new(),
1280 lines,
1281 blockquote_headings,
1282 links,
1283 images,
1284 broken_links,
1285 footnote_refs,
1286 reference_defs,
1287 reference_defs_map,
1288 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1289 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1291 math_byte_ranges_cache: OnceLock::new(), list_blocks,
1293 char_frequency,
1294 html_tags_cache: OnceLock::new(),
1295 jsx_component_tags_cache: OnceLock::new(),
1296 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1297 bare_urls_cache: OnceLock::new(),
1298 has_mixed_list_nesting_cache: OnceLock::new(),
1299 html_comment_ranges,
1300 table_blocks,
1301 line_index,
1302 jinja_ranges,
1303 flavor,
1304 source_file,
1305 link_target_policy: None,
1306 jsx_expression_ranges,
1307 mdx_comment_ranges,
1308 citation_ranges,
1309 pandoc_div_ranges,
1310 colon_fence_details,
1311 inline_footnote_ranges,
1312 pandoc_header_slugs,
1313 example_list_marker_ranges,
1314 example_reference_ranges,
1315 sub_super_ranges,
1316 inline_code_attr_ranges,
1317 bracketed_span_ranges,
1318 line_block_ranges,
1319 pipe_table_caption_ranges,
1320 pandoc_metadata_ranges,
1321 grid_table_ranges,
1322 multi_line_table_ranges,
1323 shortcode_ranges,
1324 link_title_ranges,
1325 code_span_byte_ranges: code_span_ranges,
1326 inline_config,
1327 obsidian_comment_ranges,
1328 unterminated_html_comment,
1329 unterminated_obsidian_comment,
1330 lazy_cont_lines_cache: OnceLock::new(),
1331 myst_directive_ranges,
1332 myst_comment_ranges,
1333 myst_role_ranges,
1334 front_matter_end,
1335 }
1336 }
1337
1338 pub fn front_matter_end_line(&self) -> usize {
1343 self.front_matter_end
1344 }
1345
1346 #[inline]
1349 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1350 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1352 idx > 0 && pos < ranges[idx - 1].1
1354 }
1355
1356 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1358 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1359 }
1360
1361 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
1366 let line = &self.lines[line_number - 1];
1367 heading_detection::ends_with_hard_break(
1368 line.content(self.content),
1369 line.byte_offset,
1370 &self.code_span_byte_ranges,
1371 )
1372 }
1373
1374 pub fn is_in_link(&self, pos: usize) -> bool {
1376 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
1377 }
1378
1379 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1381 let bare_urls = self.bare_urls();
1382 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1384 idx > 0 && pos < bare_urls[idx - 1].byte_end
1385 }
1386
1387 pub fn inline_config(&self) -> &InlineConfig {
1389 &self.inline_config
1390 }
1391
1392 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1397 &self.colon_fence_details
1398 }
1399
1400 pub fn raw_lines(&self) -> &[&'a str] {
1404 &self.content_lines
1405 }
1406
1407 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1412 self.inline_config.is_rule_disabled(rule_name, line_number)
1413 }
1414
1415 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1417 Arc::clone(
1418 self.code_spans_cache
1419 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1420 )
1421 }
1422
1423 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1427 self.math_byte_ranges_cache
1428 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1429 }
1430
1431 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1433 Arc::clone(
1434 self.math_spans_cache
1435 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1436 )
1437 }
1438
1439 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
1441 self.bracket_math_cache
1442 .get_or_init(|| bracket_math::parse(self.content, &self.lines, &self.code_spans(), &self.list_blocks))
1443 }
1444
1445 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1447 let math_spans = self.math_spans();
1448 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1450 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1451 }
1452
1453 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1455 &self.html_comment_ranges
1456 }
1457
1458 pub fn unterminated_html_comment(&self) -> Option<usize> {
1463 self.unterminated_html_comment
1464 }
1465
1466 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1470 self.unterminated_obsidian_comment
1471 }
1472
1473 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1477 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1478 }
1479
1480 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1485 if self.obsidian_comment_ranges.is_empty() {
1486 return false;
1487 }
1488
1489 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1491 self.is_in_obsidian_comment(byte_pos)
1492 }
1493
1494 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1496 &self.myst_directive_ranges
1497 }
1498
1499 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1501 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1502 }
1503
1504 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1506 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1507 }
1508
1509 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1516 if !self.flavor.supports_myst_directives() {
1517 return false;
1518 }
1519 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1520 info.in_myst_directive
1521 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1522 })
1523 }
1524
1525 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1527 tags.into_iter()
1528 .filter(|tag| {
1529 !self
1530 .lines
1531 .get(tag.line - 1)
1532 .is_some_and(|l| l.in_kramdown_extension_block)
1533 })
1534 .collect()
1535 }
1536
1537 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1543 Arc::clone(self.html_tags_cache.get_or_init(|| {
1544 let (html_tags, jsx_component_tags) =
1545 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1546 let _ = self
1548 .jsx_component_tags_cache
1549 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1550 Arc::new(self.filter_kramdown_tags(html_tags))
1551 }))
1552 }
1553
1554 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1557 if let Some(cached) = self.jsx_component_tags_cache.get() {
1558 return Arc::clone(cached);
1559 }
1560 let _ = self.html_tags();
1562 Arc::clone(
1563 self.jsx_component_tags_cache
1564 .get()
1565 .expect("html_tags() populates jsx_component_tags_cache"),
1566 )
1567 }
1568
1569 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1571 Arc::clone(
1572 self.emphasis_spans_cache
1573 .get()
1574 .expect("emphasis_spans_cache initialized during construction"),
1575 )
1576 }
1577
1578 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1580 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1581 Arc::new(element_parsers::parse_bare_urls(
1582 self.content,
1583 &self.lines,
1584 &self.code_blocks,
1585 ))
1586 }))
1587 }
1588
1589 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1591 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1592 Arc::new(element_parsers::detect_lazy_continuation_lines(
1593 self.content,
1594 &self.lines,
1595 &self.line_offsets,
1596 ))
1597 }))
1598 }
1599
1600 pub fn has_mixed_list_nesting(&self) -> bool {
1604 *self
1605 .has_mixed_list_nesting_cache
1606 .get_or_init(|| self.compute_mixed_list_nesting())
1607 }
1608
1609 fn compute_mixed_list_nesting(&self) -> bool {
1611 let mut stack: Vec<(usize, bool)> = Vec::new();
1616 let mut last_was_blank = false;
1617
1618 for line_info in &self.lines {
1619 if line_info.in_code_block
1621 || line_info.in_front_matter
1622 || line_info.in_mkdocstrings
1623 || line_info.in_html_comment
1624 || line_info.in_mdx_comment
1625 || line_info.in_esm_block
1626 {
1627 continue;
1628 }
1629
1630 if line_info.is_blank {
1632 last_was_blank = true;
1633 continue;
1634 }
1635
1636 if let Some(list_item) = &line_info.list_item {
1637 let current_pos = if list_item.marker_column == 1 {
1639 0
1640 } else {
1641 list_item.marker_column
1642 };
1643
1644 if last_was_blank && current_pos == 0 {
1646 stack.clear();
1647 }
1648 last_was_blank = false;
1649
1650 while let Some(&(pos, _)) = stack.last() {
1652 if pos >= current_pos {
1653 stack.pop();
1654 } else {
1655 break;
1656 }
1657 }
1658
1659 if let Some(&(_, parent_is_ordered)) = stack.last()
1661 && parent_is_ordered != list_item.is_ordered
1662 {
1663 return true; }
1665
1666 stack.push((current_pos, list_item.is_ordered));
1667 } else {
1668 last_was_blank = false;
1670 }
1671 }
1672
1673 false
1674 }
1675
1676 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1682 match self.line_offsets.binary_search(&offset) {
1683 Ok(line) => (line + 1, 1),
1684 Err(line) => {
1685 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1686 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1688 (line, col)
1689 }
1690 }
1691 }
1692
1693 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
1699 self.line_index.get_line_start_byte(line_number)
1700 }
1701
1702 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
1708 self.line_index.line_col_to_byte_range(line_number, column)
1709 }
1710
1711 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
1716 self.line_index
1717 .line_col_to_byte_range_with_length(line_number, column, length)
1718 }
1719
1720 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
1723 self.line_index.whole_line_range(line_number)
1724 }
1725
1726 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
1731 self.line_index.line_text_range(line_number, start_column, end_column)
1732 }
1733
1734 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
1737 self.line_index.line_content_range(line_number)
1738 }
1739
1740 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
1742 self.line_index.multi_line_range(start_line, end_line)
1743 }
1744
1745 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1747 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1749 return true;
1750 }
1751
1752 self.is_byte_offset_in_code_span(pos)
1754 }
1755
1756 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1758 if line_num > 0 {
1759 self.lines.get(line_num - 1)
1760 } else {
1761 None
1762 }
1763 }
1764
1765 pub fn links(&self) -> &[ParsedLink<'a>] {
1767 &self.links
1768 }
1769
1770 pub fn images(&self) -> &[ParsedImage<'a>] {
1772 &self.images
1773 }
1774
1775 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
1777 &self.broken_links
1778 }
1779
1780 pub fn footnote_references(&self) -> &[FootnoteRef] {
1782 &self.footnote_refs
1783 }
1784
1785 pub fn reference_definitions(&self) -> &[ReferenceDef] {
1787 &self.reference_defs
1788 }
1789
1790 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
1792 let start = self.links.partition_point(|link| link.line < line_number);
1793 let end = self.links.partition_point(|link| link.line <= line_number);
1794 &self.links[start..end]
1795 }
1796
1797 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
1799 let start = self.images.partition_point(|image| image.line < line_number);
1800 let end = self.images.partition_point(|image| image.line <= line_number);
1801 &self.images[start..end]
1802 }
1803
1804 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1806 self.links
1807 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
1808 .ok()
1809 .map(|index| &self.links[index])
1810 }
1811
1812 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1814 self.images
1815 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
1816 .ok()
1817 .map(|index| &self.images[index])
1818 }
1819
1820 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1822 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1823 self.links
1824 .get(index.checked_sub(1)?)
1825 .filter(|link| byte_offset < link.byte_end)
1826 }
1827
1828 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1830 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
1831 self.images
1832 .get(index.checked_sub(1)?)
1833 .filter(|image| byte_offset < image.byte_end)
1834 }
1835
1836 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
1838 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1839 &self.links[..end]
1840 }
1841
1842 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
1844 let normalized_id = ref_id.to_lowercase();
1845 self.reference_defs_map
1846 .get(&normalized_id)
1847 .map(|&index| &self.reference_defs[index])
1848 }
1849
1850 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1852 self.reference_definition(ref_id)
1853 .map(|definition| definition.url.as_str())
1854 }
1855
1856 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1858 if line_num == 0 || line_num > self.lines.len() {
1859 return false;
1860 }
1861 self.lines[line_num - 1].in_list_block
1862 }
1863
1864 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1866 if line_num == 0 || line_num > self.lines.len() {
1867 return false;
1868 }
1869 self.lines[line_num - 1].in_html_block
1870 }
1871
1872 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1878 if line_num == 0 || line_num > self.lines.len() {
1879 return false;
1880 }
1881 self.lines[line_num - 1].in_table_block
1882 }
1883
1884 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1886 if line_num == 0 || line_num > self.lines.len() {
1887 return false;
1888 }
1889
1890 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1894 let code_spans = self.code_spans();
1895 code_spans.iter().any(|span| {
1896 if line_num < span.line || line_num > span.end_line {
1898 return false;
1899 }
1900
1901 if span.line == span.end_line {
1902 col_0indexed >= span.start_col && col_0indexed < span.end_col
1904 } else if line_num == span.line {
1905 col_0indexed >= span.start_col
1907 } else if line_num == span.end_line {
1908 col_0indexed < span.end_col
1910 } else {
1911 true
1913 }
1914 })
1915 }
1916
1917 #[inline]
1919 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1920 let code_spans = self.code_spans();
1921 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1922 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1923 }
1924
1925 #[inline]
1927 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1928 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1929 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1930 }
1931
1932 #[inline]
1934 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1935 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1936 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1937 }
1938
1939 #[inline]
1942 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1943 let tags = self.html_tags();
1944 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1945 idx > 0 && byte_pos < tags[idx - 1].byte_end
1946 }
1947
1948 #[inline]
1952 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1953 if !self.flavor.supports_jsx() {
1954 return false;
1955 }
1956 let tags = self.jsx_component_tags();
1957 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1958 idx > 0 && byte_pos < tags[idx - 1].byte_end
1959 }
1960
1961 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1963 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1964 }
1965
1966 #[inline]
1968 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1969 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1970 }
1971
1972 #[inline]
1974 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1975 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1976 }
1977
1978 #[inline]
1981 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1982 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1983 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1984 }
1985
1986 #[inline]
1988 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1989 &self.citation_ranges
1990 }
1991
1992 #[inline]
1995 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1996 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1997 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1998 }
1999
2000 #[inline]
2003 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
2004 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
2005 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
2006 }
2007
2008 #[inline]
2011 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2012 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2013 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2014 }
2015
2016 #[inline]
2019 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2020 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2021 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2022 }
2023
2024 #[inline]
2027 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2028 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
2029 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
2030 }
2031
2032 #[inline]
2036 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
2037 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
2038 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
2039 }
2040
2041 #[inline]
2044 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
2045 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
2046 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
2047 }
2048
2049 #[inline]
2052 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
2053 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
2054 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
2055 }
2056
2057 #[inline]
2061 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
2062 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
2063 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
2064 }
2065
2066 #[inline]
2069 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
2070 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
2071 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
2072 }
2073
2074 #[inline]
2077 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
2078 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
2079 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
2080 }
2081
2082 #[inline]
2085 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
2086 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
2087 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
2088 }
2089
2090 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
2095 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
2096 self.pandoc_header_slugs.contains(&slug)
2097 }
2098
2099 #[inline]
2105 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
2106 self.pandoc_header_slugs.contains(slug)
2107 }
2108
2109 #[inline]
2111 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
2112 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
2113 }
2114
2115 #[inline]
2117 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
2118 &self.shortcode_ranges
2119 }
2120
2121 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
2123 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
2124 }
2125
2126 pub fn has_char(&self, ch: char) -> bool {
2128 match ch {
2129 '#' => self.char_frequency.hash_count > 0,
2130 '*' => self.char_frequency.asterisk_count > 0,
2131 '_' => self.char_frequency.underscore_count > 0,
2132 '-' => self.char_frequency.hyphen_count > 0,
2133 '+' => self.char_frequency.plus_count > 0,
2134 '>' => self.char_frequency.gt_count > 0,
2135 '|' => self.char_frequency.pipe_count > 0,
2136 '[' => self.char_frequency.bracket_count > 0,
2137 '`' => self.char_frequency.backtick_count > 0,
2138 '<' => self.char_frequency.lt_count > 0,
2139 '!' => self.char_frequency.exclamation_count > 0,
2140 '\n' => self.char_frequency.newline_count > 0,
2141 _ => self.content.contains(ch), }
2143 }
2144
2145 pub fn char_count(&self, ch: char) -> usize {
2147 match ch {
2148 '#' => self.char_frequency.hash_count,
2149 '*' => self.char_frequency.asterisk_count,
2150 '_' => self.char_frequency.underscore_count,
2151 '-' => self.char_frequency.hyphen_count,
2152 '+' => self.char_frequency.plus_count,
2153 '>' => self.char_frequency.gt_count,
2154 '|' => self.char_frequency.pipe_count,
2155 '[' => self.char_frequency.bracket_count,
2156 '`' => self.char_frequency.backtick_count,
2157 '<' => self.char_frequency.lt_count,
2158 '!' => self.char_frequency.exclamation_count,
2159 '\n' => self.char_frequency.newline_count,
2160 _ => self.content.matches(ch).count(), }
2162 }
2163
2164 pub fn likely_has_headings(&self) -> bool {
2167 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.content.contains('=')
2168 }
2169
2170 pub fn likely_has_lists(&self) -> bool {
2174 self.char_frequency.asterisk_count > 0
2175 || self.char_frequency.hyphen_count > 0
2176 || self.char_frequency.plus_count > 0
2177 }
2178
2179 pub fn likely_has_emphasis(&self) -> bool {
2181 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
2182 }
2183
2184 pub fn likely_has_tables(&self) -> bool {
2186 self.char_frequency.pipe_count > 2
2187 }
2188
2189 pub fn likely_has_blockquotes(&self) -> bool {
2191 self.char_frequency.gt_count > 0
2192 }
2193
2194 pub fn likely_has_code(&self) -> bool {
2196 self.char_frequency.backtick_count > 0
2197 }
2198
2199 pub fn likely_has_links_or_images(&self) -> bool {
2201 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
2202 }
2203
2204 pub fn likely_has_html(&self) -> bool {
2206 self.char_frequency.lt_count > 0
2207 }
2208
2209 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
2214 if let Some(line_info) = self.lines.get(line_idx)
2215 && let Some(ref bq) = line_info.blockquote
2216 {
2217 bq.prefix.trim_end().to_string()
2218 } else {
2219 String::new()
2220 }
2221 }
2222
2223 #[inline]
2234 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
2235 let idx = match lines.binary_search_by(|line| {
2237 if byte_offset < line.byte_offset {
2238 std::cmp::Ordering::Greater
2239 } else if byte_offset > line.byte_offset + line.byte_len {
2240 std::cmp::Ordering::Less
2241 } else {
2242 std::cmp::Ordering::Equal
2243 }
2244 }) {
2245 Ok(idx) => idx,
2246 Err(idx) => idx.saturating_sub(1),
2247 };
2248
2249 let line = &lines[idx];
2250 let line_num = idx + 1;
2251 let byte_col = byte_offset.saturating_sub(line.byte_offset);
2252 let col = byte_to_char_count(line.content(content), byte_col) - 1;
2255
2256 (idx, line_num, col)
2257 }
2258
2259 #[inline]
2261 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
2262 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
2264
2265 if idx > 0 {
2267 let span = &code_spans[idx - 1];
2268 if offset >= span.byte_offset && offset < span.byte_end {
2269 return true;
2270 }
2271 }
2272
2273 false
2274 }
2275
2276 #[must_use]
2296 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
2297 ValidHeadingsIter::new(&self.lines)
2298 }
2299
2300 #[must_use]
2304 pub fn has_valid_headings(&self) -> bool {
2305 self.lines
2306 .iter()
2307 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
2308 }
2309
2310 #[must_use]
2312 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
2313 ParsedListItemsIter::new(&self.lines)
2314 }
2315
2316 #[must_use]
2318 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
2319 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
2320 Some(ParsedListItem::new(
2321 line_num,
2322 line_info.list_item.as_deref()?,
2323 line_info,
2324 ))
2325 }
2326
2327 #[must_use]
2329 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
2330 ParsedListBlocks::new(&self.list_blocks, &self.lines)
2331 }
2332
2333 #[must_use]
2337 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
2338 list_blocks::item_lines_by_list(self.content, &self.lines, block)
2339 }
2340
2341 #[must_use]
2343 pub fn has_list_items(&self) -> bool {
2344 self.lines.iter().any(|line| line.list_item.is_some())
2345 }
2346
2347 #[must_use]
2349 pub fn has_unordered_list_items(&self) -> bool {
2350 self.lines
2351 .iter()
2352 .any(|line| line.list_item.as_ref().is_some_and(|item| !item.is_ordered))
2353 }
2354
2355 #[must_use]
2357 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
2358 let lists = self
2359 .commonmark_ordered_lists_cache
2360 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
2361 CommonMarkOrderedLists::new(lists, &self.lines)
2362 }
2363
2364 #[must_use]
2372 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
2373 ParsedHeadingsIter::new(&self.lines, &self.blockquote_headings)
2374 }
2375
2376 #[must_use]
2378 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
2379 let idx = line_num.checked_sub(1)?;
2380 let line_info = self.lines.get(idx)?;
2381 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
2382 Some(heading) => (heading, 0),
2383 None => (
2384 self.blockquote_headings.get(idx)?.as_deref()?,
2385 line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level),
2386 ),
2387 };
2388 Some(ParsedHeading {
2389 line_num,
2390 heading,
2391 line_info,
2392 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
2393 blockquote_depth,
2394 })
2395 }
2396}
2397
2398fn container_comment_range(
2410 opener: usize,
2411 containers: &flavor_detection::ContainerLines,
2412 lines: &[types::LineInfo],
2413 content: &str,
2414) -> Option<crate::utils::skip_context::ByteRange> {
2415 let line_index = lines
2416 .partition_point(|line| line.byte_offset <= opener)
2417 .checked_sub(1)?;
2418 let line = lines.get(line_index)?;
2419 if line.byte_offset + line.indent != opener {
2420 return None;
2421 }
2422 if !containers.is_container_body(line_index) {
2423 return None;
2424 }
2425 let end_line = lines.get(containers.body_end_line(line_index)?)?;
2426 Some(crate::utils::skip_context::ByteRange {
2427 start: opener,
2428 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
2429 })
2430}
2431
2432fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
2441 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
2442
2443 let options = crate::utils::rumdl_parser_options();
2444 let parser = Parser::new_ext(content, options).into_offset_iter();
2445
2446 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
2448 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
2449 let mut in_footnote = false;
2450
2451 for (event, range) in parser {
2452 match event {
2453 Event::Start(Tag::FootnoteDefinition(_)) => {
2454 in_footnote = true;
2455 footnote_ranges.push((range.start, range.end));
2456 }
2457 Event::End(TagEnd::FootnoteDefinition) => {
2458 in_footnote = false;
2459 }
2460 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
2461 fenced_code_ranges.push((range.start, range.end));
2462 }
2463 _ => {}
2464 }
2465 }
2466
2467 let byte_to_line = |byte_offset: usize| -> usize {
2468 line_offsets
2469 .partition_point(|&offset| offset <= byte_offset)
2470 .saturating_sub(1)
2471 };
2472
2473 for &(start, end) in &footnote_ranges {
2475 let start_line = byte_to_line(start);
2476 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2477
2478 for line in &mut lines[start_line..end_line] {
2479 line.in_footnote_definition = true;
2480 line.in_code_block = false;
2481 }
2482 }
2483
2484 for &(start, end) in &fenced_code_ranges {
2486 let start_line = byte_to_line(start);
2487 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2488
2489 for line in &mut lines[start_line..end_line] {
2490 line.in_code_block = true;
2491 }
2492 }
2493}