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, definition_lists: DefinitionListLines, 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>,
229 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>, invalid_utf8: Option<&'a [crate::encoding::InvalidSeq]>, 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, }
275
276pub struct CodeRanges {
278 pub blocks: Vec<(usize, usize)>,
280 pub spans: Vec<(usize, usize)>,
282}
283
284pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
295 let ctx = LintContext::new(content, flavor, None);
296 CodeRanges {
297 spans: code_span_byte_ranges(&ctx.code_spans()),
298 blocks: ctx.code_blocks,
299 }
300}
301
302pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
304 code_spans
305 .iter()
306 .map(|span| (span.byte_offset, span.byte_end))
307 .collect()
308}
309
310impl<'a> LintContext<'a> {
311 pub fn source_file(&self) -> Option<&Path> {
316 self.source_file.as_deref()
317 }
318
319 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
320 self.link_target_policy.as_ref()
321 }
322
323 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
324 self.link_target_policy = Some(policy);
325 self
326 }
327
328 pub fn invalid_utf8(&self) -> Option<&'a [crate::encoding::InvalidSeq]> {
330 self.invalid_utf8
331 }
332
333 pub fn with_invalid_utf8(mut self, invalid: &'a [crate::encoding::InvalidSeq]) -> Self {
334 self.invalid_utf8 = Some(invalid);
335 self
336 }
337
338 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
339 #[cfg(not(target_arch = "wasm32"))]
340 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
341
342 let line_offsets = profile_section!("Line offsets", profile, {
343 let mut offsets = vec![0];
344 for (i, c) in content.char_indices() {
345 if c == '\n' {
346 offsets.push(i + 1);
347 }
348 }
349 offsets
350 });
351
352 let content_lines: Vec<&str> = content.lines().collect();
354
355 #[allow(clippy::disallowed_methods)]
359 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
360
361 let parse_result = profile_section!(
363 "Code blocks",
364 profile,
365 CodeBlockUtils::detect_code_blocks_and_spans(content)
366 );
367 let mut code_blocks = parse_result.code_blocks;
368 let mut code_span_ranges = parse_result.code_spans;
369 let code_block_details = parse_result.code_block_details;
370 let strong_spans = parse_result.strong_spans;
371 let line_to_list = parse_result.line_to_list;
372 let list_start_values = parse_result.list_start_values;
373 let html_blocks = parse_result.html_blocks;
374 let definition_lists = DefinitionListLines::new(
375 &line_offsets,
376 &parse_result.definition_items,
377 &parse_result.definition_terms,
378 &parse_result.definition_texts,
379 );
380
381 let containers = profile_section!(
384 "Container lines",
385 profile,
386 flavor_detection::detect_container_lines(&content_lines, flavor)
387 );
388
389 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
398 .iter()
399 .flat_map(|detail| {
400 if detail.is_fenced {
401 return vec![(detail.start, detail.end)];
402 }
403 let start_line = line_offsets
404 .partition_point(|&offset| offset <= detail.start)
405 .saturating_sub(1);
406 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
407 containers
408 .code_line_spans_in(start_line..end_line)
409 .into_iter()
410 .map(|span| {
411 let start = line_offsets[span.start].max(detail.start);
412 let end = line_offsets
413 .get(span.end)
414 .copied()
415 .unwrap_or(content.len())
416 .min(detail.end);
417 (start, end)
418 })
419 .collect()
420 })
421 .collect();
422 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
428 let html_comment_scan = profile_section!(
429 "HTML comment ranges",
430 profile,
431 crate::utils::skip_context::scan_html_comments(
432 content,
433 &code_span_ranges,
434 &comment_code_block_ranges,
435 body_start
436 )
437 );
438 let mut html_comment_ranges = html_comment_scan.ranges;
439 let unterminated_html_comment = html_comment_scan.unterminated;
440
441 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
445 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
446 Vec::new()
447 } else {
448 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content, flavor)
449 }
450 });
451
452 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
454 if flavor.is_pandoc_compatible() {
455 crate::utils::pandoc::detect_div_block_ranges(content)
456 } else {
457 Vec::new()
458 }
459 });
460
461 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
463 if flavor == MarkdownFlavor::MkDocs {
464 crate::utils::pymdown_blocks::detect_block_ranges(content)
465 } else {
466 Vec::new()
467 }
468 });
469
470 let skip_ranges = SkipByteRanges {
473 html_comment_ranges: &html_comment_ranges,
474 autodoc_ranges: &autodoc_ranges,
475 pandoc_div_ranges: &pandoc_div_ranges,
476 pymdown_block_ranges: &pymdown_block_ranges,
477 };
478 let (mut lines, emphasis_spans) = profile_section!(
479 "Basic line info",
480 profile,
481 line_computation::compute_basic_line_info(
482 content,
483 &content_lines,
484 &line_offsets,
485 &code_blocks,
486 flavor,
487 &skip_ranges,
488 front_matter_end,
489 )
490 );
491
492 profile_section!(
494 "HTML blocks",
495 profile,
496 heading_detection::detect_html_blocks(content, &mut lines)
497 );
498
499 profile_section!(
501 "ESM blocks",
502 profile,
503 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
504 );
505
506 profile_section!(
508 "JSX block detection",
509 profile,
510 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
511 );
512
513 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
515 "JSX/MDX detection",
516 profile,
517 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
518 );
519
520 profile_section!(
525 "Markdown-in-HTML blocks",
526 profile,
527 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
528 );
529
530 let mdx_context = if flavor == MarkdownFlavor::MDX {
531 mdx::MdxContext::parse(content, &lines)
532 } else {
533 None
534 };
535 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
536 mdx.apply_lines(&mut lines);
537 code_blocks.clone_from(&mdx.code_blocks);
538 code_span_ranges.clone_from(&mdx.code_spans);
539 (mdx.expressions.clone(), mdx.comments.clone())
540 } else {
541 (jsx_expression_ranges, mdx_comment_ranges)
542 };
543
544 profile_section!(
546 "MkDocs constructs",
547 profile,
548 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
549 );
550
551 profile_section!(
556 "Footnote definitions",
557 profile,
558 detect_footnote_definitions(content, &mut lines, &line_offsets)
559 );
560
561 {
564 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
565 for &(start, end) in &code_blocks {
566 let start_line = line_offsets
567 .partition_point(|&offset| offset <= start)
568 .saturating_sub(1);
569 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
570
571 let mut sub_start: Option<usize> = None;
572 for (i, &offset) in line_offsets[start_line..end_line]
573 .iter()
574 .enumerate()
575 .map(|(j, o)| (j + start_line, o))
576 {
577 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
578 if is_real_code && sub_start.is_none() {
579 let byte_start = if i == start_line { start } else { offset };
580 sub_start = Some(byte_start);
581 } else if !is_real_code && sub_start.is_some() {
582 new_code_blocks.push((sub_start.unwrap(), offset));
583 sub_start = None;
584 }
585 }
586 if let Some(s) = sub_start {
587 new_code_blocks.push((s, end));
588 }
589 }
590 code_blocks = new_code_blocks;
591 }
592
593 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
601 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
602 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
603 for &(start, end) in &code_blocks {
604 let start_line = line_offsets
605 .partition_point(|&offset| offset <= start)
606 .saturating_sub(1);
607 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
608
609 let mut sub_start: Option<usize> = None;
611 for (i, &offset) in line_offsets[start_line..end_line]
612 .iter()
613 .enumerate()
614 .map(|(j, o)| (j + start_line, o))
615 {
616 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
617 if is_real_code && sub_start.is_none() {
618 let byte_start = if i == start_line { start } else { offset };
619 sub_start = Some(byte_start);
620 } else if !is_real_code && sub_start.is_some() {
621 new_code_blocks.push((sub_start.unwrap(), offset));
622 sub_start = None;
623 }
624 }
625 if let Some(s) = sub_start {
626 new_code_blocks.push((s, end));
627 }
628 }
629 code_blocks = new_code_blocks;
630 }
631
632 if flavor.supports_jsx() {
636 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
637 for &(start, end) in &code_blocks {
638 let start_line = line_offsets
639 .partition_point(|&offset| offset <= start)
640 .saturating_sub(1);
641 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
642
643 let mut sub_start: Option<usize> = None;
644 for (i, &offset) in line_offsets[start_line..end_line]
645 .iter()
646 .enumerate()
647 .map(|(j, o)| (j + start_line, o))
648 {
649 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
650 if is_real_code && sub_start.is_none() {
651 let byte_start = if i == start_line { start } else { offset };
652 sub_start = Some(byte_start);
653 } else if !is_real_code && sub_start.is_some() {
654 new_code_blocks.push((sub_start.unwrap(), offset));
655 sub_start = None;
656 }
657 }
658 if let Some(s) = sub_start {
659 new_code_blocks.push((s, end));
660 }
661 }
662 code_blocks = new_code_blocks;
663
664 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
671 let mut run: Option<(usize, usize)> = None;
672 for line in &lines {
673 if line.in_jsx_block && line.in_code_block {
674 let line_end = line.byte_offset + line.byte_len;
675 match &mut run {
676 Some((_, end)) => *end = line_end,
677 None => run = Some((line.byte_offset, line_end)),
678 }
679 } else if let Some(r) = run.take() {
680 jsx_fence_ranges.push(r);
681 }
682 }
683 if let Some(r) = run.take() {
684 jsx_fence_ranges.push(r);
685 }
686 if !jsx_fence_ranges.is_empty() {
687 code_blocks.extend(jsx_fence_ranges);
688 code_blocks.sort_by_key(|&(start, _)| start);
689 }
690 }
691
692 let colon_fence_details = profile_section!(
695 "Azure colon fence detection",
696 profile,
697 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
698 );
699 if !colon_fence_details.is_empty() {
700 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
701 code_blocks.sort_by_key(|&(start, _)| start);
702 }
703
704 let myst_directive_ranges = profile_section!(
707 "MyST colon directives",
708 profile,
709 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
710 );
711
712 let myst_comment_ranges = profile_section!(
714 "MyST comments",
715 profile,
716 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
717 );
718
719 profile_section!(
722 "MyST backtick directives",
723 profile,
724 flavor_detection::detect_myst_backtick_directives(
725 content,
726 &mut lines,
727 flavor,
728 &code_block_details,
729 &line_offsets
730 )
731 );
732
733 if flavor.supports_myst_directives() {
736 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
737 for &(start, end) in &code_blocks {
738 let start_line = line_offsets
739 .partition_point(|&offset| offset <= start)
740 .saturating_sub(1);
741 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
742
743 let mut sub_start: Option<usize> = None;
744 for (i, &offset) in line_offsets[start_line..end_line]
745 .iter()
746 .enumerate()
747 .map(|(j, o)| (j + start_line, o))
748 {
749 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
750 if is_real_code && sub_start.is_none() {
751 let byte_start = if i == start_line { start } else { offset };
752 sub_start = Some(byte_start);
753 } else if !is_real_code && sub_start.is_some() {
754 new_code_blocks.push((sub_start.unwrap(), offset));
755 sub_start = None;
756 }
757 }
758 if let Some(s) = sub_start {
759 new_code_blocks.push((s, end));
760 }
761 }
762 code_blocks = new_code_blocks;
763 }
764
765 profile_section!(
767 "Kramdown constructs",
768 profile,
769 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
770 );
771
772 for line in &mut lines {
777 if line.in_kramdown_extension_block {
778 line.list_item = None;
779 line.is_horizontal_rule = false;
780 line.blockquote = None;
781 line.is_kramdown_block_ial = false;
782 }
783 }
784
785 let obsidian_comment_scan = profile_section!(
787 "Obsidian comments",
788 profile,
789 flavor_detection::detect_obsidian_comments(
790 content,
791 &mut lines,
792 flavor,
793 &code_span_ranges,
794 &html_comment_ranges,
795 body_start
796 )
797 );
798 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
799 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
800
801 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
806 unterminated_html_comment,
807 &obsidian_comment_ranges,
808 content,
809 &code_span_ranges,
810 &comment_code_block_ranges,
811 body_start,
812 );
813
814 if let Some(range) = unterminated_html_comment.and_then(|opener| {
827 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
828 .or_else(|| container_comment_range(opener, &containers, &lines, content))
829 }) {
830 html_comment_ranges.push(range);
833
834 for line in &mut lines {
840 let text = line.content(content);
841 let content_start = line.byte_offset + line.indent;
842 let content_end = line.byte_offset + text.trim_end().len();
843 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
844 &html_comment_ranges,
845 content_start,
846 content_end,
847 );
848 line.in_obsidian_comment = false;
849 }
850
851 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
862 content,
863 &mut lines,
864 flavor,
865 &code_span_ranges,
866 &html_comment_ranges,
867 body_start,
868 );
869 obsidian_comment_ranges = obsidian_rescan.ranges;
870 unterminated_obsidian_comment = obsidian_rescan.unterminated;
871 }
872
873 let myst_role_ranges = profile_section!(
875 "MyST roles",
876 profile,
877 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
878 );
879
880 let mut pulldown_result = profile_section!(
884 "Links, images & link ranges",
885 profile,
886 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
887 );
888
889 if let Some(mdx) = &mdx_context {
890 let (links, images) = mdx.links_and_images(content, &lines);
891 pulldown_result.link_byte_ranges = links.iter().map(|link| (link.byte_offset, link.byte_end)).collect();
892 pulldown_result.link_found_positions = links.iter().map(|link| link.byte_offset).collect();
893 pulldown_result.image_found_positions = images.iter().map(|image| image.byte_offset).collect();
894 pulldown_result.links = links;
895 pulldown_result.images = images;
896 pulldown_result.footnote_refs = mdx.footnote_refs();
897 pulldown_result
898 .broken_links
899 .retain(|link| mdx.contains_text(link.span.start, link.span.end));
900 }
901
902 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
904 let mut blockquote_headings = profile_section!(
905 "Headings & blockquotes",
906 profile,
907 heading_detection::detect_headings_and_blockquotes(
908 &content_lines,
909 &mut lines,
910 flavor,
911 &html_comment_ranges,
912 &html_blocks,
913 &code_blocks,
914 &code_span_ranges,
915 &pulldown_result.link_byte_ranges,
916 front_matter_end,
917 mdx_flow_lines.as_deref(),
918 )
919 );
920
921 for line in &mut lines {
923 if line.in_kramdown_extension_block {
924 line.heading = None;
925 }
926 }
927 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
928 if line.in_kramdown_extension_block {
929 *heading = None;
930 }
931 }
932
933 for line in &mut lines {
944 if line.is_horizontal_rule
945 && (line.in_code_block
946 || line.in_html_block
947 || line.in_html_comment
948 || line.in_math_block
949 || line.in_mdx_comment
950 || line.in_obsidian_comment)
951 {
952 line.is_horizontal_rule = false;
953 }
954 }
955
956 let mut code_spans = profile_section!(
958 "Code spans",
959 profile,
960 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
961 );
962
963 if flavor == MarkdownFlavor::MkDocs {
967 let extra = profile_section!(
968 "MkDocs code spans",
969 profile,
970 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
971 );
972 if !extra.is_empty() {
973 code_spans.extend(extra);
974 code_spans.sort_by_key(|span| span.byte_offset);
975 }
976 }
977
978 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
983 let extra = profile_section!(
984 "MDX JSX code spans",
985 profile,
986 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
987 );
988 if !extra.is_empty() {
989 code_spans.extend(extra);
990 code_spans.sort_by_key(|span| span.byte_offset);
991 }
992 }
993
994 for span in &code_spans {
997 if span.end_line > span.line {
998 for line_num in (span.line + 1)..=span.end_line {
1000 if let Some(line_info) = lines.get_mut(line_num - 1) {
1001 line_info.in_code_span_continuation = true;
1002 }
1003 }
1004 }
1005 }
1006
1007 let (links, images, broken_links, footnote_refs) = profile_section!(
1009 "Links & images finalize",
1010 profile,
1011 link_parser::finalize_links_and_images(
1012 content,
1013 &lines,
1014 flavor,
1015 &link_parser::LinkExclusions {
1016 code_blocks: &code_blocks,
1017 code_spans: &code_spans,
1018 html_comment_ranges: &html_comment_ranges,
1019 mdx: mdx_context.as_ref(),
1020 },
1021 pulldown_result,
1022 )
1023 );
1024
1025 let reference_defs = profile_section!("Reference defs", profile, {
1026 if let Some(mdx) = &mdx_context {
1027 mdx.reference_defs(content)
1028 } else {
1029 link_parser::parse_reference_defs(content, &lines)
1030 }
1031 });
1032
1033 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1034
1035 let char_frequency = profile_section!(
1037 "Char frequency",
1038 profile,
1039 line_computation::compute_char_frequency(content)
1040 );
1041
1042 let table_blocks = profile_section!(
1044 "Table blocks",
1045 profile,
1046 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
1047 content,
1048 &code_blocks,
1049 &code_spans,
1050 &html_comment_ranges,
1051 flavor,
1052 )
1053 );
1054
1055 let links = links
1058 .into_iter()
1059 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1060 .collect::<Vec<_>>();
1061 let images = images
1062 .into_iter()
1063 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1064 .collect::<Vec<_>>();
1065 let broken_links = broken_links
1066 .into_iter()
1067 .filter(|bl| {
1068 let line_idx = line_offsets
1070 .partition_point(|&offset| offset <= bl.span.start)
1071 .saturating_sub(1);
1072 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1073 })
1074 .collect::<Vec<_>>();
1075 let footnote_refs = footnote_refs
1076 .into_iter()
1077 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1078 .collect::<Vec<_>>();
1079 let reference_defs = reference_defs
1080 .into_iter()
1081 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1082 .collect::<Vec<_>>();
1083 let list_blocks = list_blocks
1084 .into_iter()
1085 .filter(|block| {
1086 !lines
1087 .get(block.start_line - 1)
1088 .is_some_and(|l| l.in_kramdown_extension_block)
1089 })
1090 .collect::<Vec<_>>();
1091 let table_blocks = table_blocks
1092 .into_iter()
1093 .filter(|block| {
1094 !lines
1096 .get(block.start_line)
1097 .is_some_and(|l| l.in_kramdown_extension_block)
1098 })
1099 .collect::<Vec<_>>();
1100 let emphasis_spans = emphasis_spans
1101 .into_iter()
1102 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1103 .collect::<Vec<_>>();
1104
1105 for block in &list_blocks {
1109 for line_num in block.start_line..=block.end_line {
1111 if let Some(li) = lines.get_mut(line_num - 1) {
1112 li.in_list_block = true;
1113 }
1114 }
1115 }
1116 for block in &table_blocks {
1117 for idx in block.start_line..=block.end_line {
1119 if let Some(li) = lines.get_mut(idx) {
1120 li.in_table_block = true;
1121 }
1122 }
1123 }
1124
1125 let reference_defs_map: HashMap<String, usize> = reference_defs
1127 .iter()
1128 .enumerate()
1129 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1130 .collect();
1131
1132 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1134 .iter()
1135 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1136 (Some(start), Some(end)) => Some((start, end)),
1137 _ => None,
1138 })
1139 .collect();
1140
1141 let line_index = profile_section!(
1143 "Line index",
1144 profile,
1145 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
1146 content,
1147 line_offsets.clone(),
1148 &code_blocks,
1149 )
1150 );
1151
1152 let jinja_ranges = profile_section!(
1154 "Jinja ranges",
1155 profile,
1156 crate::utils::jinja_utils::find_jinja_ranges(content)
1157 );
1158
1159 let citation_ranges = profile_section!("Citation ranges", profile, {
1161 if flavor.is_pandoc_compatible() {
1162 crate::utils::pandoc::find_citation_ranges(content)
1163 } else {
1164 Vec::new()
1165 }
1166 });
1167
1168 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1170 if flavor.is_pandoc_compatible() {
1171 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1172 } else {
1173 Vec::new()
1174 }
1175 });
1176
1177 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1179 if flavor.is_pandoc_compatible() {
1180 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1181 } else {
1182 std::collections::HashSet::new()
1183 }
1184 });
1185
1186 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1188 if flavor.is_pandoc_compatible() {
1189 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1190 } else {
1191 Vec::new()
1192 }
1193 });
1194
1195 let example_reference_ranges = profile_section!("Example references", profile, {
1197 if flavor.is_pandoc_compatible() {
1198 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1199 } else {
1200 Vec::new()
1201 }
1202 });
1203
1204 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1206 if flavor.is_pandoc_compatible() {
1207 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1208 } else {
1209 Vec::new()
1210 }
1211 });
1212
1213 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1215 if flavor.is_pandoc_compatible() {
1216 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1217 } else {
1218 Vec::new()
1219 }
1220 });
1221
1222 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1224 if flavor.is_pandoc_compatible() {
1225 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1226 } else {
1227 Vec::new()
1228 }
1229 });
1230
1231 let line_block_ranges = profile_section!("Line block ranges", profile, {
1233 if flavor.is_pandoc_compatible() {
1234 crate::utils::pandoc::detect_line_block_ranges(content)
1235 } else {
1236 Vec::new()
1237 }
1238 });
1239
1240 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1242 if flavor.is_pandoc_compatible() {
1243 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1244 } else {
1245 Vec::new()
1246 }
1247 });
1248
1249 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1251 if flavor.is_pandoc_compatible() {
1252 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1253 } else {
1254 Vec::new()
1255 }
1256 });
1257
1258 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1260 if flavor.is_pandoc_compatible() {
1261 crate::utils::pandoc::detect_grid_table_ranges(content)
1262 } else {
1263 Vec::new()
1264 }
1265 });
1266
1267 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1269 if flavor.is_pandoc_compatible() {
1270 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1271 } else {
1272 Vec::new()
1273 }
1274 });
1275
1276 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1278 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
1279 let mut ranges = Vec::new();
1280 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
1281 ranges.push((mat.start(), mat.end()));
1282 }
1283 ranges
1284 });
1285
1286 let inline_config =
1287 InlineConfig::from_content_with_code_blocks(content, &code_blocks, &code_span_byte_ranges(&code_spans));
1288 Self {
1289 content,
1290 content_lines,
1291 line_offsets,
1292 code_blocks,
1293 code_block_details,
1294 strong_spans,
1295 line_to_list,
1296 list_start_values,
1297 definition_lists,
1298 commonmark_ordered_lists_cache: OnceLock::new(),
1299 lines,
1300 blockquote_headings,
1301 links,
1302 images,
1303 broken_links,
1304 footnote_refs,
1305 reference_defs,
1306 reference_defs_map,
1307 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1308 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1310 math_byte_ranges_cache: OnceLock::new(), list_blocks,
1312 char_frequency,
1313 html_tags_cache: OnceLock::new(),
1314 jsx_component_tags_cache: OnceLock::new(),
1315 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1316 bare_urls_cache: OnceLock::new(),
1317 has_mixed_list_nesting_cache: OnceLock::new(),
1318 html_comment_ranges,
1319 table_blocks,
1320 line_index,
1321 jinja_ranges,
1322 flavor,
1323 source_file,
1324 link_target_policy: None,
1325 invalid_utf8: None,
1326 jsx_expression_ranges,
1327 mdx_comment_ranges,
1328 citation_ranges,
1329 pandoc_div_ranges,
1330 colon_fence_details,
1331 inline_footnote_ranges,
1332 pandoc_header_slugs,
1333 example_list_marker_ranges,
1334 example_reference_ranges,
1335 sub_super_ranges,
1336 inline_code_attr_ranges,
1337 bracketed_span_ranges,
1338 line_block_ranges,
1339 pipe_table_caption_ranges,
1340 pandoc_metadata_ranges,
1341 grid_table_ranges,
1342 multi_line_table_ranges,
1343 shortcode_ranges,
1344 link_title_ranges,
1345 code_span_byte_ranges: code_span_ranges,
1346 inline_config,
1347 obsidian_comment_ranges,
1348 unterminated_html_comment,
1349 unterminated_obsidian_comment,
1350 lazy_cont_lines_cache: OnceLock::new(),
1351 myst_directive_ranges,
1352 myst_comment_ranges,
1353 myst_role_ranges,
1354 front_matter_end,
1355 }
1356 }
1357
1358 pub fn front_matter_end_line(&self) -> usize {
1363 self.front_matter_end
1364 }
1365
1366 #[inline]
1369 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1370 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1372 idx > 0 && pos < ranges[idx - 1].1
1374 }
1375
1376 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1378 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1379 }
1380
1381 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
1386 let line = &self.lines[line_number - 1];
1387 heading_detection::ends_with_hard_break(
1388 line.content(self.content),
1389 line.byte_offset,
1390 &self.code_span_byte_ranges,
1391 )
1392 }
1393
1394 pub fn is_in_link(&self, pos: usize) -> bool {
1396 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
1397 }
1398
1399 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1401 let bare_urls = self.bare_urls();
1402 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1404 idx > 0 && pos < bare_urls[idx - 1].byte_end
1405 }
1406
1407 pub fn inline_config(&self) -> &InlineConfig {
1409 &self.inline_config
1410 }
1411
1412 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1417 &self.colon_fence_details
1418 }
1419
1420 pub fn raw_lines(&self) -> &[&'a str] {
1424 &self.content_lines
1425 }
1426
1427 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1432 self.inline_config.is_rule_disabled(rule_name, line_number)
1433 }
1434
1435 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1437 Arc::clone(
1438 self.code_spans_cache
1439 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1440 )
1441 }
1442
1443 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1447 self.math_byte_ranges_cache
1448 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1449 }
1450
1451 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1453 Arc::clone(
1454 self.math_spans_cache
1455 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1456 )
1457 }
1458
1459 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
1461 self.bracket_math_cache
1462 .get_or_init(|| bracket_math::parse(self.content, &self.lines, &self.code_spans(), &self.list_blocks))
1463 }
1464
1465 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1467 let math_spans = self.math_spans();
1468 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1470 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1471 }
1472
1473 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1475 &self.html_comment_ranges
1476 }
1477
1478 pub fn unterminated_html_comment(&self) -> Option<usize> {
1483 self.unterminated_html_comment
1484 }
1485
1486 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1490 self.unterminated_obsidian_comment
1491 }
1492
1493 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1497 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1498 }
1499
1500 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1505 if self.obsidian_comment_ranges.is_empty() {
1506 return false;
1507 }
1508
1509 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1511 self.is_in_obsidian_comment(byte_pos)
1512 }
1513
1514 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1516 &self.myst_directive_ranges
1517 }
1518
1519 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1521 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1522 }
1523
1524 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1526 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1527 }
1528
1529 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1536 if !self.flavor.supports_myst_directives() {
1537 return false;
1538 }
1539 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1540 info.in_myst_directive
1541 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1542 })
1543 }
1544
1545 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1547 tags.into_iter()
1548 .filter(|tag| {
1549 !self
1550 .lines
1551 .get(tag.line - 1)
1552 .is_some_and(|l| l.in_kramdown_extension_block)
1553 })
1554 .collect()
1555 }
1556
1557 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1563 Arc::clone(self.html_tags_cache.get_or_init(|| {
1564 let (html_tags, jsx_component_tags) =
1565 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1566 let _ = self
1568 .jsx_component_tags_cache
1569 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1570 Arc::new(self.filter_kramdown_tags(html_tags))
1571 }))
1572 }
1573
1574 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1577 if let Some(cached) = self.jsx_component_tags_cache.get() {
1578 return Arc::clone(cached);
1579 }
1580 let _ = self.html_tags();
1582 Arc::clone(
1583 self.jsx_component_tags_cache
1584 .get()
1585 .expect("html_tags() populates jsx_component_tags_cache"),
1586 )
1587 }
1588
1589 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1591 Arc::clone(
1592 self.emphasis_spans_cache
1593 .get()
1594 .expect("emphasis_spans_cache initialized during construction"),
1595 )
1596 }
1597
1598 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1600 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1601 Arc::new(element_parsers::parse_bare_urls(
1602 self.content,
1603 &self.lines,
1604 &self.code_blocks,
1605 ))
1606 }))
1607 }
1608
1609 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1611 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1612 Arc::new(element_parsers::detect_lazy_continuation_lines(
1613 self.content,
1614 &self.lines,
1615 &self.line_offsets,
1616 ))
1617 }))
1618 }
1619
1620 pub fn has_mixed_list_nesting(&self) -> bool {
1624 *self
1625 .has_mixed_list_nesting_cache
1626 .get_or_init(|| self.compute_mixed_list_nesting())
1627 }
1628
1629 fn compute_mixed_list_nesting(&self) -> bool {
1631 let mut stack: Vec<(usize, bool)> = Vec::new();
1636 let mut last_was_blank = false;
1637
1638 for line_info in &self.lines {
1639 if line_info.in_code_block
1641 || line_info.in_front_matter
1642 || line_info.in_mkdocstrings
1643 || line_info.in_html_comment
1644 || line_info.in_mdx_comment
1645 || line_info.in_esm_block
1646 {
1647 continue;
1648 }
1649
1650 if line_info.is_blank {
1652 last_was_blank = true;
1653 continue;
1654 }
1655
1656 if let Some(list_item) = &line_info.list_item {
1657 let current_pos = if list_item.marker_column == 1 {
1659 0
1660 } else {
1661 list_item.marker_column
1662 };
1663
1664 if last_was_blank && current_pos == 0 {
1666 stack.clear();
1667 }
1668 last_was_blank = false;
1669
1670 while let Some(&(pos, _)) = stack.last() {
1672 if pos >= current_pos {
1673 stack.pop();
1674 } else {
1675 break;
1676 }
1677 }
1678
1679 if let Some(&(_, parent_is_ordered)) = stack.last()
1681 && parent_is_ordered != list_item.is_ordered
1682 {
1683 return true; }
1685
1686 stack.push((current_pos, list_item.is_ordered));
1687 } else {
1688 last_was_blank = false;
1690 }
1691 }
1692
1693 false
1694 }
1695
1696 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1702 match self.line_offsets.binary_search(&offset) {
1703 Ok(line) => (line + 1, 1),
1704 Err(line) => {
1705 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1706 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1708 (line, col)
1709 }
1710 }
1711 }
1712
1713 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
1719 self.line_index.get_line_start_byte(line_number)
1720 }
1721
1722 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
1728 self.line_index.line_col_to_byte_range(line_number, column)
1729 }
1730
1731 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
1736 self.line_index
1737 .line_col_to_byte_range_with_length(line_number, column, length)
1738 }
1739
1740 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
1743 self.line_index.whole_line_range(line_number)
1744 }
1745
1746 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
1751 self.line_index.line_text_range(line_number, start_column, end_column)
1752 }
1753
1754 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
1757 self.line_index.line_content_range(line_number)
1758 }
1759
1760 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
1762 self.line_index.multi_line_range(start_line, end_line)
1763 }
1764
1765 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1767 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1769 return true;
1770 }
1771
1772 self.is_byte_offset_in_code_span(pos)
1774 }
1775
1776 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1778 if line_num > 0 {
1779 self.lines.get(line_num - 1)
1780 } else {
1781 None
1782 }
1783 }
1784
1785 pub fn links(&self) -> &[ParsedLink<'a>] {
1787 &self.links
1788 }
1789
1790 pub fn images(&self) -> &[ParsedImage<'a>] {
1792 &self.images
1793 }
1794
1795 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
1797 &self.broken_links
1798 }
1799
1800 pub fn footnote_references(&self) -> &[FootnoteRef] {
1802 &self.footnote_refs
1803 }
1804
1805 pub fn reference_definitions(&self) -> &[ReferenceDef] {
1807 &self.reference_defs
1808 }
1809
1810 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
1812 let start = self.links.partition_point(|link| link.line < line_number);
1813 let end = self.links.partition_point(|link| link.line <= line_number);
1814 &self.links[start..end]
1815 }
1816
1817 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
1819 let start = self.images.partition_point(|image| image.line < line_number);
1820 let end = self.images.partition_point(|image| image.line <= line_number);
1821 &self.images[start..end]
1822 }
1823
1824 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1826 self.links
1827 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
1828 .ok()
1829 .map(|index| &self.links[index])
1830 }
1831
1832 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1834 self.images
1835 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
1836 .ok()
1837 .map(|index| &self.images[index])
1838 }
1839
1840 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1842 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1843 self.links
1844 .get(index.checked_sub(1)?)
1845 .filter(|link| byte_offset < link.byte_end)
1846 }
1847
1848 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1850 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
1851 self.images
1852 .get(index.checked_sub(1)?)
1853 .filter(|image| byte_offset < image.byte_end)
1854 }
1855
1856 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
1858 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1859 &self.links[..end]
1860 }
1861
1862 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
1864 let normalized_id = ref_id.to_lowercase();
1865 self.reference_defs_map
1866 .get(&normalized_id)
1867 .map(|&index| &self.reference_defs[index])
1868 }
1869
1870 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1872 self.reference_definition(ref_id)
1873 .map(|definition| definition.url.as_str())
1874 }
1875
1876 pub fn is_in_list_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_list_block
1882 }
1883
1884 pub fn is_in_definition_list(&self, line_num: usize) -> bool {
1892 let lists = &self.definition_lists.outer;
1893 let idx = lists.partition_point(|&(start, _)| start <= line_num);
1894 idx > 0 && line_num <= lists[idx - 1].1
1895 }
1896
1897 pub fn is_definition_term(&self, line_num: usize) -> bool {
1899 let terms = &self.definition_lists.terms;
1900 let idx = terms.partition_point(|&(_, end)| end < line_num);
1901 terms.get(idx).is_some_and(|&(start, _)| start <= line_num)
1902 }
1903
1904 pub fn definition_text_at(&self, line_num: usize) -> Option<&DefinitionText> {
1906 let texts = &self.definition_lists.texts;
1907 let idx = texts.partition_point(|text| text.end_line < line_num);
1908 texts.get(idx).filter(|text| text.start_line <= line_num)
1909 }
1910
1911 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1913 if line_num == 0 || line_num > self.lines.len() {
1914 return false;
1915 }
1916 self.lines[line_num - 1].in_html_block
1917 }
1918
1919 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1925 if line_num == 0 || line_num > self.lines.len() {
1926 return false;
1927 }
1928 self.lines[line_num - 1].in_table_block
1929 }
1930
1931 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1933 if line_num == 0 || line_num > self.lines.len() {
1934 return false;
1935 }
1936
1937 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1941 let code_spans = self.code_spans();
1942 code_spans.iter().any(|span| {
1943 if line_num < span.line || line_num > span.end_line {
1945 return false;
1946 }
1947
1948 if span.line == span.end_line {
1949 col_0indexed >= span.start_col && col_0indexed < span.end_col
1951 } else if line_num == span.line {
1952 col_0indexed >= span.start_col
1954 } else if line_num == span.end_line {
1955 col_0indexed < span.end_col
1957 } else {
1958 true
1960 }
1961 })
1962 }
1963
1964 #[inline]
1966 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1967 let code_spans = self.code_spans();
1968 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1969 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1970 }
1971
1972 #[inline]
1974 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1975 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1976 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1977 }
1978
1979 #[inline]
1981 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1982 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1983 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1984 }
1985
1986 #[inline]
1989 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1990 let tags = self.html_tags();
1991 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1992 idx > 0 && byte_pos < tags[idx - 1].byte_end
1993 }
1994
1995 #[inline]
1999 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
2000 if !self.flavor.supports_jsx() {
2001 return false;
2002 }
2003 let tags = self.jsx_component_tags();
2004 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2005 idx > 0 && byte_pos < tags[idx - 1].byte_end
2006 }
2007
2008 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
2010 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
2011 }
2012
2013 #[inline]
2015 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
2016 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
2017 }
2018
2019 #[inline]
2021 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
2022 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
2023 }
2024
2025 #[inline]
2028 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
2029 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
2030 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
2031 }
2032
2033 #[inline]
2035 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2036 &self.citation_ranges
2037 }
2038
2039 #[inline]
2042 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
2043 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
2044 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
2045 }
2046
2047 #[inline]
2050 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
2051 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
2052 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
2053 }
2054
2055 #[inline]
2058 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2059 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2060 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2061 }
2062
2063 #[inline]
2066 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2067 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2068 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2069 }
2070
2071 #[inline]
2074 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2075 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
2076 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
2077 }
2078
2079 #[inline]
2083 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
2084 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
2085 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
2086 }
2087
2088 #[inline]
2091 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
2092 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
2093 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
2094 }
2095
2096 #[inline]
2099 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
2100 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
2101 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
2102 }
2103
2104 #[inline]
2108 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
2109 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
2110 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
2111 }
2112
2113 #[inline]
2116 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
2117 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
2118 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
2119 }
2120
2121 #[inline]
2124 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
2125 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
2126 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
2127 }
2128
2129 #[inline]
2132 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
2133 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
2134 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
2135 }
2136
2137 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
2142 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
2143 self.pandoc_header_slugs.contains(&slug)
2144 }
2145
2146 #[inline]
2152 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
2153 self.pandoc_header_slugs.contains(slug)
2154 }
2155
2156 #[inline]
2158 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
2159 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
2160 }
2161
2162 #[inline]
2164 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
2165 &self.shortcode_ranges
2166 }
2167
2168 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
2170 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
2171 }
2172
2173 pub fn has_char(&self, ch: char) -> bool {
2175 match ch {
2176 '#' => self.char_frequency.hash_count > 0,
2177 '*' => self.char_frequency.asterisk_count > 0,
2178 '_' => self.char_frequency.underscore_count > 0,
2179 '-' => self.char_frequency.hyphen_count > 0,
2180 '+' => self.char_frequency.plus_count > 0,
2181 '>' => self.char_frequency.gt_count > 0,
2182 '|' => self.char_frequency.pipe_count > 0,
2183 '[' => self.char_frequency.bracket_count > 0,
2184 '`' => self.char_frequency.backtick_count > 0,
2185 '<' => self.char_frequency.lt_count > 0,
2186 '!' => self.char_frequency.exclamation_count > 0,
2187 '\n' => self.char_frequency.newline_count > 0,
2188 _ => self.content.contains(ch), }
2190 }
2191
2192 pub fn char_count(&self, ch: char) -> usize {
2194 match ch {
2195 '#' => self.char_frequency.hash_count,
2196 '*' => self.char_frequency.asterisk_count,
2197 '_' => self.char_frequency.underscore_count,
2198 '-' => self.char_frequency.hyphen_count,
2199 '+' => self.char_frequency.plus_count,
2200 '>' => self.char_frequency.gt_count,
2201 '|' => self.char_frequency.pipe_count,
2202 '[' => self.char_frequency.bracket_count,
2203 '`' => self.char_frequency.backtick_count,
2204 '<' => self.char_frequency.lt_count,
2205 '!' => self.char_frequency.exclamation_count,
2206 '\n' => self.char_frequency.newline_count,
2207 _ => self.content.matches(ch).count(), }
2209 }
2210
2211 pub fn likely_has_headings(&self) -> bool {
2214 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.content.contains('=')
2215 }
2216
2217 pub fn likely_has_lists(&self) -> bool {
2221 self.char_frequency.asterisk_count > 0
2222 || self.char_frequency.hyphen_count > 0
2223 || self.char_frequency.plus_count > 0
2224 }
2225
2226 pub fn likely_has_emphasis(&self) -> bool {
2228 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
2229 }
2230
2231 pub fn likely_has_tables(&self) -> bool {
2233 self.char_frequency.pipe_count > 2
2234 }
2235
2236 pub fn likely_has_blockquotes(&self) -> bool {
2238 self.char_frequency.gt_count > 0
2239 }
2240
2241 pub fn likely_has_code(&self) -> bool {
2243 self.char_frequency.backtick_count > 0
2244 }
2245
2246 pub fn likely_has_links_or_images(&self) -> bool {
2248 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
2249 }
2250
2251 pub fn likely_has_html(&self) -> bool {
2253 self.char_frequency.lt_count > 0
2254 }
2255
2256 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
2261 if let Some(line_info) = self.lines.get(line_idx)
2262 && let Some(ref bq) = line_info.blockquote
2263 {
2264 bq.prefix.trim_end().to_string()
2265 } else {
2266 String::new()
2267 }
2268 }
2269
2270 #[inline]
2281 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
2282 let idx = match lines.binary_search_by(|line| {
2284 if byte_offset < line.byte_offset {
2285 std::cmp::Ordering::Greater
2286 } else if byte_offset > line.byte_offset + line.byte_len {
2287 std::cmp::Ordering::Less
2288 } else {
2289 std::cmp::Ordering::Equal
2290 }
2291 }) {
2292 Ok(idx) => idx,
2293 Err(idx) => idx.saturating_sub(1),
2294 };
2295
2296 let line = &lines[idx];
2297 let line_num = idx + 1;
2298 let byte_col = byte_offset.saturating_sub(line.byte_offset);
2299 let col = byte_to_char_count(line.content(content), byte_col) - 1;
2302
2303 (idx, line_num, col)
2304 }
2305
2306 #[inline]
2308 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
2309 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
2311
2312 if idx > 0 {
2314 let span = &code_spans[idx - 1];
2315 if offset >= span.byte_offset && offset < span.byte_end {
2316 return true;
2317 }
2318 }
2319
2320 false
2321 }
2322
2323 #[must_use]
2343 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
2344 ValidHeadingsIter::new(&self.lines)
2345 }
2346
2347 #[must_use]
2351 pub fn has_valid_headings(&self) -> bool {
2352 self.lines.iter().any(|line| line.heading.is_some())
2353 }
2354
2355 #[must_use]
2357 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
2358 ParsedListItemsIter::new(&self.lines)
2359 }
2360
2361 #[must_use]
2363 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
2364 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
2365 Some(ParsedListItem::new(
2366 line_num,
2367 line_info.list_item.as_deref()?,
2368 line_info,
2369 ))
2370 }
2371
2372 #[must_use]
2374 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
2375 ParsedListBlocks::new(&self.list_blocks, &self.lines)
2376 }
2377
2378 #[must_use]
2382 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
2383 list_blocks::item_lines_by_list(self.content, &self.lines, block)
2384 }
2385
2386 #[must_use]
2388 pub fn has_list_items(&self) -> bool {
2389 self.lines.iter().any(|line| line.list_item.is_some())
2390 }
2391
2392 #[must_use]
2394 pub fn has_unordered_list_items(&self) -> bool {
2395 self.lines
2396 .iter()
2397 .any(|line| line.list_item.as_ref().is_some_and(|item| !item.is_ordered))
2398 }
2399
2400 #[must_use]
2402 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
2403 let lists = self
2404 .commonmark_ordered_lists_cache
2405 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
2406 CommonMarkOrderedLists::new(lists, &self.lines)
2407 }
2408
2409 #[must_use]
2417 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
2418 ParsedHeadingsIter::new(&self.lines, &self.blockquote_headings)
2419 }
2420
2421 #[must_use]
2423 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
2424 let idx = line_num.checked_sub(1)?;
2425 let line_info = self.lines.get(idx)?;
2426 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
2427 Some(heading) => (heading, 0),
2428 None => (
2429 self.blockquote_headings.get(idx)?.as_deref()?,
2430 line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level),
2431 ),
2432 };
2433 Some(ParsedHeading {
2434 line_num,
2435 heading,
2436 line_info,
2437 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
2438 blockquote_depth,
2439 })
2440 }
2441}
2442
2443fn container_comment_range(
2455 opener: usize,
2456 containers: &flavor_detection::ContainerLines,
2457 lines: &[types::LineInfo],
2458 content: &str,
2459) -> Option<crate::utils::skip_context::ByteRange> {
2460 let line_index = lines
2461 .partition_point(|line| line.byte_offset <= opener)
2462 .checked_sub(1)?;
2463 let line = lines.get(line_index)?;
2464 if line.byte_offset + line.indent != opener {
2465 return None;
2466 }
2467 if !containers.is_container_body(line_index) {
2468 return None;
2469 }
2470 let end_line = lines.get(containers.body_end_line(line_index)?)?;
2471 Some(crate::utils::skip_context::ByteRange {
2472 start: opener,
2473 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
2474 })
2475}
2476
2477#[derive(Debug)]
2479struct DefinitionListLines {
2480 outer: Vec<(usize, usize)>,
2483 terms: Vec<(usize, usize)>,
2485 texts: Vec<DefinitionText>,
2487}
2488
2489impl DefinitionListLines {
2490 fn new(
2491 line_offsets: &[usize],
2492 items: &[(usize, usize)],
2493 terms: &[(usize, usize)],
2494 texts: &[crate::utils::code_block_utils::DefinitionTextDetail],
2495 ) -> Self {
2496 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
2499 let last_line_of = |start: usize, end: usize| line_of(end.saturating_sub(1).max(start));
2500
2501 let mut outer: Vec<(usize, usize)> = Vec::new();
2504 for &(start, end) in items {
2505 let (start_line, end_line) = (line_of(start), last_line_of(start, end));
2506 match outer.last_mut() {
2507 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
2508 _ => outer.push((start_line, end_line)),
2509 }
2510 }
2511 let terms = terms
2512 .iter()
2513 .map(|&(start, end)| (line_of(start), last_line_of(start, end)))
2514 .collect();
2515
2516 let texts = texts
2517 .iter()
2518 .map(|text| {
2519 let start_line = line_of(text.start);
2520 let line_start = line_offsets[start_line - 1];
2521 let on_marker_line = line_of(text.definition_start) == start_line;
2522 DefinitionText {
2523 start_line,
2524 end_line: last_line_of(text.start, text.end),
2525 marker_prefix_len: on_marker_line.then(|| text.start - line_start),
2526 }
2527 })
2528 .collect();
2529
2530 Self { outer, terms, texts }
2531 }
2532}
2533
2534fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
2543 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
2544
2545 let options = crate::utils::rumdl_parser_options();
2546 let parser = Parser::new_ext(content, options).into_offset_iter();
2547
2548 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
2550 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
2551 let mut in_footnote = false;
2552
2553 for (event, range) in parser {
2554 match event {
2555 Event::Start(Tag::FootnoteDefinition(_)) => {
2556 in_footnote = true;
2557 footnote_ranges.push((range.start, range.end));
2558 }
2559 Event::End(TagEnd::FootnoteDefinition) => {
2560 in_footnote = false;
2561 }
2562 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
2563 fenced_code_ranges.push((range.start, range.end));
2564 }
2565 _ => {}
2566 }
2567 }
2568
2569 let byte_to_line = |byte_offset: usize| -> usize {
2570 line_offsets
2571 .partition_point(|&offset| offset <= byte_offset)
2572 .saturating_sub(1)
2573 };
2574
2575 for &(start, end) in &footnote_ranges {
2577 let start_line = byte_to_line(start);
2578 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2579
2580 for line in &mut lines[start_line..end_line] {
2581 line.in_footnote_definition = true;
2582 line.in_code_block = false;
2583 }
2584 }
2585
2586 for &(start, end) in &fenced_code_ranges {
2588 let start_line = byte_to_line(start);
2589 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2590
2591 for line in &mut lines[start_line..end_line] {
2592 line.in_code_block = true;
2593 }
2594 }
2595}