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 element_parsers;
7mod flavor_detection;
8mod heading_detection;
9mod line_computation;
10mod link_parser;
11mod list_blocks;
12mod mdx;
13#[cfg(test)]
14mod tests;
15
16use crate::config::MarkdownFlavor;
17use crate::inline_config::InlineConfig;
18use crate::rules::front_matter_utils::FrontMatterUtils;
19use crate::utils::code_block_utils::{CodeBlockDetail, CodeBlockUtils};
20use crate::utils::range_utils::byte_to_char_count;
21use std::collections::HashMap;
22use std::ops::Range;
23use std::path::{Path, PathBuf};
24
25#[derive(Debug, Clone)]
28pub struct LinkTargetPolicy {
29 supplied_paths: Arc<std::collections::HashSet<PathBuf>>,
30 allow_disk_fallback: bool,
31}
32
33impl LinkTargetPolicy {
34 pub fn open_world<I, P>(paths: I) -> Self
37 where
38 I: IntoIterator<Item = P>,
39 P: AsRef<Path>,
40 {
41 Self::from_paths(paths, true)
42 }
43
44 pub fn closed_world<I, P>(paths: I) -> Self
46 where
47 I: IntoIterator<Item = P>,
48 P: AsRef<Path>,
49 {
50 Self::from_paths(paths, false)
51 }
52
53 fn from_paths<I, P>(paths: I, allow_disk_fallback: bool) -> Self
54 where
55 I: IntoIterator<Item = P>,
56 P: AsRef<Path>,
57 {
58 let mut roots = Vec::new();
59 if let Ok(cwd) = std::env::current_dir() {
60 if let Ok(canonical_cwd) = cwd.canonicalize()
61 && canonical_cwd != cwd
62 {
63 roots.push(canonical_cwd);
64 }
65 roots.push(cwd);
66 }
67 Self::from_paths_with_roots(paths, allow_disk_fallback, roots)
68 }
69
70 fn from_paths_with_roots<I, P, R, Q>(paths: I, allow_disk_fallback: bool, roots: R) -> Self
71 where
72 I: IntoIterator<Item = P>,
73 P: AsRef<Path>,
74 R: IntoIterator<Item = Q>,
75 Q: AsRef<Path>,
76 {
77 let roots: Vec<PathBuf> = roots
78 .into_iter()
79 .map(|root| crate::workspace_index::normalize_relative_path(root.as_ref()))
80 .collect();
81 let mut supplied_paths = std::collections::HashSet::new();
82 for path in paths {
83 let path = path.as_ref();
84 supplied_paths.insert(crate::workspace_index::normalize_relative_path(path));
85
86 if path.is_relative() {
87 for root in &roots {
88 supplied_paths.insert(crate::workspace_index::normalize_relative_path(&root.join(path)));
89 }
90 } else {
91 for source_root in &roots {
92 if let Ok(relative) = path.strip_prefix(source_root) {
93 for root in &roots {
94 supplied_paths
95 .insert(crate::workspace_index::normalize_relative_path(&root.join(relative)));
96 }
97 }
98 }
99 }
100 }
101 Self {
102 supplied_paths: Arc::new(supplied_paths),
103 allow_disk_fallback,
104 }
105 }
106
107 pub fn contains(&self, path: &Path) -> bool {
108 self.supplied_paths
109 .contains(&crate::workspace_index::normalize_relative_path(path))
110 }
111
112 pub(crate) fn resolve_supplied(&self, path: &Path) -> Option<PathBuf> {
113 let normalized = crate::workspace_index::normalize_relative_path(path);
114 if self.supplied_paths.contains(&normalized) {
115 return Some(normalized);
116 }
117
118 if path.extension().is_none() {
119 for extension in ["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"] {
120 let candidate = crate::workspace_index::normalize_relative_path(&path.with_extension(extension));
121 if self.supplied_paths.contains(&candidate) {
122 return Some(candidate);
123 }
124 }
125 }
126
127 None
128 }
129
130 pub fn contains_with_markdown_extension(&self, path: &Path) -> bool {
131 self.resolve_supplied(path).is_some()
132 }
133
134 pub fn allow_disk_fallback(&self) -> bool {
135 self.allow_disk_fallback
136 }
137}
138
139#[cfg(not(target_arch = "wasm32"))]
141macro_rules! profile_section {
142 ($name:expr, $profile:expr, $code:expr) => {{
143 let start = std::time::Instant::now();
144 let result = $code;
145 if $profile {
146 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
147 }
148 result
149 }};
150}
151
152fn build_commonmark_ordered_lists(
153 lines: &[LineInfo],
154 line_to_list: &crate::utils::code_block_utils::LineToListMap,
155 list_start_values: &crate::utils::code_block_utils::ListStartValues,
156) -> Vec<CommonMarkOrderedListInfo> {
157 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
158
159 for (&line_num, &list_id) in line_to_list {
160 let is_ordered_item = line_num
161 .checked_sub(1)
162 .and_then(|index| lines.get(index))
163 .and_then(|line| line.list_item.as_deref())
164 .is_some_and(|item| item.is_ordered);
165 if is_ordered_item {
166 grouped_lines.entry(list_id).or_default().push(line_num);
167 }
168 }
169
170 let mut lists: Vec<_> = grouped_lines
171 .into_iter()
172 .map(|(list_id, mut item_lines)| {
173 item_lines.sort_unstable();
174 CommonMarkOrderedListInfo {
175 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
176 item_lines,
177 }
178 })
179 .collect();
180 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
181 lists
182}
183
184#[cfg(target_arch = "wasm32")]
185macro_rules! profile_section {
186 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
187}
188
189pub(super) struct SkipByteRanges<'a> {
192 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
193 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
194 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
195 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
196}
197
198use std::sync::{Arc, OnceLock};
199
200pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
202
203pub(super) type ByteRanges = Vec<(usize, usize)>;
205
206pub struct LintContext<'a> {
207 pub content: &'a str,
208 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
210 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>>>, 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, }
271
272pub struct CodeRanges {
274 pub blocks: Vec<(usize, usize)>,
276 pub spans: Vec<(usize, usize)>,
278}
279
280pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
291 let ctx = LintContext::new(content, flavor, None);
292 CodeRanges {
293 spans: code_span_byte_ranges(&ctx.code_spans()),
294 blocks: ctx.code_blocks,
295 }
296}
297
298pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
300 code_spans
301 .iter()
302 .map(|span| (span.byte_offset, span.byte_end))
303 .collect()
304}
305
306impl<'a> LintContext<'a> {
307 pub fn source_file(&self) -> Option<&Path> {
312 self.source_file.as_deref()
313 }
314
315 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
316 self.link_target_policy.as_ref()
317 }
318
319 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
320 self.link_target_policy = Some(policy);
321 self
322 }
323
324 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
325 #[cfg(not(target_arch = "wasm32"))]
326 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
327
328 let line_offsets = profile_section!("Line offsets", profile, {
329 let mut offsets = vec![0];
330 for (i, c) in content.char_indices() {
331 if c == '\n' {
332 offsets.push(i + 1);
333 }
334 }
335 offsets
336 });
337
338 let content_lines: Vec<&str> = content.lines().collect();
340
341 #[allow(clippy::disallowed_methods)]
345 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
346
347 let parse_result = profile_section!(
349 "Code blocks",
350 profile,
351 CodeBlockUtils::detect_code_blocks_and_spans(content)
352 );
353 let mut code_blocks = parse_result.code_blocks;
354 let mut code_span_ranges = parse_result.code_spans;
355 let code_block_details = parse_result.code_block_details;
356 let strong_spans = parse_result.strong_spans;
357 let line_to_list = parse_result.line_to_list;
358 let list_start_values = parse_result.list_start_values;
359 let html_blocks = parse_result.html_blocks;
360
361 let containers = profile_section!(
364 "Container lines",
365 profile,
366 flavor_detection::detect_container_lines(&content_lines, flavor)
367 );
368
369 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
378 .iter()
379 .flat_map(|detail| {
380 if detail.is_fenced {
381 return vec![(detail.start, detail.end)];
382 }
383 let start_line = line_offsets
384 .partition_point(|&offset| offset <= detail.start)
385 .saturating_sub(1);
386 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
387 containers
388 .code_line_spans_in(start_line..end_line)
389 .into_iter()
390 .map(|span| {
391 let start = line_offsets[span.start].max(detail.start);
392 let end = line_offsets
393 .get(span.end)
394 .copied()
395 .unwrap_or(content.len())
396 .min(detail.end);
397 (start, end)
398 })
399 .collect()
400 })
401 .collect();
402 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
408 let html_comment_scan = profile_section!(
409 "HTML comment ranges",
410 profile,
411 crate::utils::skip_context::scan_html_comments(
412 content,
413 &code_span_ranges,
414 &comment_code_block_ranges,
415 body_start
416 )
417 );
418 let mut html_comment_ranges = html_comment_scan.ranges;
419 let unterminated_html_comment = html_comment_scan.unterminated;
420
421 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
425 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
426 Vec::new()
427 } else {
428 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
429 }
430 });
431
432 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
434 if flavor.is_pandoc_compatible() {
435 crate::utils::pandoc::detect_div_block_ranges(content)
436 } else {
437 Vec::new()
438 }
439 });
440
441 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
443 if flavor == MarkdownFlavor::MkDocs {
444 crate::utils::pymdown_blocks::detect_block_ranges(content)
445 } else {
446 Vec::new()
447 }
448 });
449
450 let skip_ranges = SkipByteRanges {
453 html_comment_ranges: &html_comment_ranges,
454 autodoc_ranges: &autodoc_ranges,
455 pandoc_div_ranges: &pandoc_div_ranges,
456 pymdown_block_ranges: &pymdown_block_ranges,
457 };
458 let (mut lines, emphasis_spans) = profile_section!(
459 "Basic line info",
460 profile,
461 line_computation::compute_basic_line_info(
462 content,
463 &content_lines,
464 &line_offsets,
465 &code_blocks,
466 flavor,
467 &skip_ranges,
468 front_matter_end,
469 )
470 );
471
472 profile_section!(
474 "HTML blocks",
475 profile,
476 heading_detection::detect_html_blocks(content, &mut lines)
477 );
478
479 profile_section!(
481 "ESM blocks",
482 profile,
483 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
484 );
485
486 profile_section!(
488 "JSX block detection",
489 profile,
490 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
491 );
492
493 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
495 "JSX/MDX detection",
496 profile,
497 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
498 );
499
500 profile_section!(
505 "Markdown-in-HTML blocks",
506 profile,
507 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
508 );
509
510 let mdx_context = if flavor == MarkdownFlavor::MDX {
511 mdx::MdxContext::parse(content, &lines)
512 } else {
513 None
514 };
515 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
516 mdx.apply_lines(&mut lines);
517 code_blocks.clone_from(&mdx.code_blocks);
518 code_span_ranges.clone_from(&mdx.code_spans);
519 (mdx.expressions.clone(), mdx.comments.clone())
520 } else {
521 (jsx_expression_ranges, mdx_comment_ranges)
522 };
523
524 profile_section!(
526 "MkDocs constructs",
527 profile,
528 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
529 );
530
531 profile_section!(
536 "Footnote definitions",
537 profile,
538 detect_footnote_definitions(content, &mut lines, &line_offsets)
539 );
540
541 {
544 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
545 for &(start, end) in &code_blocks {
546 let start_line = line_offsets
547 .partition_point(|&offset| offset <= start)
548 .saturating_sub(1);
549 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
550
551 let mut sub_start: Option<usize> = None;
552 for (i, &offset) in line_offsets[start_line..end_line]
553 .iter()
554 .enumerate()
555 .map(|(j, o)| (j + start_line, o))
556 {
557 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
558 if is_real_code && sub_start.is_none() {
559 let byte_start = if i == start_line { start } else { offset };
560 sub_start = Some(byte_start);
561 } else if !is_real_code && sub_start.is_some() {
562 new_code_blocks.push((sub_start.unwrap(), offset));
563 sub_start = None;
564 }
565 }
566 if let Some(s) = sub_start {
567 new_code_blocks.push((s, end));
568 }
569 }
570 code_blocks = new_code_blocks;
571 }
572
573 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
581 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
582 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
583 for &(start, end) in &code_blocks {
584 let start_line = line_offsets
585 .partition_point(|&offset| offset <= start)
586 .saturating_sub(1);
587 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
588
589 let mut sub_start: Option<usize> = None;
591 for (i, &offset) in line_offsets[start_line..end_line]
592 .iter()
593 .enumerate()
594 .map(|(j, o)| (j + start_line, o))
595 {
596 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
597 if is_real_code && sub_start.is_none() {
598 let byte_start = if i == start_line { start } else { offset };
599 sub_start = Some(byte_start);
600 } else if !is_real_code && sub_start.is_some() {
601 new_code_blocks.push((sub_start.unwrap(), offset));
602 sub_start = None;
603 }
604 }
605 if let Some(s) = sub_start {
606 new_code_blocks.push((s, end));
607 }
608 }
609 code_blocks = new_code_blocks;
610 }
611
612 if flavor.supports_jsx() {
616 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
617 for &(start, end) in &code_blocks {
618 let start_line = line_offsets
619 .partition_point(|&offset| offset <= start)
620 .saturating_sub(1);
621 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
622
623 let mut sub_start: Option<usize> = None;
624 for (i, &offset) in line_offsets[start_line..end_line]
625 .iter()
626 .enumerate()
627 .map(|(j, o)| (j + start_line, o))
628 {
629 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
630 if is_real_code && sub_start.is_none() {
631 let byte_start = if i == start_line { start } else { offset };
632 sub_start = Some(byte_start);
633 } else if !is_real_code && sub_start.is_some() {
634 new_code_blocks.push((sub_start.unwrap(), offset));
635 sub_start = None;
636 }
637 }
638 if let Some(s) = sub_start {
639 new_code_blocks.push((s, end));
640 }
641 }
642 code_blocks = new_code_blocks;
643
644 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
651 let mut run: Option<(usize, usize)> = None;
652 for line in &lines {
653 if line.in_jsx_block && line.in_code_block {
654 let line_end = line.byte_offset + line.byte_len;
655 match &mut run {
656 Some((_, end)) => *end = line_end,
657 None => run = Some((line.byte_offset, line_end)),
658 }
659 } else if let Some(r) = run.take() {
660 jsx_fence_ranges.push(r);
661 }
662 }
663 if let Some(r) = run.take() {
664 jsx_fence_ranges.push(r);
665 }
666 if !jsx_fence_ranges.is_empty() {
667 code_blocks.extend(jsx_fence_ranges);
668 code_blocks.sort_by_key(|&(start, _)| start);
669 }
670 }
671
672 let colon_fence_details = profile_section!(
675 "Azure colon fence detection",
676 profile,
677 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
678 );
679 if !colon_fence_details.is_empty() {
680 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
681 code_blocks.sort_by_key(|&(start, _)| start);
682 }
683
684 let myst_directive_ranges = profile_section!(
687 "MyST colon directives",
688 profile,
689 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
690 );
691
692 let myst_comment_ranges = profile_section!(
694 "MyST comments",
695 profile,
696 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
697 );
698
699 profile_section!(
702 "MyST backtick directives",
703 profile,
704 flavor_detection::detect_myst_backtick_directives(
705 content,
706 &mut lines,
707 flavor,
708 &code_block_details,
709 &line_offsets
710 )
711 );
712
713 if flavor.supports_myst_directives() {
716 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
717 for &(start, end) in &code_blocks {
718 let start_line = line_offsets
719 .partition_point(|&offset| offset <= start)
720 .saturating_sub(1);
721 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
722
723 let mut sub_start: Option<usize> = None;
724 for (i, &offset) in line_offsets[start_line..end_line]
725 .iter()
726 .enumerate()
727 .map(|(j, o)| (j + start_line, o))
728 {
729 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
730 if is_real_code && sub_start.is_none() {
731 let byte_start = if i == start_line { start } else { offset };
732 sub_start = Some(byte_start);
733 } else if !is_real_code && sub_start.is_some() {
734 new_code_blocks.push((sub_start.unwrap(), offset));
735 sub_start = None;
736 }
737 }
738 if let Some(s) = sub_start {
739 new_code_blocks.push((s, end));
740 }
741 }
742 code_blocks = new_code_blocks;
743 }
744
745 profile_section!(
747 "Kramdown constructs",
748 profile,
749 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
750 );
751
752 for line in &mut lines {
757 if line.in_kramdown_extension_block {
758 line.list_item = None;
759 line.is_horizontal_rule = false;
760 line.blockquote = None;
761 line.is_kramdown_block_ial = false;
762 }
763 }
764
765 let obsidian_comment_scan = profile_section!(
767 "Obsidian comments",
768 profile,
769 flavor_detection::detect_obsidian_comments(
770 content,
771 &mut lines,
772 flavor,
773 &code_span_ranges,
774 &html_comment_ranges,
775 body_start
776 )
777 );
778 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
779 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
780
781 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
786 unterminated_html_comment,
787 &obsidian_comment_ranges,
788 content,
789 &code_span_ranges,
790 &comment_code_block_ranges,
791 body_start,
792 );
793
794 if let Some(range) = unterminated_html_comment.and_then(|opener| {
807 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
808 .or_else(|| container_comment_range(opener, &containers, &lines, content))
809 }) {
810 html_comment_ranges.push(range);
813
814 for line in &mut lines {
820 let text = line.content(content);
821 let content_start = line.byte_offset + line.indent;
822 let content_end = line.byte_offset + text.trim_end().len();
823 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
824 &html_comment_ranges,
825 content_start,
826 content_end,
827 );
828 line.in_obsidian_comment = false;
829 }
830
831 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
842 content,
843 &mut lines,
844 flavor,
845 &code_span_ranges,
846 &html_comment_ranges,
847 body_start,
848 );
849 obsidian_comment_ranges = obsidian_rescan.ranges;
850 unterminated_obsidian_comment = obsidian_rescan.unterminated;
851 }
852
853 let myst_role_ranges = profile_section!(
855 "MyST roles",
856 profile,
857 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
858 );
859
860 let mut pulldown_result = profile_section!(
864 "Links, images & link ranges",
865 profile,
866 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
867 );
868
869 if let Some(mdx) = &mdx_context {
870 let (links, images) = mdx.links_and_images(content, &lines);
871 pulldown_result.link_byte_ranges = links.iter().map(|link| (link.byte_offset, link.byte_end)).collect();
872 pulldown_result.link_found_positions = links.iter().map(|link| link.byte_offset).collect();
873 pulldown_result.image_found_positions = images.iter().map(|image| image.byte_offset).collect();
874 pulldown_result.links = links;
875 pulldown_result.images = images;
876 pulldown_result.footnote_refs = mdx.footnote_refs();
877 pulldown_result
878 .broken_links
879 .retain(|link| mdx.contains_text(link.span.start, link.span.end));
880 }
881
882 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
884 let mut blockquote_headings = profile_section!(
885 "Headings & blockquotes",
886 profile,
887 heading_detection::detect_headings_and_blockquotes(
888 &content_lines,
889 &mut lines,
890 flavor,
891 &html_comment_ranges,
892 &html_blocks,
893 &code_blocks,
894 &code_span_ranges,
895 &pulldown_result.link_byte_ranges,
896 front_matter_end,
897 mdx_flow_lines.as_deref(),
898 )
899 );
900
901 for line in &mut lines {
903 if line.in_kramdown_extension_block {
904 line.heading = None;
905 }
906 }
907 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
908 if line.in_kramdown_extension_block {
909 *heading = None;
910 }
911 }
912
913 for line in &mut lines {
924 if line.is_horizontal_rule
925 && (line.in_code_block
926 || line.in_html_block
927 || line.in_html_comment
928 || line.in_math_block
929 || line.in_mdx_comment
930 || line.in_obsidian_comment)
931 {
932 line.is_horizontal_rule = false;
933 }
934 }
935
936 let mut code_spans = profile_section!(
938 "Code spans",
939 profile,
940 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
941 );
942
943 if flavor == MarkdownFlavor::MkDocs {
947 let extra = profile_section!(
948 "MkDocs code spans",
949 profile,
950 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
951 );
952 if !extra.is_empty() {
953 code_spans.extend(extra);
954 code_spans.sort_by_key(|span| span.byte_offset);
955 }
956 }
957
958 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
963 let extra = profile_section!(
964 "MDX JSX code spans",
965 profile,
966 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
967 );
968 if !extra.is_empty() {
969 code_spans.extend(extra);
970 code_spans.sort_by_key(|span| span.byte_offset);
971 }
972 }
973
974 for span in &code_spans {
977 if span.end_line > span.line {
978 for line_num in (span.line + 1)..=span.end_line {
980 if let Some(line_info) = lines.get_mut(line_num - 1) {
981 line_info.in_code_span_continuation = true;
982 }
983 }
984 }
985 }
986
987 let (links, images, broken_links, footnote_refs) = profile_section!(
989 "Links & images finalize",
990 profile,
991 link_parser::finalize_links_and_images(
992 content,
993 &lines,
994 flavor,
995 &link_parser::LinkExclusions {
996 code_blocks: &code_blocks,
997 code_spans: &code_spans,
998 html_comment_ranges: &html_comment_ranges,
999 mdx: mdx_context.as_ref(),
1000 },
1001 pulldown_result,
1002 )
1003 );
1004
1005 let reference_defs = profile_section!("Reference defs", profile, {
1006 if let Some(mdx) = &mdx_context {
1007 mdx.reference_defs(content)
1008 } else {
1009 link_parser::parse_reference_defs(content, &lines)
1010 }
1011 });
1012
1013 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1014
1015 let char_frequency = profile_section!(
1017 "Char frequency",
1018 profile,
1019 line_computation::compute_char_frequency(content)
1020 );
1021
1022 let table_blocks = profile_section!(
1024 "Table blocks",
1025 profile,
1026 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
1027 content,
1028 &code_blocks,
1029 &code_spans,
1030 &html_comment_ranges,
1031 flavor,
1032 )
1033 );
1034
1035 let links = links
1038 .into_iter()
1039 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1040 .collect::<Vec<_>>();
1041 let images = images
1042 .into_iter()
1043 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1044 .collect::<Vec<_>>();
1045 let broken_links = broken_links
1046 .into_iter()
1047 .filter(|bl| {
1048 let line_idx = line_offsets
1050 .partition_point(|&offset| offset <= bl.span.start)
1051 .saturating_sub(1);
1052 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1053 })
1054 .collect::<Vec<_>>();
1055 let footnote_refs = footnote_refs
1056 .into_iter()
1057 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1058 .collect::<Vec<_>>();
1059 let reference_defs = reference_defs
1060 .into_iter()
1061 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1062 .collect::<Vec<_>>();
1063 let list_blocks = list_blocks
1064 .into_iter()
1065 .filter(|block| {
1066 !lines
1067 .get(block.start_line - 1)
1068 .is_some_and(|l| l.in_kramdown_extension_block)
1069 })
1070 .collect::<Vec<_>>();
1071 let table_blocks = table_blocks
1072 .into_iter()
1073 .filter(|block| {
1074 !lines
1076 .get(block.start_line)
1077 .is_some_and(|l| l.in_kramdown_extension_block)
1078 })
1079 .collect::<Vec<_>>();
1080 let emphasis_spans = emphasis_spans
1081 .into_iter()
1082 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1083 .collect::<Vec<_>>();
1084
1085 for block in &list_blocks {
1089 for line_num in block.start_line..=block.end_line {
1091 if let Some(li) = lines.get_mut(line_num - 1) {
1092 li.in_list_block = true;
1093 }
1094 }
1095 }
1096 for block in &table_blocks {
1097 for idx in block.start_line..=block.end_line {
1099 if let Some(li) = lines.get_mut(idx) {
1100 li.in_table_block = true;
1101 }
1102 }
1103 }
1104
1105 let reference_defs_map: HashMap<String, usize> = reference_defs
1107 .iter()
1108 .enumerate()
1109 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1110 .collect();
1111
1112 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1114 .iter()
1115 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1116 (Some(start), Some(end)) => Some((start, end)),
1117 _ => None,
1118 })
1119 .collect();
1120
1121 let line_index = profile_section!(
1123 "Line index",
1124 profile,
1125 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
1126 content,
1127 line_offsets.clone(),
1128 &code_blocks,
1129 )
1130 );
1131
1132 let jinja_ranges = profile_section!(
1134 "Jinja ranges",
1135 profile,
1136 crate::utils::jinja_utils::find_jinja_ranges(content)
1137 );
1138
1139 let citation_ranges = profile_section!("Citation ranges", profile, {
1141 if flavor.is_pandoc_compatible() {
1142 crate::utils::pandoc::find_citation_ranges(content)
1143 } else {
1144 Vec::new()
1145 }
1146 });
1147
1148 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1150 if flavor.is_pandoc_compatible() {
1151 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1152 } else {
1153 Vec::new()
1154 }
1155 });
1156
1157 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1159 if flavor.is_pandoc_compatible() {
1160 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1161 } else {
1162 std::collections::HashSet::new()
1163 }
1164 });
1165
1166 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1168 if flavor.is_pandoc_compatible() {
1169 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1170 } else {
1171 Vec::new()
1172 }
1173 });
1174
1175 let example_reference_ranges = profile_section!("Example references", profile, {
1177 if flavor.is_pandoc_compatible() {
1178 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1179 } else {
1180 Vec::new()
1181 }
1182 });
1183
1184 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1186 if flavor.is_pandoc_compatible() {
1187 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1188 } else {
1189 Vec::new()
1190 }
1191 });
1192
1193 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1195 if flavor.is_pandoc_compatible() {
1196 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1197 } else {
1198 Vec::new()
1199 }
1200 });
1201
1202 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1204 if flavor.is_pandoc_compatible() {
1205 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1206 } else {
1207 Vec::new()
1208 }
1209 });
1210
1211 let line_block_ranges = profile_section!("Line block ranges", profile, {
1213 if flavor.is_pandoc_compatible() {
1214 crate::utils::pandoc::detect_line_block_ranges(content)
1215 } else {
1216 Vec::new()
1217 }
1218 });
1219
1220 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1222 if flavor.is_pandoc_compatible() {
1223 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1224 } else {
1225 Vec::new()
1226 }
1227 });
1228
1229 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1231 if flavor.is_pandoc_compatible() {
1232 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1233 } else {
1234 Vec::new()
1235 }
1236 });
1237
1238 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1240 if flavor.is_pandoc_compatible() {
1241 crate::utils::pandoc::detect_grid_table_ranges(content)
1242 } else {
1243 Vec::new()
1244 }
1245 });
1246
1247 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1249 if flavor.is_pandoc_compatible() {
1250 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1251 } else {
1252 Vec::new()
1253 }
1254 });
1255
1256 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1258 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
1259 let mut ranges = Vec::new();
1260 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
1261 ranges.push((mat.start(), mat.end()));
1262 }
1263 ranges
1264 });
1265
1266 let inline_config =
1267 InlineConfig::from_content_with_code_blocks(content, &code_blocks, &code_span_byte_ranges(&code_spans));
1268 Self {
1269 content,
1270 content_lines,
1271 line_offsets,
1272 code_blocks,
1273 code_block_details,
1274 strong_spans,
1275 line_to_list,
1276 list_start_values,
1277 commonmark_ordered_lists_cache: OnceLock::new(),
1278 lines,
1279 blockquote_headings,
1280 links,
1281 images,
1282 broken_links,
1283 footnote_refs,
1284 reference_defs,
1285 reference_defs_map,
1286 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1287 math_spans_cache: OnceLock::new(), math_byte_ranges_cache: OnceLock::new(), list_blocks,
1290 char_frequency,
1291 html_tags_cache: OnceLock::new(),
1292 jsx_component_tags_cache: OnceLock::new(),
1293 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1294 bare_urls_cache: OnceLock::new(),
1295 has_mixed_list_nesting_cache: OnceLock::new(),
1296 html_comment_ranges,
1297 table_blocks,
1298 line_index,
1299 jinja_ranges,
1300 flavor,
1301 source_file,
1302 link_target_policy: None,
1303 jsx_expression_ranges,
1304 mdx_comment_ranges,
1305 citation_ranges,
1306 pandoc_div_ranges,
1307 colon_fence_details,
1308 inline_footnote_ranges,
1309 pandoc_header_slugs,
1310 example_list_marker_ranges,
1311 example_reference_ranges,
1312 sub_super_ranges,
1313 inline_code_attr_ranges,
1314 bracketed_span_ranges,
1315 line_block_ranges,
1316 pipe_table_caption_ranges,
1317 pandoc_metadata_ranges,
1318 grid_table_ranges,
1319 multi_line_table_ranges,
1320 shortcode_ranges,
1321 link_title_ranges,
1322 code_span_byte_ranges: code_span_ranges,
1323 inline_config,
1324 obsidian_comment_ranges,
1325 unterminated_html_comment,
1326 unterminated_obsidian_comment,
1327 lazy_cont_lines_cache: OnceLock::new(),
1328 myst_directive_ranges,
1329 myst_comment_ranges,
1330 myst_role_ranges,
1331 front_matter_end,
1332 }
1333 }
1334
1335 pub fn front_matter_end_line(&self) -> usize {
1340 self.front_matter_end
1341 }
1342
1343 #[inline]
1346 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1347 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1349 idx > 0 && pos < ranges[idx - 1].1
1351 }
1352
1353 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1355 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1356 }
1357
1358 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
1363 let line = &self.lines[line_number - 1];
1364 heading_detection::ends_with_hard_break(
1365 line.content(self.content),
1366 line.byte_offset,
1367 &self.code_span_byte_ranges,
1368 )
1369 }
1370
1371 pub fn is_in_link(&self, pos: usize) -> bool {
1373 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
1374 }
1375
1376 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1378 let bare_urls = self.bare_urls();
1379 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1381 idx > 0 && pos < bare_urls[idx - 1].byte_end
1382 }
1383
1384 pub fn inline_config(&self) -> &InlineConfig {
1386 &self.inline_config
1387 }
1388
1389 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1394 &self.colon_fence_details
1395 }
1396
1397 pub fn raw_lines(&self) -> &[&'a str] {
1401 &self.content_lines
1402 }
1403
1404 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1409 self.inline_config.is_rule_disabled(rule_name, line_number)
1410 }
1411
1412 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1414 Arc::clone(
1415 self.code_spans_cache
1416 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1417 )
1418 }
1419
1420 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1424 self.math_byte_ranges_cache
1425 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1426 }
1427
1428 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1430 Arc::clone(
1431 self.math_spans_cache
1432 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1433 )
1434 }
1435
1436 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1438 let math_spans = self.math_spans();
1439 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1441 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1442 }
1443
1444 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1446 &self.html_comment_ranges
1447 }
1448
1449 pub fn unterminated_html_comment(&self) -> Option<usize> {
1454 self.unterminated_html_comment
1455 }
1456
1457 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1461 self.unterminated_obsidian_comment
1462 }
1463
1464 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1468 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1469 }
1470
1471 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1476 if self.obsidian_comment_ranges.is_empty() {
1477 return false;
1478 }
1479
1480 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1482 self.is_in_obsidian_comment(byte_pos)
1483 }
1484
1485 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1487 &self.myst_directive_ranges
1488 }
1489
1490 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1492 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1493 }
1494
1495 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1497 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1498 }
1499
1500 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1507 if !self.flavor.supports_myst_directives() {
1508 return false;
1509 }
1510 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1511 info.in_myst_directive
1512 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1513 })
1514 }
1515
1516 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1518 tags.into_iter()
1519 .filter(|tag| {
1520 !self
1521 .lines
1522 .get(tag.line - 1)
1523 .is_some_and(|l| l.in_kramdown_extension_block)
1524 })
1525 .collect()
1526 }
1527
1528 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1534 Arc::clone(self.html_tags_cache.get_or_init(|| {
1535 let (html_tags, jsx_component_tags) =
1536 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1537 let _ = self
1539 .jsx_component_tags_cache
1540 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1541 Arc::new(self.filter_kramdown_tags(html_tags))
1542 }))
1543 }
1544
1545 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1548 if let Some(cached) = self.jsx_component_tags_cache.get() {
1549 return Arc::clone(cached);
1550 }
1551 let _ = self.html_tags();
1553 Arc::clone(
1554 self.jsx_component_tags_cache
1555 .get()
1556 .expect("html_tags() populates jsx_component_tags_cache"),
1557 )
1558 }
1559
1560 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1562 Arc::clone(
1563 self.emphasis_spans_cache
1564 .get()
1565 .expect("emphasis_spans_cache initialized during construction"),
1566 )
1567 }
1568
1569 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1571 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1572 Arc::new(element_parsers::parse_bare_urls(
1573 self.content,
1574 &self.lines,
1575 &self.code_blocks,
1576 ))
1577 }))
1578 }
1579
1580 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1582 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1583 Arc::new(element_parsers::detect_lazy_continuation_lines(
1584 self.content,
1585 &self.lines,
1586 &self.line_offsets,
1587 ))
1588 }))
1589 }
1590
1591 pub fn has_mixed_list_nesting(&self) -> bool {
1595 *self
1596 .has_mixed_list_nesting_cache
1597 .get_or_init(|| self.compute_mixed_list_nesting())
1598 }
1599
1600 fn compute_mixed_list_nesting(&self) -> bool {
1602 let mut stack: Vec<(usize, bool)> = Vec::new();
1607 let mut last_was_blank = false;
1608
1609 for line_info in &self.lines {
1610 if line_info.in_code_block
1612 || line_info.in_front_matter
1613 || line_info.in_mkdocstrings
1614 || line_info.in_html_comment
1615 || line_info.in_mdx_comment
1616 || line_info.in_esm_block
1617 {
1618 continue;
1619 }
1620
1621 if line_info.is_blank {
1623 last_was_blank = true;
1624 continue;
1625 }
1626
1627 if let Some(list_item) = &line_info.list_item {
1628 let current_pos = if list_item.marker_column == 1 {
1630 0
1631 } else {
1632 list_item.marker_column
1633 };
1634
1635 if last_was_blank && current_pos == 0 {
1637 stack.clear();
1638 }
1639 last_was_blank = false;
1640
1641 while let Some(&(pos, _)) = stack.last() {
1643 if pos >= current_pos {
1644 stack.pop();
1645 } else {
1646 break;
1647 }
1648 }
1649
1650 if let Some(&(_, parent_is_ordered)) = stack.last()
1652 && parent_is_ordered != list_item.is_ordered
1653 {
1654 return true; }
1656
1657 stack.push((current_pos, list_item.is_ordered));
1658 } else {
1659 last_was_blank = false;
1661 }
1662 }
1663
1664 false
1665 }
1666
1667 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1673 match self.line_offsets.binary_search(&offset) {
1674 Ok(line) => (line + 1, 1),
1675 Err(line) => {
1676 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1677 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1679 (line, col)
1680 }
1681 }
1682 }
1683
1684 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
1690 self.line_index.get_line_start_byte(line_number)
1691 }
1692
1693 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
1699 self.line_index.line_col_to_byte_range(line_number, column)
1700 }
1701
1702 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
1707 self.line_index
1708 .line_col_to_byte_range_with_length(line_number, column, length)
1709 }
1710
1711 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
1714 self.line_index.whole_line_range(line_number)
1715 }
1716
1717 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
1722 self.line_index.line_text_range(line_number, start_column, end_column)
1723 }
1724
1725 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
1728 self.line_index.line_content_range(line_number)
1729 }
1730
1731 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
1733 self.line_index.multi_line_range(start_line, end_line)
1734 }
1735
1736 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1738 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1740 return true;
1741 }
1742
1743 self.is_byte_offset_in_code_span(pos)
1745 }
1746
1747 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1749 if line_num > 0 {
1750 self.lines.get(line_num - 1)
1751 } else {
1752 None
1753 }
1754 }
1755
1756 pub fn links(&self) -> &[ParsedLink<'a>] {
1758 &self.links
1759 }
1760
1761 pub fn images(&self) -> &[ParsedImage<'a>] {
1763 &self.images
1764 }
1765
1766 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
1768 &self.broken_links
1769 }
1770
1771 pub fn footnote_references(&self) -> &[FootnoteRef] {
1773 &self.footnote_refs
1774 }
1775
1776 pub fn reference_definitions(&self) -> &[ReferenceDef] {
1778 &self.reference_defs
1779 }
1780
1781 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
1783 let start = self.links.partition_point(|link| link.line < line_number);
1784 let end = self.links.partition_point(|link| link.line <= line_number);
1785 &self.links[start..end]
1786 }
1787
1788 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
1790 let start = self.images.partition_point(|image| image.line < line_number);
1791 let end = self.images.partition_point(|image| image.line <= line_number);
1792 &self.images[start..end]
1793 }
1794
1795 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1797 self.links
1798 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
1799 .ok()
1800 .map(|index| &self.links[index])
1801 }
1802
1803 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1805 self.images
1806 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
1807 .ok()
1808 .map(|index| &self.images[index])
1809 }
1810
1811 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1813 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1814 self.links
1815 .get(index.checked_sub(1)?)
1816 .filter(|link| byte_offset < link.byte_end)
1817 }
1818
1819 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1821 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
1822 self.images
1823 .get(index.checked_sub(1)?)
1824 .filter(|image| byte_offset < image.byte_end)
1825 }
1826
1827 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
1829 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1830 &self.links[..end]
1831 }
1832
1833 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
1835 let normalized_id = ref_id.to_lowercase();
1836 self.reference_defs_map
1837 .get(&normalized_id)
1838 .map(|&index| &self.reference_defs[index])
1839 }
1840
1841 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1843 self.reference_definition(ref_id)
1844 .map(|definition| definition.url.as_str())
1845 }
1846
1847 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1849 if line_num == 0 || line_num > self.lines.len() {
1850 return false;
1851 }
1852 self.lines[line_num - 1].in_list_block
1853 }
1854
1855 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1857 if line_num == 0 || line_num > self.lines.len() {
1858 return false;
1859 }
1860 self.lines[line_num - 1].in_html_block
1861 }
1862
1863 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1869 if line_num == 0 || line_num > self.lines.len() {
1870 return false;
1871 }
1872 self.lines[line_num - 1].in_table_block
1873 }
1874
1875 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1877 if line_num == 0 || line_num > self.lines.len() {
1878 return false;
1879 }
1880
1881 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1885 let code_spans = self.code_spans();
1886 code_spans.iter().any(|span| {
1887 if line_num < span.line || line_num > span.end_line {
1889 return false;
1890 }
1891
1892 if span.line == span.end_line {
1893 col_0indexed >= span.start_col && col_0indexed < span.end_col
1895 } else if line_num == span.line {
1896 col_0indexed >= span.start_col
1898 } else if line_num == span.end_line {
1899 col_0indexed < span.end_col
1901 } else {
1902 true
1904 }
1905 })
1906 }
1907
1908 #[inline]
1910 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1911 let code_spans = self.code_spans();
1912 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1913 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1914 }
1915
1916 #[inline]
1918 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1919 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1920 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1921 }
1922
1923 #[inline]
1925 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1926 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1927 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1928 }
1929
1930 #[inline]
1933 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1934 let tags = self.html_tags();
1935 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1936 idx > 0 && byte_pos < tags[idx - 1].byte_end
1937 }
1938
1939 #[inline]
1943 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1944 if !self.flavor.supports_jsx() {
1945 return false;
1946 }
1947 let tags = self.jsx_component_tags();
1948 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1949 idx > 0 && byte_pos < tags[idx - 1].byte_end
1950 }
1951
1952 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1954 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1955 }
1956
1957 #[inline]
1959 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1960 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1961 }
1962
1963 #[inline]
1965 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1966 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1967 }
1968
1969 #[inline]
1972 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1973 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1974 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1975 }
1976
1977 #[inline]
1979 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1980 &self.citation_ranges
1981 }
1982
1983 #[inline]
1986 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1987 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1988 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1989 }
1990
1991 #[inline]
1994 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1995 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1996 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1997 }
1998
1999 #[inline]
2002 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2003 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2004 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2005 }
2006
2007 #[inline]
2010 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2011 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2012 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2013 }
2014
2015 #[inline]
2018 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2019 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
2020 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
2021 }
2022
2023 #[inline]
2027 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
2028 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
2029 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
2030 }
2031
2032 #[inline]
2035 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
2036 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
2037 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
2038 }
2039
2040 #[inline]
2043 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
2044 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
2045 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
2046 }
2047
2048 #[inline]
2052 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
2053 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
2054 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
2055 }
2056
2057 #[inline]
2060 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
2061 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
2062 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
2063 }
2064
2065 #[inline]
2068 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
2069 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
2070 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
2071 }
2072
2073 #[inline]
2076 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
2077 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
2078 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
2079 }
2080
2081 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
2086 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
2087 self.pandoc_header_slugs.contains(&slug)
2088 }
2089
2090 #[inline]
2096 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
2097 self.pandoc_header_slugs.contains(slug)
2098 }
2099
2100 #[inline]
2102 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
2103 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
2104 }
2105
2106 #[inline]
2108 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
2109 &self.shortcode_ranges
2110 }
2111
2112 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
2114 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
2115 }
2116
2117 pub fn has_char(&self, ch: char) -> bool {
2119 match ch {
2120 '#' => self.char_frequency.hash_count > 0,
2121 '*' => self.char_frequency.asterisk_count > 0,
2122 '_' => self.char_frequency.underscore_count > 0,
2123 '-' => self.char_frequency.hyphen_count > 0,
2124 '+' => self.char_frequency.plus_count > 0,
2125 '>' => self.char_frequency.gt_count > 0,
2126 '|' => self.char_frequency.pipe_count > 0,
2127 '[' => self.char_frequency.bracket_count > 0,
2128 '`' => self.char_frequency.backtick_count > 0,
2129 '<' => self.char_frequency.lt_count > 0,
2130 '!' => self.char_frequency.exclamation_count > 0,
2131 '\n' => self.char_frequency.newline_count > 0,
2132 _ => self.content.contains(ch), }
2134 }
2135
2136 pub fn char_count(&self, ch: char) -> usize {
2138 match ch {
2139 '#' => self.char_frequency.hash_count,
2140 '*' => self.char_frequency.asterisk_count,
2141 '_' => self.char_frequency.underscore_count,
2142 '-' => self.char_frequency.hyphen_count,
2143 '+' => self.char_frequency.plus_count,
2144 '>' => self.char_frequency.gt_count,
2145 '|' => self.char_frequency.pipe_count,
2146 '[' => self.char_frequency.bracket_count,
2147 '`' => self.char_frequency.backtick_count,
2148 '<' => self.char_frequency.lt_count,
2149 '!' => self.char_frequency.exclamation_count,
2150 '\n' => self.char_frequency.newline_count,
2151 _ => self.content.matches(ch).count(), }
2153 }
2154
2155 pub fn likely_has_headings(&self) -> bool {
2158 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.content.contains('=')
2159 }
2160
2161 pub fn likely_has_lists(&self) -> bool {
2165 self.char_frequency.asterisk_count > 0
2166 || self.char_frequency.hyphen_count > 0
2167 || self.char_frequency.plus_count > 0
2168 }
2169
2170 pub fn likely_has_emphasis(&self) -> bool {
2172 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
2173 }
2174
2175 pub fn likely_has_tables(&self) -> bool {
2177 self.char_frequency.pipe_count > 2
2178 }
2179
2180 pub fn likely_has_blockquotes(&self) -> bool {
2182 self.char_frequency.gt_count > 0
2183 }
2184
2185 pub fn likely_has_code(&self) -> bool {
2187 self.char_frequency.backtick_count > 0
2188 }
2189
2190 pub fn likely_has_links_or_images(&self) -> bool {
2192 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
2193 }
2194
2195 pub fn likely_has_html(&self) -> bool {
2197 self.char_frequency.lt_count > 0
2198 }
2199
2200 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
2205 if let Some(line_info) = self.lines.get(line_idx)
2206 && let Some(ref bq) = line_info.blockquote
2207 {
2208 bq.prefix.trim_end().to_string()
2209 } else {
2210 String::new()
2211 }
2212 }
2213
2214 #[inline]
2225 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
2226 let idx = match lines.binary_search_by(|line| {
2228 if byte_offset < line.byte_offset {
2229 std::cmp::Ordering::Greater
2230 } else if byte_offset > line.byte_offset + line.byte_len {
2231 std::cmp::Ordering::Less
2232 } else {
2233 std::cmp::Ordering::Equal
2234 }
2235 }) {
2236 Ok(idx) => idx,
2237 Err(idx) => idx.saturating_sub(1),
2238 };
2239
2240 let line = &lines[idx];
2241 let line_num = idx + 1;
2242 let byte_col = byte_offset.saturating_sub(line.byte_offset);
2243 let col = byte_to_char_count(line.content(content), byte_col) - 1;
2246
2247 (idx, line_num, col)
2248 }
2249
2250 #[inline]
2252 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
2253 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
2255
2256 if idx > 0 {
2258 let span = &code_spans[idx - 1];
2259 if offset >= span.byte_offset && offset < span.byte_end {
2260 return true;
2261 }
2262 }
2263
2264 false
2265 }
2266
2267 #[must_use]
2287 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
2288 ValidHeadingsIter::new(&self.lines)
2289 }
2290
2291 #[must_use]
2295 pub fn has_valid_headings(&self) -> bool {
2296 self.lines
2297 .iter()
2298 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
2299 }
2300
2301 #[must_use]
2303 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
2304 ParsedListItemsIter::new(&self.lines)
2305 }
2306
2307 #[must_use]
2309 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
2310 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
2311 Some(ParsedListItem::new(
2312 line_num,
2313 line_info.list_item.as_deref()?,
2314 line_info,
2315 ))
2316 }
2317
2318 #[must_use]
2320 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
2321 ParsedListBlocks::new(&self.list_blocks, &self.lines)
2322 }
2323
2324 #[must_use]
2328 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
2329 list_blocks::item_lines_by_list(self.content, &self.lines, block)
2330 }
2331
2332 #[must_use]
2334 pub fn has_list_items(&self) -> bool {
2335 self.lines.iter().any(|line| line.list_item.is_some())
2336 }
2337
2338 #[must_use]
2340 pub fn has_unordered_list_items(&self) -> bool {
2341 self.lines
2342 .iter()
2343 .any(|line| line.list_item.as_ref().is_some_and(|item| !item.is_ordered))
2344 }
2345
2346 #[must_use]
2348 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
2349 let lists = self
2350 .commonmark_ordered_lists_cache
2351 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
2352 CommonMarkOrderedLists::new(lists, &self.lines)
2353 }
2354
2355 #[must_use]
2363 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
2364 ParsedHeadingsIter::new(&self.lines, &self.blockquote_headings)
2365 }
2366
2367 #[must_use]
2369 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
2370 let idx = line_num.checked_sub(1)?;
2371 let line_info = self.lines.get(idx)?;
2372 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
2373 Some(heading) => (heading, 0),
2374 None => (
2375 self.blockquote_headings.get(idx)?.as_deref()?,
2376 line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level),
2377 ),
2378 };
2379 Some(ParsedHeading {
2380 line_num,
2381 heading,
2382 line_info,
2383 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
2384 blockquote_depth,
2385 })
2386 }
2387}
2388
2389fn container_comment_range(
2401 opener: usize,
2402 containers: &flavor_detection::ContainerLines,
2403 lines: &[types::LineInfo],
2404 content: &str,
2405) -> Option<crate::utils::skip_context::ByteRange> {
2406 let line_index = lines
2407 .partition_point(|line| line.byte_offset <= opener)
2408 .checked_sub(1)?;
2409 let line = lines.get(line_index)?;
2410 if line.byte_offset + line.indent != opener {
2411 return None;
2412 }
2413 if !containers.is_container_body(line_index) {
2414 return None;
2415 }
2416 let end_line = lines.get(containers.body_end_line(line_index)?)?;
2417 Some(crate::utils::skip_context::ByteRange {
2418 start: opener,
2419 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
2420 })
2421}
2422
2423fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
2432 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
2433
2434 let options = crate::utils::rumdl_parser_options();
2435 let parser = Parser::new_ext(content, options).into_offset_iter();
2436
2437 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
2439 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
2440 let mut in_footnote = false;
2441
2442 for (event, range) in parser {
2443 match event {
2444 Event::Start(Tag::FootnoteDefinition(_)) => {
2445 in_footnote = true;
2446 footnote_ranges.push((range.start, range.end));
2447 }
2448 Event::End(TagEnd::FootnoteDefinition) => {
2449 in_footnote = false;
2450 }
2451 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
2452 fenced_code_ranges.push((range.start, range.end));
2453 }
2454 _ => {}
2455 }
2456 }
2457
2458 let byte_to_line = |byte_offset: usize| -> usize {
2459 line_offsets
2460 .partition_point(|&offset| offset <= byte_offset)
2461 .saturating_sub(1)
2462 };
2463
2464 for &(start, end) in &footnote_ranges {
2466 let start_line = byte_to_line(start);
2467 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2468
2469 for line in &mut lines[start_line..end_line] {
2470 line.in_footnote_definition = true;
2471 line.in_code_block = false;
2472 }
2473 }
2474
2475 for &(start, end) in &fenced_code_ranges {
2477 let start_line = byte_to_line(start);
2478 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2479
2480 for line in &mut lines[start_line..end_line] {
2481 line.in_code_block = true;
2482 }
2483 }
2484}