1pub mod types;
2pub(crate) use heading_detection::is_paragraph_text_line;
3pub(crate) use link_parser::{image_pattern, link_pattern};
4pub(crate) use list_blocks::column_at;
5pub use types::*;
6
7mod bracket_math;
8mod element_parsers;
9mod flavor_detection;
10mod heading_detection;
11mod line_computation;
12mod link_parser;
13mod list_blocks;
14mod mdx;
15#[cfg(test)]
16mod tests;
17
18use crate::config::MarkdownFlavor;
19use crate::inline_config::InlineConfig;
20use crate::rules::front_matter_utils::FrontMatterUtils;
21use crate::utils::code_block_utils::{CodeBlockDetail, CodeBlockUtils};
22use crate::utils::range_utils::byte_to_char_count;
23use std::collections::HashMap;
24use std::ops::Range;
25use std::path::{Path, PathBuf};
26
27#[derive(Debug, Clone)]
30pub struct LinkTargetPolicy {
31 supplied_paths: Arc<std::collections::HashSet<PathBuf>>,
32 supplied_dirs: Arc<std::collections::HashSet<PathBuf>>,
36 allow_disk_fallback: bool,
37}
38
39impl LinkTargetPolicy {
40 pub fn open_world<I, P>(paths: I) -> Self
43 where
44 I: IntoIterator<Item = P>,
45 P: AsRef<Path>,
46 {
47 Self::from_paths(paths, true)
48 }
49
50 pub fn closed_world<I, P>(paths: I) -> Self
52 where
53 I: IntoIterator<Item = P>,
54 P: AsRef<Path>,
55 {
56 Self::from_paths(paths, false)
57 }
58
59 fn from_paths<I, P>(paths: I, allow_disk_fallback: bool) -> Self
60 where
61 I: IntoIterator<Item = P>,
62 P: AsRef<Path>,
63 {
64 let mut roots = Vec::new();
65 if let Ok(cwd) = std::env::current_dir() {
66 if let Ok(canonical_cwd) = cwd.canonicalize()
67 && canonical_cwd != cwd
68 {
69 roots.push(canonical_cwd);
70 }
71 roots.push(cwd);
72 }
73 Self::from_paths_with_roots(paths, allow_disk_fallback, roots)
74 }
75
76 fn from_paths_with_roots<I, P, R, Q>(paths: I, allow_disk_fallback: bool, roots: R) -> Self
77 where
78 I: IntoIterator<Item = P>,
79 P: AsRef<Path>,
80 R: IntoIterator<Item = Q>,
81 Q: AsRef<Path>,
82 {
83 let roots: Vec<PathBuf> = roots
84 .into_iter()
85 .map(|root| crate::workspace_index::normalize_relative_path(root.as_ref()))
86 .collect();
87 let mut supplied_paths = std::collections::HashSet::new();
88 for path in paths {
89 let path = path.as_ref();
90 supplied_paths.insert(crate::workspace_index::normalize_relative_path(path));
91
92 if path.is_relative() {
93 for root in &roots {
94 supplied_paths.insert(crate::workspace_index::normalize_relative_path(&root.join(path)));
95 }
96 } else {
97 let resolved = crate::discovery::resolve_for_matching(path);
103 let relative = [path, resolved.as_path()].into_iter().find_map(|spelling| {
104 roots
105 .iter()
106 .find_map(|source_root| spelling.strip_prefix(source_root).ok())
107 });
108 if let Some(relative) = relative {
109 supplied_paths.insert(crate::workspace_index::normalize_relative_path(relative));
110 for root in &roots {
111 supplied_paths.insert(crate::workspace_index::normalize_relative_path(&root.join(relative)));
112 }
113 }
114 }
115 }
116 let escapes_roots = |path: &Path| {
125 if path.is_absolute() {
126 !roots.iter().any(|root| path.starts_with(root))
127 } else {
128 path.starts_with("..")
129 }
130 };
131 let outside_root = |absolute: bool| {
132 supplied_paths
133 .iter()
134 .filter(|path| path.is_absolute() == absolute && escapes_roots(path))
135 .filter_map(|path| path.parent())
136 .fold(None::<&Path>, |common, dir| {
137 Some(common.map_or(dir, |common| {
138 common
139 .ancestors()
140 .find(|ancestor| dir.starts_with(ancestor))
141 .unwrap_or(common)
142 }))
143 })
144 };
145 let outside_roots = [outside_root(true), outside_root(false)];
146 let supplied_dirs = supplied_paths
147 .iter()
148 .flat_map(|path| path.ancestors().skip(1))
149 .filter(|dir| {
150 !escapes_roots(dir)
151 || outside_roots
152 .iter()
153 .flatten()
154 .any(|outside_root| dir.starts_with(outside_root))
155 })
156 .map(Path::to_path_buf)
157 .collect();
158 Self {
159 supplied_paths: Arc::new(supplied_paths),
160 supplied_dirs: Arc::new(supplied_dirs),
161 allow_disk_fallback,
162 }
163 }
164
165 pub fn contains(&self, path: &Path) -> bool {
166 self.supplied_paths
167 .contains(&crate::workspace_index::normalize_relative_path(path))
168 }
169
170 pub(crate) fn resolve_supplied(&self, path: &Path) -> Option<PathBuf> {
171 let normalized = crate::workspace_index::normalize_relative_path(path);
172 if self.supplied_paths.contains(&normalized) {
173 return Some(normalized);
174 }
175
176 if path.extension().is_none() {
177 for extension in ["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"] {
178 let candidate = crate::workspace_index::normalize_relative_path(&path.with_extension(extension));
179 if self.supplied_paths.contains(&candidate) {
180 return Some(candidate);
181 }
182 }
183 }
184
185 if self.supplied_dirs.contains(&normalized) {
188 return Some(normalized);
189 }
190
191 None
192 }
193
194 pub fn contains_with_markdown_extension(&self, path: &Path) -> bool {
195 self.resolve_supplied(path).is_some()
196 }
197
198 pub fn allow_disk_fallback(&self) -> bool {
199 self.allow_disk_fallback
200 }
201}
202
203#[cfg(not(target_arch = "wasm32"))]
205macro_rules! profile_section {
206 ($name:expr, $profile:expr, $code:expr) => {{
207 let start = std::time::Instant::now();
208 let result = $code;
209 if $profile {
210 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
211 }
212 result
213 }};
214}
215
216fn build_commonmark_ordered_lists(
217 lines: &[LineInfo],
218 line_to_list: &crate::utils::code_block_utils::LineToListMap,
219 list_start_values: &crate::utils::code_block_utils::ListStartValues,
220) -> Vec<CommonMarkOrderedListInfo> {
221 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
222
223 for (&line_num, &list_id) in line_to_list {
224 let is_ordered_item = line_num
225 .checked_sub(1)
226 .and_then(|index| lines.get(index))
227 .and_then(|line| line.list_item.as_deref())
228 .is_some_and(|item| item.is_ordered);
229 if is_ordered_item {
230 grouped_lines.entry(list_id).or_default().push(line_num);
231 }
232 }
233
234 let mut lists: Vec<_> = grouped_lines
235 .into_iter()
236 .map(|(list_id, mut item_lines)| {
237 item_lines.sort_unstable();
238 CommonMarkOrderedListInfo {
239 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
240 item_lines,
241 }
242 })
243 .collect();
244 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
245 lists
246}
247
248#[cfg(target_arch = "wasm32")]
249macro_rules! profile_section {
250 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
251}
252
253pub(super) struct SkipByteRanges<'a> {
256 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
257 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
258 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
259 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
260}
261
262use std::sync::{Arc, OnceLock};
263
264pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
266
267pub(super) type ByteRanges = Vec<(usize, usize)>;
269
270pub struct LintContext<'a> {
271 pub content: &'a str,
272 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
274 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, colon_definition_lines: Vec<(usize, usize)>, commonmark_ordered_lists_cache: OnceLock<Vec<CommonMarkOrderedListInfo>>, pub lines: Vec<LineInfo>, blockquote_headings: Vec<Option<Box<HeadingInfo>>>, links: Vec<ParsedLink<'a>>, parsed_link_ranges: Vec<(usize, usize)>, 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>,
294 math_byte_ranges_cache: OnceLock<Vec<(usize, usize)>>, code_spans_without_math_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>, html_block_lines: Vec<std::ops::Range<usize>>, pub table_blocks: Vec<crate::utils::table_utils::TableBlock>, line_index: crate::utils::range_utils::LineIndex<'a>, jinja_ranges: Vec<(usize, usize)>, jinja_string_ranges_cache: OnceLock<Vec<(usize, usize)>>,
309 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_esm_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, }
344
345pub struct CodeRanges {
347 pub blocks: Vec<(usize, usize)>,
349 pub spans: Vec<(usize, usize)>,
351}
352
353pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
364 let ctx = LintContext::new(content, flavor, None);
365 CodeRanges {
366 spans: code_span_byte_ranges(&ctx.code_spans()),
367 blocks: ctx.code_blocks,
368 }
369}
370
371pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
373 code_spans
374 .iter()
375 .map(|span| (span.byte_offset, span.byte_end))
376 .collect()
377}
378
379impl<'a> LintContext<'a> {
380 pub fn source_file(&self) -> Option<&Path> {
385 self.source_file.as_deref()
386 }
387
388 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
389 self.link_target_policy.as_ref()
390 }
391
392 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
393 self.link_target_policy = Some(policy);
394 self
395 }
396
397 pub fn invalid_utf8(&self) -> Option<&'a [crate::encoding::InvalidSeq]> {
399 self.invalid_utf8
400 }
401
402 pub fn with_invalid_utf8(mut self, invalid: &'a [crate::encoding::InvalidSeq]) -> Self {
403 self.invalid_utf8 = Some(invalid);
404 self
405 }
406
407 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
408 #[cfg(not(target_arch = "wasm32"))]
409 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
410
411 let line_offsets = profile_section!("Line offsets", profile, {
412 let mut offsets = vec![0];
413 for (i, c) in content.char_indices() {
414 if c == '\n' {
415 offsets.push(i + 1);
416 }
417 }
418 offsets
419 });
420
421 let content_lines: Vec<&str> = content.lines().collect();
423
424 #[allow(clippy::disallowed_methods)]
428 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
429
430 let parse_result = profile_section!(
432 "Code blocks",
433 profile,
434 CodeBlockUtils::detect_code_blocks_and_spans(content)
435 );
436 let mut code_blocks = parse_result.code_blocks;
437 let mut code_span_ranges = parse_result.code_spans;
438 let code_block_details = parse_result.code_block_details;
439 let strong_spans = parse_result.strong_spans;
440 let line_to_list = parse_result.line_to_list;
441 let list_start_values = parse_result.list_start_values;
442 let html_blocks = parse_result.html_blocks;
443 let definition_lists = DefinitionListLines::new(
444 &line_offsets,
445 &parse_result.definition_items,
446 &parse_result.definition_terms,
447 &parse_result.definition_texts,
448 );
449
450 let mut in_fenced_code = vec![false; content_lines.len()];
455 for detail in code_block_details.iter().filter(|detail| detail.is_fenced) {
456 let start_line = line_offsets
457 .partition_point(|&offset| offset <= detail.start)
458 .saturating_sub(1);
459 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
460 for flag in in_fenced_code.iter_mut().take(end_line).skip(start_line) {
461 *flag = true;
462 }
463 }
464 let containers = profile_section!(
465 "Container lines",
466 profile,
467 flavor_detection::detect_container_lines(&content_lines, &in_fenced_code, flavor)
468 );
469
470 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
479 .iter()
480 .flat_map(|detail| {
481 if detail.is_fenced {
482 return vec![(detail.start, detail.end)];
483 }
484 let start_line = line_offsets
485 .partition_point(|&offset| offset <= detail.start)
486 .saturating_sub(1);
487 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
488 containers
489 .code_line_spans_in(start_line..end_line)
490 .into_iter()
491 .map(|span| {
492 let start = line_offsets[span.start].max(detail.start);
493 let end = line_offsets
494 .get(span.end)
495 .copied()
496 .unwrap_or(content.len())
497 .min(detail.end);
498 (start, end)
499 })
500 .collect()
501 })
502 .collect();
503 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
509 let html_comment_scan = profile_section!(
510 "HTML comment ranges",
511 profile,
512 crate::utils::skip_context::scan_html_comments(
513 content,
514 &code_span_ranges,
515 &comment_code_block_ranges,
516 body_start
517 )
518 );
519 let mut html_comment_ranges = html_comment_scan.ranges;
520 let unterminated_html_comment = html_comment_scan.unterminated;
521
522 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
526 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
527 Vec::new()
528 } else {
529 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content, flavor)
530 }
531 });
532
533 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
535 if flavor.is_pandoc_compatible() {
536 crate::utils::pandoc::detect_div_block_ranges(content)
537 } else {
538 Vec::new()
539 }
540 });
541
542 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
544 if flavor == MarkdownFlavor::MkDocs {
545 crate::utils::pymdown_blocks::detect_block_ranges(content)
546 } else {
547 Vec::new()
548 }
549 });
550
551 let skip_ranges = SkipByteRanges {
554 html_comment_ranges: &html_comment_ranges,
555 autodoc_ranges: &autodoc_ranges,
556 pandoc_div_ranges: &pandoc_div_ranges,
557 pymdown_block_ranges: &pymdown_block_ranges,
558 };
559 let (mut lines, emphasis_spans) = profile_section!(
560 "Basic line info",
561 profile,
562 line_computation::compute_basic_line_info(
563 content,
564 &content_lines,
565 &line_offsets,
566 &code_blocks,
567 flavor,
568 &skip_ranges,
569 front_matter_end,
570 )
571 );
572
573 profile_section!(
575 "HTML blocks",
576 profile,
577 heading_detection::detect_html_blocks(content, &mut lines)
578 );
579
580 profile_section!(
582 "ESM blocks",
583 profile,
584 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
585 );
586
587 profile_section!(
589 "JSX block detection",
590 profile,
591 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
592 );
593
594 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
596 "JSX/MDX detection",
597 profile,
598 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
599 );
600
601 profile_section!(
606 "Markdown-in-HTML blocks",
607 profile,
608 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
609 );
610
611 let mdx_context = if flavor == MarkdownFlavor::MDX {
612 mdx::MdxContext::parse(content, &lines)
613 } else {
614 None
615 };
616 let mdx_esm_ranges = mdx_context.as_ref().map_or_else(Vec::new, |mdx| mdx.esm.clone());
617 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
618 mdx.apply_lines(&mut lines);
619 code_blocks.clone_from(&mdx.code_blocks);
620 code_span_ranges.clone_from(&mdx.code_spans);
621 (mdx.expressions.clone(), mdx.comments.clone())
622 } else {
623 (jsx_expression_ranges, mdx_comment_ranges)
624 };
625
626 if !flavor.is_pandoc_compatible() {
634 let mut spans = code_span_ranges.iter().peekable();
637 for (line, &offset) in lines.iter_mut().zip(&line_offsets) {
638 if line.is_div_marker {
639 let end = offset + content[offset..].find('\n').unwrap_or(content.len() - offset);
640 while spans.peek().is_some_and(|&&(_, stop)| stop <= end) {
641 spans.next();
642 }
643 line.is_div_marker = spans.peek().is_none_or(|&&(start, _)| start >= end);
644 }
645 }
646 }
647
648 let mut colon_definition_lines: Vec<(usize, usize)> = Vec::new();
653 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
654 for detail in &parse_result.colon_definitions {
655 let definition_line = line_of(detail.definition_start);
656 if flavor.is_pandoc_compatible() && lines[definition_line - 1].is_div_marker {
657 continue;
658 }
659 let (start_line, end_line) = (
660 line_of(detail.start),
661 line_of(detail.end.saturating_sub(1).max(detail.start)),
662 );
663 match colon_definition_lines.last_mut() {
664 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
665 _ => colon_definition_lines.push((start_line, end_line)),
666 }
667 }
668
669 profile_section!(
671 "MkDocs constructs",
672 profile,
673 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
674 );
675
676 profile_section!(
681 "Footnote definitions",
682 profile,
683 detect_footnote_definitions(content, &mut lines, &line_offsets)
684 );
685
686 {
689 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
690 for &(start, end) in &code_blocks {
691 let start_line = line_offsets
692 .partition_point(|&offset| offset <= start)
693 .saturating_sub(1);
694 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
695
696 let mut sub_start: Option<usize> = None;
697 for (i, &offset) in line_offsets[start_line..end_line]
698 .iter()
699 .enumerate()
700 .map(|(j, o)| (j + start_line, o))
701 {
702 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
703 if is_real_code && sub_start.is_none() {
704 let byte_start = if i == start_line { start } else { offset };
705 sub_start = Some(byte_start);
706 } else if !is_real_code && sub_start.is_some() {
707 new_code_blocks.push((sub_start.unwrap(), offset));
708 sub_start = None;
709 }
710 }
711 if let Some(s) = sub_start {
712 new_code_blocks.push((s, end));
713 }
714 }
715 code_blocks = new_code_blocks;
716 }
717
718 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
726 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
727 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
728 for &(start, end) in &code_blocks {
729 let start_line = line_offsets
730 .partition_point(|&offset| offset <= start)
731 .saturating_sub(1);
732 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
733
734 let mut sub_start: Option<usize> = None;
736 for (i, &offset) in line_offsets[start_line..end_line]
737 .iter()
738 .enumerate()
739 .map(|(j, o)| (j + start_line, o))
740 {
741 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
742 if is_real_code && sub_start.is_none() {
743 let byte_start = if i == start_line { start } else { offset };
744 sub_start = Some(byte_start);
745 } else if !is_real_code && sub_start.is_some() {
746 new_code_blocks.push((sub_start.unwrap(), offset));
747 sub_start = None;
748 }
749 }
750 if let Some(s) = sub_start {
751 new_code_blocks.push((s, end));
752 }
753 }
754 code_blocks = new_code_blocks;
755 }
756
757 if flavor.supports_jsx() {
761 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
762 for &(start, end) in &code_blocks {
763 let start_line = line_offsets
764 .partition_point(|&offset| offset <= start)
765 .saturating_sub(1);
766 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
767
768 let mut sub_start: Option<usize> = None;
769 for (i, &offset) in line_offsets[start_line..end_line]
770 .iter()
771 .enumerate()
772 .map(|(j, o)| (j + start_line, o))
773 {
774 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
775 if is_real_code && sub_start.is_none() {
776 let byte_start = if i == start_line { start } else { offset };
777 sub_start = Some(byte_start);
778 } else if !is_real_code && sub_start.is_some() {
779 new_code_blocks.push((sub_start.unwrap(), offset));
780 sub_start = None;
781 }
782 }
783 if let Some(s) = sub_start {
784 new_code_blocks.push((s, end));
785 }
786 }
787 code_blocks = new_code_blocks;
788
789 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
796 let mut run: Option<(usize, usize)> = None;
797 for line in &lines {
798 if line.in_jsx_block && line.in_code_block {
799 let line_end = line.byte_offset + line.byte_len;
800 match &mut run {
801 Some((_, end)) => *end = line_end,
802 None => run = Some((line.byte_offset, line_end)),
803 }
804 } else if let Some(r) = run.take() {
805 jsx_fence_ranges.push(r);
806 }
807 }
808 if let Some(r) = run.take() {
809 jsx_fence_ranges.push(r);
810 }
811 if !jsx_fence_ranges.is_empty() {
812 code_blocks.extend(jsx_fence_ranges);
813 code_blocks.sort_by_key(|&(start, _)| start);
814 }
815 }
816
817 let colon_fence_details = profile_section!(
820 "Azure colon fence detection",
821 profile,
822 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
823 );
824 if !colon_fence_details.is_empty() {
825 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
826 code_blocks.sort_by_key(|&(start, _)| start);
827 }
828
829 let myst_directive_ranges = profile_section!(
832 "MyST colon directives",
833 profile,
834 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
835 );
836
837 let myst_comment_ranges = profile_section!(
839 "MyST comments",
840 profile,
841 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
842 );
843
844 profile_section!(
847 "MyST backtick directives",
848 profile,
849 flavor_detection::detect_myst_backtick_directives(
850 content,
851 &mut lines,
852 flavor,
853 &code_block_details,
854 &line_offsets
855 )
856 );
857
858 if flavor.supports_myst_directives() {
861 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
862 for &(start, end) in &code_blocks {
863 let start_line = line_offsets
864 .partition_point(|&offset| offset <= start)
865 .saturating_sub(1);
866 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
867
868 let mut sub_start: Option<usize> = None;
869 for (i, &offset) in line_offsets[start_line..end_line]
870 .iter()
871 .enumerate()
872 .map(|(j, o)| (j + start_line, o))
873 {
874 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
875 if is_real_code && sub_start.is_none() {
876 let byte_start = if i == start_line { start } else { offset };
877 sub_start = Some(byte_start);
878 } else if !is_real_code && sub_start.is_some() {
879 new_code_blocks.push((sub_start.unwrap(), offset));
880 sub_start = None;
881 }
882 }
883 if let Some(s) = sub_start {
884 new_code_blocks.push((s, end));
885 }
886 }
887 code_blocks = new_code_blocks;
888 }
889
890 profile_section!(
892 "Kramdown constructs",
893 profile,
894 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
895 );
896
897 for line in &mut lines {
902 if line.in_kramdown_extension_block {
903 line.list_item = None;
904 line.is_horizontal_rule = false;
905 line.blockquote = None;
906 line.is_kramdown_block_ial = false;
907 }
908 }
909
910 let obsidian_comment_scan = profile_section!(
912 "Obsidian comments",
913 profile,
914 flavor_detection::detect_obsidian_comments(
915 content,
916 &mut lines,
917 flavor,
918 &code_span_ranges,
919 &html_comment_ranges,
920 body_start
921 )
922 );
923 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
924 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
925
926 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
931 unterminated_html_comment,
932 &obsidian_comment_ranges,
933 content,
934 &code_span_ranges,
935 &comment_code_block_ranges,
936 body_start,
937 );
938
939 if let Some(range) = unterminated_html_comment.and_then(|opener| {
952 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
953 .or_else(|| container_comment_range(opener, &containers, &lines, content))
954 }) {
955 html_comment_ranges.push(range);
958
959 for line in &mut lines {
965 let text = line.content(content);
966 let content_start = line.byte_offset + line.indent;
967 let content_end = line.byte_offset + text.trim_end().len();
968 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
969 &html_comment_ranges,
970 content_start,
971 content_end,
972 );
973 line.in_obsidian_comment = false;
974 }
975
976 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
987 content,
988 &mut lines,
989 flavor,
990 &code_span_ranges,
991 &html_comment_ranges,
992 body_start,
993 );
994 obsidian_comment_ranges = obsidian_rescan.ranges;
995 unterminated_obsidian_comment = obsidian_rescan.unterminated;
996 }
997
998 let myst_role_ranges = profile_section!(
1000 "MyST roles",
1001 profile,
1002 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
1003 );
1004
1005 let mut pulldown_result = profile_section!(
1009 "Links, images & link ranges",
1010 profile,
1011 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
1012 );
1013
1014 if let Some(mdx) = &mdx_context {
1015 let (links, images) = mdx.links_and_images(content, &lines);
1016 pulldown_result.link_byte_ranges = links.iter().map(|link| (link.byte_offset, link.byte_end)).collect();
1017 pulldown_result.link_found_positions = links.iter().map(|link| link.byte_offset).collect();
1018 pulldown_result.image_found_positions = images.iter().map(|image| image.byte_offset).collect();
1019 pulldown_result.links = links;
1020 pulldown_result.images = images;
1021 pulldown_result.footnote_refs = mdx.footnote_refs();
1022 pulldown_result
1023 .broken_links
1024 .retain(|link| mdx.contains_text(link.span.start, link.span.end));
1025 }
1026
1027 heading_detection::mark_parser_html_blocks(content, &mut lines, &html_blocks, flavor);
1028 let html_block_lines = heading_detection::parser_html_block_lines(&lines, &html_blocks, flavor);
1029
1030 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
1032 let mut blockquote_headings = profile_section!(
1033 "Headings & blockquotes",
1034 profile,
1035 heading_detection::detect_headings_and_blockquotes(
1036 &content_lines,
1037 &mut lines,
1038 flavor,
1039 &html_comment_ranges,
1040 &html_blocks,
1041 &code_blocks,
1042 &code_span_ranges,
1043 &pulldown_result.link_byte_ranges,
1044 front_matter_end,
1045 mdx_flow_lines.as_deref(),
1046 mdx_context
1047 .as_ref()
1048 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1049 )
1050 );
1051
1052 for line in &mut lines {
1054 if line.in_kramdown_extension_block {
1055 line.heading = None;
1056 }
1057 }
1058 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
1059 if line.in_kramdown_extension_block {
1060 *heading = None;
1061 }
1062 }
1063
1064 for line in &mut lines {
1075 if line.is_horizontal_rule
1076 && (line.in_code_block
1077 || line.in_html_block
1078 || line.in_html_comment
1079 || line.in_math_block
1080 || line.in_mdx_comment
1081 || line.in_obsidian_comment)
1082 {
1083 line.is_horizontal_rule = false;
1084 }
1085 }
1086
1087 let mut code_spans = profile_section!(
1089 "Code spans",
1090 profile,
1091 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
1092 );
1093
1094 if flavor == MarkdownFlavor::MkDocs {
1098 let extra = profile_section!(
1099 "MkDocs code spans",
1100 profile,
1101 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
1102 );
1103 if !extra.is_empty() {
1104 code_spans.extend(extra);
1105 code_spans.sort_by_key(|span| span.byte_offset);
1106 }
1107 }
1108
1109 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
1114 let extra = profile_section!(
1115 "MDX JSX code spans",
1116 profile,
1117 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
1118 );
1119 if !extra.is_empty() {
1120 code_spans.extend(extra);
1121 code_spans.sort_by_key(|span| span.byte_offset);
1122 }
1123 }
1124
1125 for span in &code_spans {
1128 if span.end_line > span.line {
1129 for line_num in (span.line + 1)..=span.end_line {
1131 if let Some(line_info) = lines.get_mut(line_num - 1) {
1132 line_info.in_code_span_continuation = true;
1133 }
1134 }
1135 }
1136 }
1137
1138 let mut parsed_link_ranges = std::mem::take(&mut pulldown_result.link_byte_ranges);
1141 parsed_link_ranges.sort_unstable();
1142
1143 let (links, images, broken_links, footnote_refs) = profile_section!(
1145 "Links & images finalize",
1146 profile,
1147 link_parser::finalize_links_and_images(
1148 content,
1149 &lines,
1150 flavor,
1151 &link_parser::LinkExclusions {
1152 code_blocks: &code_blocks,
1153 code_spans: &code_spans,
1154 html_comment_ranges: &html_comment_ranges,
1155 mdx: mdx_context.as_ref(),
1156 },
1157 pulldown_result,
1158 )
1159 );
1160
1161 let reference_defs = profile_section!("Reference defs", profile, {
1162 if let Some(mdx) = &mdx_context {
1163 mdx.reference_defs(content)
1164 } else {
1165 link_parser::parse_reference_defs(content, &lines)
1166 }
1167 });
1168
1169 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1170
1171 let char_frequency = profile_section!(
1173 "Char frequency",
1174 profile,
1175 line_computation::compute_char_frequency(content)
1176 );
1177
1178 let table_blocks = profile_section!(
1180 "Table blocks",
1181 profile,
1182 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
1183 content,
1184 &code_blocks,
1185 &code_spans,
1186 &html_comment_ranges,
1187 flavor,
1188 )
1189 );
1190
1191 let links = links
1194 .into_iter()
1195 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1196 .collect::<Vec<_>>();
1197 let images = images
1198 .into_iter()
1199 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1200 .collect::<Vec<_>>();
1201 let broken_links = broken_links
1202 .into_iter()
1203 .filter(|bl| {
1204 let line_idx = line_offsets
1206 .partition_point(|&offset| offset <= bl.span.start)
1207 .saturating_sub(1);
1208 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1209 })
1210 .collect::<Vec<_>>();
1211 let footnote_refs = footnote_refs
1212 .into_iter()
1213 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1214 .collect::<Vec<_>>();
1215 let reference_defs = reference_defs
1216 .into_iter()
1217 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1218 .collect::<Vec<_>>();
1219 let list_blocks = list_blocks
1220 .into_iter()
1221 .filter(|block| {
1222 !lines
1223 .get(block.start_line - 1)
1224 .is_some_and(|l| l.in_kramdown_extension_block)
1225 })
1226 .collect::<Vec<_>>();
1227 let table_blocks = table_blocks
1228 .into_iter()
1229 .filter(|block| {
1230 !lines
1232 .get(block.start_line)
1233 .is_some_and(|l| l.in_kramdown_extension_block)
1234 })
1235 .collect::<Vec<_>>();
1236 let emphasis_spans = emphasis_spans
1237 .into_iter()
1238 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1239 .collect::<Vec<_>>();
1240
1241 for block in &list_blocks {
1245 for line_num in block.start_line..=block.end_line {
1247 if let Some(li) = lines.get_mut(line_num - 1) {
1248 li.in_list_block = true;
1249 }
1250 }
1251 }
1252 for block in &table_blocks {
1253 for idx in block.start_line..=block.end_line {
1255 if let Some(li) = lines.get_mut(idx) {
1256 li.in_table_block = true;
1257 }
1258 }
1259 }
1260
1261 let reference_defs_map: HashMap<String, usize> = reference_defs
1263 .iter()
1264 .enumerate()
1265 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1266 .collect();
1267
1268 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1270 .iter()
1271 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1272 (Some(start), Some(end)) => Some((start, end)),
1273 _ => None,
1274 })
1275 .collect();
1276
1277 let line_index = profile_section!(
1279 "Line index",
1280 profile,
1281 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
1282 content,
1283 line_offsets.clone(),
1284 &code_blocks,
1285 )
1286 );
1287
1288 let jinja_ranges = profile_section!(
1290 "Jinja ranges",
1291 profile,
1292 crate::utils::jinja_utils::find_jinja_ranges(content)
1293 );
1294
1295 let citation_ranges = profile_section!("Citation ranges", profile, {
1297 if flavor.is_pandoc_compatible() {
1298 crate::utils::pandoc::find_citation_ranges(content)
1299 } else {
1300 Vec::new()
1301 }
1302 });
1303
1304 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1306 if flavor.is_pandoc_compatible() {
1307 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1308 } else {
1309 Vec::new()
1310 }
1311 });
1312
1313 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1315 if flavor.is_pandoc_compatible() {
1316 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1317 } else {
1318 std::collections::HashSet::new()
1319 }
1320 });
1321
1322 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1324 if flavor.is_pandoc_compatible() {
1325 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1326 } else {
1327 Vec::new()
1328 }
1329 });
1330
1331 let example_reference_ranges = profile_section!("Example references", profile, {
1333 if flavor.is_pandoc_compatible() {
1334 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1335 } else {
1336 Vec::new()
1337 }
1338 });
1339
1340 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1342 if flavor.is_pandoc_compatible() {
1343 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1344 } else {
1345 Vec::new()
1346 }
1347 });
1348
1349 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1351 if flavor.is_pandoc_compatible() {
1352 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1353 } else {
1354 Vec::new()
1355 }
1356 });
1357
1358 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1360 if flavor.is_pandoc_compatible() {
1361 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1362 } else {
1363 Vec::new()
1364 }
1365 });
1366
1367 let line_block_ranges = profile_section!("Line block ranges", profile, {
1369 if flavor.is_pandoc_compatible() {
1370 crate::utils::pandoc::detect_line_block_ranges(content)
1371 } else {
1372 Vec::new()
1373 }
1374 });
1375
1376 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1378 if flavor.is_pandoc_compatible() {
1379 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1380 } else {
1381 Vec::new()
1382 }
1383 });
1384
1385 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1387 if flavor.is_pandoc_compatible() {
1388 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1389 } else {
1390 Vec::new()
1391 }
1392 });
1393
1394 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1396 if flavor.is_pandoc_compatible() {
1397 crate::utils::pandoc::detect_grid_table_ranges(content)
1398 } else {
1399 Vec::new()
1400 }
1401 });
1402
1403 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1405 if flavor.is_pandoc_compatible() {
1406 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1407 } else {
1408 Vec::new()
1409 }
1410 });
1411
1412 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1414 crate::utils::shortcode_utils::shortcode_ranges(content).collect()
1415 });
1416
1417 let inline_config =
1418 InlineConfig::from_content_with_code_blocks(content, &code_blocks, &code_span_byte_ranges(&code_spans));
1419 let ctx = Self {
1420 content,
1421 content_lines,
1422 line_offsets,
1423 code_blocks,
1424 code_block_details,
1425 strong_spans,
1426 line_to_list,
1427 list_start_values,
1428 definition_lists,
1429 colon_definition_lines,
1430 commonmark_ordered_lists_cache: OnceLock::new(),
1431 lines,
1432 blockquote_headings,
1433 links,
1434 parsed_link_ranges,
1435 images,
1436 broken_links,
1437 footnote_refs,
1438 reference_defs,
1439 reference_defs_map,
1440 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1441 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1443 math_byte_ranges_cache: OnceLock::new(), code_spans_without_math_cache: OnceLock::new(),
1445 list_blocks,
1446 char_frequency,
1447 html_tags_cache: OnceLock::new(),
1448 jsx_component_tags_cache: OnceLock::new(),
1449 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1450 bare_urls_cache: OnceLock::new(),
1451 has_mixed_list_nesting_cache: OnceLock::new(),
1452 html_comment_ranges,
1453 html_block_lines,
1454 table_blocks,
1455 line_index,
1456 jinja_ranges,
1457 jinja_string_ranges_cache: OnceLock::new(),
1458 flavor,
1459 source_file,
1460 link_target_policy: None,
1461 invalid_utf8: None,
1462 jsx_expression_ranges,
1463 mdx_esm_ranges,
1464 mdx_comment_ranges,
1465 citation_ranges,
1466 pandoc_div_ranges,
1467 colon_fence_details,
1468 inline_footnote_ranges,
1469 pandoc_header_slugs,
1470 example_list_marker_ranges,
1471 example_reference_ranges,
1472 sub_super_ranges,
1473 inline_code_attr_ranges,
1474 bracketed_span_ranges,
1475 line_block_ranges,
1476 pipe_table_caption_ranges,
1477 pandoc_metadata_ranges,
1478 grid_table_ranges,
1479 multi_line_table_ranges,
1480 shortcode_ranges,
1481 link_title_ranges,
1482 code_span_byte_ranges: code_span_ranges,
1483 inline_config,
1484 obsidian_comment_ranges,
1485 unterminated_html_comment,
1486 unterminated_obsidian_comment,
1487 lazy_cont_lines_cache: OnceLock::new(),
1488 myst_directive_ranges,
1489 myst_comment_ranges,
1490 myst_role_ranges,
1491 front_matter_end,
1492 };
1493 if let Some(mdx) = mdx_context {
1494 let (html_tags, jsx_component_tags) = mdx.html_tags(content, &ctx.lines);
1495 let _ = ctx.html_tags_cache.set(Arc::new(ctx.filter_kramdown_tags(html_tags)));
1496 let _ = ctx
1497 .jsx_component_tags_cache
1498 .set(Arc::new(ctx.filter_kramdown_tags(jsx_component_tags)));
1499 }
1500 ctx
1501 }
1502
1503 pub fn front_matter_end_line(&self) -> usize {
1508 self.front_matter_end
1509 }
1510
1511 #[inline]
1514 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1515 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1517 idx > 0 && pos < ranges[idx - 1].1
1519 }
1520
1521 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1523 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1524 }
1525
1526 pub fn is_in_code_span_byte_with_or_without_math(&self, pos: usize) -> bool {
1532 let without_math = self.code_spans_without_math_cache.get_or_init(|| {
1533 if self.content.contains('$') {
1534 crate::utils::parser_options::code_span_ranges_without_math(self.content)
1535 } else {
1536 Vec::new()
1537 }
1538 });
1539 self.is_in_code_span_byte(pos) || Self::binary_search_ranges(without_math, pos)
1540 }
1541
1542 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
1547 let line = &self.lines[line_number - 1];
1548 heading_detection::ends_with_hard_break(
1549 line.content(self.content),
1550 line.byte_offset,
1551 &self.code_span_byte_ranges,
1552 )
1553 }
1554
1555 pub fn is_in_link(&self, pos: usize) -> bool {
1557 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
1558 }
1559
1560 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1562 let bare_urls = self.bare_urls();
1563 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1565 idx > 0 && pos < bare_urls[idx - 1].byte_end
1566 }
1567
1568 pub fn inline_config(&self) -> &InlineConfig {
1570 &self.inline_config
1571 }
1572
1573 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1578 &self.colon_fence_details
1579 }
1580
1581 pub fn raw_lines(&self) -> &[&'a str] {
1585 &self.content_lines
1586 }
1587
1588 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1593 self.inline_config.is_rule_disabled(rule_name, line_number)
1594 }
1595
1596 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1598 Arc::clone(
1599 self.code_spans_cache
1600 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1601 )
1602 }
1603
1604 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1608 self.math_byte_ranges_cache
1609 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1610 }
1611
1612 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1614 Arc::clone(
1615 self.math_spans_cache
1616 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1617 )
1618 }
1619
1620 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
1622 self.bracket_math_cache
1623 .get_or_init(|| bracket_math::parse(self.content, &self.lines, &self.code_spans(), &self.list_blocks))
1624 }
1625
1626 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1628 let math_spans = self.math_spans();
1629 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1631 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1632 }
1633
1634 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1636 &self.html_comment_ranges
1637 }
1638
1639 pub fn unterminated_html_comment(&self) -> Option<usize> {
1644 self.unterminated_html_comment
1645 }
1646
1647 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1651 self.unterminated_obsidian_comment
1652 }
1653
1654 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1658 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1659 }
1660
1661 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1666 if self.obsidian_comment_ranges.is_empty() {
1667 return false;
1668 }
1669
1670 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1672 self.is_in_obsidian_comment(byte_pos)
1673 }
1674
1675 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1677 &self.myst_directive_ranges
1678 }
1679
1680 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1682 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1683 }
1684
1685 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1687 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1688 }
1689
1690 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1697 if !self.flavor.supports_myst_directives() {
1698 return false;
1699 }
1700 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1701 info.in_myst_directive
1702 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1703 })
1704 }
1705
1706 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1708 tags.into_iter()
1709 .filter(|tag| {
1710 !self
1711 .lines
1712 .get(tag.line - 1)
1713 .is_some_and(|l| l.in_kramdown_extension_block)
1714 })
1715 .collect()
1716 }
1717
1718 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1725 Arc::clone(self.html_tags_cache.get_or_init(|| {
1726 let (html_tags, jsx_component_tags) =
1727 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1728 let _ = self
1730 .jsx_component_tags_cache
1731 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1732 Arc::new(self.filter_kramdown_tags(html_tags))
1733 }))
1734 }
1735
1736 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1739 if let Some(cached) = self.jsx_component_tags_cache.get() {
1740 return Arc::clone(cached);
1741 }
1742 let _ = self.html_tags();
1744 Arc::clone(
1745 self.jsx_component_tags_cache
1746 .get()
1747 .expect("html_tags() populates jsx_component_tags_cache"),
1748 )
1749 }
1750
1751 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1753 Arc::clone(
1754 self.emphasis_spans_cache
1755 .get()
1756 .expect("emphasis_spans_cache initialized during construction"),
1757 )
1758 }
1759
1760 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1762 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1763 Arc::new(element_parsers::parse_bare_urls(
1764 self.content,
1765 &self.lines,
1766 &self.code_blocks,
1767 ))
1768 }))
1769 }
1770
1771 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1773 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1774 Arc::new(element_parsers::detect_lazy_continuation_lines(
1775 self.content,
1776 &self.lines,
1777 &self.line_offsets,
1778 ))
1779 }))
1780 }
1781
1782 pub fn has_mixed_list_nesting(&self) -> bool {
1786 *self
1787 .has_mixed_list_nesting_cache
1788 .get_or_init(|| self.compute_mixed_list_nesting())
1789 }
1790
1791 fn compute_mixed_list_nesting(&self) -> bool {
1793 let mut stack: Vec<(usize, bool)> = Vec::new();
1798 let mut last_was_blank = false;
1799
1800 for line_info in &self.lines {
1801 if line_info.in_code_block
1803 || line_info.in_front_matter
1804 || line_info.in_mkdocstrings
1805 || line_info.in_html_comment
1806 || line_info.in_mdx_comment
1807 || line_info.in_esm_block
1808 {
1809 continue;
1810 }
1811
1812 if line_info.is_blank {
1814 last_was_blank = true;
1815 continue;
1816 }
1817
1818 if let Some(list_item) = &line_info.list_item {
1819 let current_pos = if list_item.marker_column == 1 {
1821 0
1822 } else {
1823 list_item.marker_column
1824 };
1825
1826 if last_was_blank && current_pos == 0 {
1828 stack.clear();
1829 }
1830 last_was_blank = false;
1831
1832 while let Some(&(pos, _)) = stack.last() {
1834 if pos >= current_pos {
1835 stack.pop();
1836 } else {
1837 break;
1838 }
1839 }
1840
1841 if let Some(&(_, parent_is_ordered)) = stack.last()
1843 && parent_is_ordered != list_item.is_ordered
1844 {
1845 return true; }
1847
1848 stack.push((current_pos, list_item.is_ordered));
1849 } else {
1850 last_was_blank = false;
1852 }
1853 }
1854
1855 false
1856 }
1857
1858 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1864 match self.line_offsets.binary_search(&offset) {
1865 Ok(line) => (line + 1, 1),
1866 Err(line) => {
1867 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1868 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1870 (line, col)
1871 }
1872 }
1873 }
1874
1875 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
1881 self.line_index.get_line_start_byte(line_number)
1882 }
1883
1884 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
1890 self.line_index.line_col_to_byte_range(line_number, column)
1891 }
1892
1893 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
1898 self.line_index
1899 .line_col_to_byte_range_with_length(line_number, column, length)
1900 }
1901
1902 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
1905 self.line_index.whole_line_range(line_number)
1906 }
1907
1908 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
1913 self.line_index.line_text_range(line_number, start_column, end_column)
1914 }
1915
1916 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
1919 self.line_index.line_content_range(line_number)
1920 }
1921
1922 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
1924 self.line_index.multi_line_range(start_line, end_line)
1925 }
1926
1927 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1929 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1931 return true;
1932 }
1933
1934 self.is_byte_offset_in_code_span(pos)
1936 }
1937
1938 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1940 if line_num > 0 {
1941 self.lines.get(line_num - 1)
1942 } else {
1943 None
1944 }
1945 }
1946
1947 pub fn links(&self) -> &[ParsedLink<'a>] {
1949 &self.links
1950 }
1951
1952 #[inline]
1955 pub(crate) fn is_in_parsed_link(&self, pos: usize) -> bool {
1956 Self::binary_search_ranges(&self.parsed_link_ranges, pos)
1957 }
1958
1959 pub(crate) fn has_parsed_link_start_in(&self, start: usize, end: usize) -> bool {
1962 let index = self.parsed_link_ranges.partition_point(|&(open, _)| open < start);
1963 self.parsed_link_ranges.get(index).is_some_and(|&(open, _)| open < end)
1964 }
1965
1966 pub fn images(&self) -> &[ParsedImage<'a>] {
1968 &self.images
1969 }
1970
1971 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
1973 &self.broken_links
1974 }
1975
1976 pub fn footnote_references(&self) -> &[FootnoteRef] {
1978 &self.footnote_refs
1979 }
1980
1981 pub fn reference_definitions(&self) -> &[ReferenceDef] {
1983 &self.reference_defs
1984 }
1985
1986 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
1988 let start = self.links.partition_point(|link| link.line < line_number);
1989 let end = self.links.partition_point(|link| link.line <= line_number);
1990 &self.links[start..end]
1991 }
1992
1993 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
1995 let start = self.images.partition_point(|image| image.line < line_number);
1996 let end = self.images.partition_point(|image| image.line <= line_number);
1997 &self.images[start..end]
1998 }
1999
2000 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2002 self.links
2003 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
2004 .ok()
2005 .map(|index| &self.links[index])
2006 }
2007
2008 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2010 self.images
2011 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
2012 .ok()
2013 .map(|index| &self.images[index])
2014 }
2015
2016 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2018 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2019 self.links
2020 .get(index.checked_sub(1)?)
2021 .filter(|link| byte_offset < link.byte_end)
2022 }
2023
2024 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2026 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
2027 self.images
2028 .get(index.checked_sub(1)?)
2029 .filter(|image| byte_offset < image.byte_end)
2030 }
2031
2032 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
2034 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2035 &self.links[..end]
2036 }
2037
2038 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
2040 let normalized_id = ref_id.to_lowercase();
2041 self.reference_defs_map
2042 .get(&normalized_id)
2043 .map(|&index| &self.reference_defs[index])
2044 }
2045
2046 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
2048 self.reference_definition(ref_id)
2049 .map(|definition| definition.url.as_str())
2050 }
2051
2052 pub fn is_in_list_block(&self, line_num: usize) -> bool {
2054 if line_num == 0 || line_num > self.lines.len() {
2055 return false;
2056 }
2057 self.lines[line_num - 1].in_list_block
2058 }
2059
2060 pub fn is_in_definition_list(&self, line_num: usize) -> bool {
2068 let lists = &self.definition_lists.outer;
2069 let idx = lists.partition_point(|&(start, _)| start <= line_num);
2070 idx > 0 && line_num <= lists[idx - 1].1
2071 }
2072
2073 pub fn is_definition_term(&self, line_num: usize) -> bool {
2075 let terms = &self.definition_lists.terms;
2076 let idx = terms.partition_point(|&(_, end)| end < line_num);
2077 terms.get(idx).is_some_and(|&(start, _)| start <= line_num)
2078 }
2079
2080 pub fn in_colon_definition(&self, line_num: usize) -> bool {
2088 let ranges = &self.colon_definition_lines;
2089 let idx = ranges.partition_point(|&(_, end)| end < line_num);
2090 ranges.get(idx).is_some_and(|&(start, _)| start <= line_num)
2091 }
2092
2093 pub fn definition_text_at(&self, line_num: usize) -> Option<&DefinitionText> {
2095 let texts = &self.definition_lists.texts;
2096 let idx = texts.partition_point(|text| text.end_line < line_num);
2097 texts.get(idx).filter(|text| text.start_line <= line_num)
2098 }
2099
2100 pub fn line_holds_html_block(&self, line_idx: usize) -> bool {
2105 let run = self.html_block_lines.partition_point(|run| run.end <= line_idx);
2106 self.html_block_lines
2107 .get(run)
2108 .is_some_and(|run| run.contains(&line_idx))
2109 }
2110
2111 pub fn is_in_html_block(&self, line_num: usize) -> bool {
2113 if line_num == 0 || line_num > self.lines.len() {
2114 return false;
2115 }
2116 self.lines[line_num - 1].in_html_block
2117 }
2118
2119 pub fn is_in_table_block(&self, line_num: usize) -> bool {
2125 if line_num == 0 || line_num > self.lines.len() {
2126 return false;
2127 }
2128 self.lines[line_num - 1].in_table_block
2129 }
2130
2131 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
2133 if line_num == 0 || line_num > self.lines.len() {
2134 return false;
2135 }
2136
2137 let col_0indexed = if col > 0 { col - 1 } else { 0 };
2141 let code_spans = self.code_spans();
2142 code_spans.iter().any(|span| {
2143 if line_num < span.line || line_num > span.end_line {
2145 return false;
2146 }
2147
2148 if span.line == span.end_line {
2149 col_0indexed >= span.start_col && col_0indexed < span.end_col
2151 } else if line_num == span.line {
2152 col_0indexed >= span.start_col
2154 } else if line_num == span.end_line {
2155 col_0indexed < span.end_col
2157 } else {
2158 true
2160 }
2161 })
2162 }
2163
2164 #[inline]
2166 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
2167 let code_spans = self.code_spans();
2168 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
2169 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
2170 }
2171
2172 #[inline]
2174 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
2175 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
2176 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
2177 }
2178
2179 #[inline]
2181 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
2182 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
2183 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
2184 }
2185
2186 #[inline]
2189 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
2190 let tags = self.html_tags();
2191 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2192 idx > 0 && byte_pos < tags[idx - 1].byte_end
2193 }
2194
2195 #[inline]
2199 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
2200 if !self.flavor.supports_jsx() {
2201 return false;
2202 }
2203 let tags = self.jsx_component_tags();
2204 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2205 idx > 0 && byte_pos < tags[idx - 1].byte_end
2206 }
2207
2208 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
2210 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
2211 }
2212
2213 #[inline]
2216 pub(crate) fn overlaps_template_code(&self, start: usize, end: usize) -> bool {
2217 if start >= end {
2218 return false;
2219 }
2220 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2221 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2222 idx > 0 && ranges[idx - 1].1 > start
2223 })
2224 }
2225
2226 #[inline]
2229 pub(crate) fn is_inside_template_code(&self, pos: usize) -> bool {
2230 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2231 let idx = ranges.partition_point(|&(start, _)| start < pos);
2232 idx > 0 && ranges[idx - 1].1 > pos
2233 })
2234 }
2235
2236 #[inline]
2238 pub(crate) fn is_in_jinja_string(&self, byte_pos: usize) -> bool {
2239 let ranges = self
2240 .jinja_string_ranges_cache
2241 .get_or_init(|| crate::utils::jinja_utils::find_jinja_string_ranges(self.content, &self.jinja_ranges));
2242 Self::binary_search_ranges(ranges, byte_pos)
2243 }
2244
2245 #[inline]
2247 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
2248 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
2249 }
2250
2251 #[inline]
2253 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
2254 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
2255 }
2256
2257 pub(crate) fn mdx_comment_ranges(&self) -> &[(usize, usize)] {
2259 &self.mdx_comment_ranges
2260 }
2261
2262 pub(crate) fn mdx_inline_code_ranges(&self, start: usize, end: usize) -> impl Iterator<Item = (usize, usize)> + '_ {
2264 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2265 .into_iter()
2266 .flat_map(move |ranges| {
2267 let first = ranges.partition_point(|&(_, range_end)| range_end <= start);
2268 ranges[first..]
2269 .iter()
2270 .copied()
2271 .take_while(move |&(range_start, _)| start < end && range_start < end)
2272 })
2273 }
2274
2275 #[inline]
2278 pub(crate) fn overlaps_mdx_inline_code(&self, start: usize, end: usize) -> bool {
2279 if start >= end {
2280 return false;
2281 }
2282 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2283 .into_iter()
2284 .any(|ranges| {
2285 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2286 idx > 0 && ranges[idx - 1].1 > start
2287 })
2288 }
2289
2290 pub(crate) fn is_inside_mdx_code(&self, pos: usize) -> bool {
2293 if self.flavor != MarkdownFlavor::MDX {
2294 return false;
2295 }
2296 if [
2297 &self.mdx_esm_ranges,
2298 &self.jsx_expression_ranges,
2299 &self.mdx_comment_ranges,
2300 ]
2301 .into_iter()
2302 .any(|ranges| {
2303 let idx = ranges.partition_point(|&(start, _)| start < pos);
2304 idx > 0 && ranges[idx - 1].1 > pos
2305 }) {
2306 return true;
2307 }
2308 [self.html_tags(), self.jsx_component_tags()].into_iter().any(|tags| {
2309 let idx = tags.partition_point(|tag| tag.byte_offset < pos);
2310 idx > 0 && tags[idx - 1].byte_end > pos
2311 })
2312 }
2313
2314 pub(crate) fn overlaps_mdx_code(&self, start: usize, end: usize) -> bool {
2316 if start >= end || self.flavor != MarkdownFlavor::MDX {
2317 return false;
2318 }
2319 if [
2320 &self.mdx_esm_ranges,
2321 &self.jsx_expression_ranges,
2322 &self.mdx_comment_ranges,
2323 ]
2324 .into_iter()
2325 .any(|ranges| {
2326 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2327 idx > 0 && ranges[idx - 1].1 > start
2328 }) {
2329 return true;
2330 }
2331 [self.html_tags(), self.jsx_component_tags()].into_iter().any(|tags| {
2332 let idx = tags.partition_point(|tag| tag.byte_offset < end);
2333 idx > 0 && tags[idx - 1].byte_end > start
2334 })
2335 }
2336
2337 #[inline]
2340 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
2341 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
2342 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
2343 }
2344
2345 #[inline]
2347 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2348 &self.citation_ranges
2349 }
2350
2351 #[inline]
2354 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
2355 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
2356 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
2357 }
2358
2359 #[inline]
2362 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
2363 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
2364 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
2365 }
2366
2367 #[inline]
2370 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2371 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2372 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2373 }
2374
2375 #[inline]
2378 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2379 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2380 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2381 }
2382
2383 #[inline]
2386 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2387 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
2388 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
2389 }
2390
2391 #[inline]
2395 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
2396 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
2397 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
2398 }
2399
2400 #[inline]
2403 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
2404 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
2405 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
2406 }
2407
2408 #[inline]
2411 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
2412 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
2413 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
2414 }
2415
2416 #[inline]
2420 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
2421 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
2422 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
2423 }
2424
2425 #[inline]
2428 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
2429 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
2430 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
2431 }
2432
2433 #[inline]
2436 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
2437 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
2438 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
2439 }
2440
2441 #[inline]
2444 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
2445 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
2446 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
2447 }
2448
2449 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
2454 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
2455 self.pandoc_header_slugs.contains(&slug)
2456 }
2457
2458 #[inline]
2464 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
2465 self.pandoc_header_slugs.contains(slug)
2466 }
2467
2468 #[inline]
2470 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
2471 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
2472 }
2473
2474 #[inline]
2476 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
2477 &self.shortcode_ranges
2478 }
2479
2480 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
2482 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
2483 }
2484
2485 pub fn has_char(&self, ch: char) -> bool {
2487 match ch {
2488 '#' => self.char_frequency.hash_count > 0,
2489 '*' => self.char_frequency.asterisk_count > 0,
2490 '_' => self.char_frequency.underscore_count > 0,
2491 '-' => self.char_frequency.hyphen_count > 0,
2492 '+' => self.char_frequency.plus_count > 0,
2493 '>' => self.char_frequency.gt_count > 0,
2494 '|' => self.char_frequency.pipe_count > 0,
2495 '[' => self.char_frequency.bracket_count > 0,
2496 '`' => self.char_frequency.backtick_count > 0,
2497 '<' => self.char_frequency.lt_count > 0,
2498 '!' => self.char_frequency.exclamation_count > 0,
2499 '\n' => self.char_frequency.newline_count > 0,
2500 _ => self.content.contains(ch), }
2502 }
2503
2504 pub fn char_count(&self, ch: char) -> usize {
2506 match ch {
2507 '#' => self.char_frequency.hash_count,
2508 '*' => self.char_frequency.asterisk_count,
2509 '_' => self.char_frequency.underscore_count,
2510 '-' => self.char_frequency.hyphen_count,
2511 '+' => self.char_frequency.plus_count,
2512 '>' => self.char_frequency.gt_count,
2513 '|' => self.char_frequency.pipe_count,
2514 '[' => self.char_frequency.bracket_count,
2515 '`' => self.char_frequency.backtick_count,
2516 '<' => self.char_frequency.lt_count,
2517 '!' => self.char_frequency.exclamation_count,
2518 '\n' => self.char_frequency.newline_count,
2519 _ => self.content.matches(ch).count(), }
2521 }
2522
2523 pub fn likely_has_headings(&self) -> bool {
2526 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.content.contains('=')
2527 }
2528
2529 pub fn likely_has_lists(&self) -> bool {
2533 self.char_frequency.asterisk_count > 0
2534 || self.char_frequency.hyphen_count > 0
2535 || self.char_frequency.plus_count > 0
2536 }
2537
2538 pub fn likely_has_emphasis(&self) -> bool {
2540 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
2541 }
2542
2543 pub fn likely_has_tables(&self) -> bool {
2545 self.char_frequency.pipe_count > 2
2546 }
2547
2548 pub fn likely_has_blockquotes(&self) -> bool {
2550 self.char_frequency.gt_count > 0
2551 }
2552
2553 pub fn likely_has_code(&self) -> bool {
2555 self.char_frequency.backtick_count > 0
2556 }
2557
2558 pub fn likely_has_links_or_images(&self) -> bool {
2560 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
2561 }
2562
2563 pub fn likely_has_html(&self) -> bool {
2565 self.char_frequency.lt_count > 0
2566 }
2567
2568 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
2573 if let Some(line_info) = self.lines.get(line_idx)
2574 && let Some(ref bq) = line_info.blockquote
2575 {
2576 bq.prefix.trim_end().to_string()
2577 } else {
2578 String::new()
2579 }
2580 }
2581
2582 #[inline]
2593 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
2594 let idx = match lines.binary_search_by(|line| {
2596 if byte_offset < line.byte_offset {
2597 std::cmp::Ordering::Greater
2598 } else if byte_offset > line.byte_offset + line.byte_len {
2599 std::cmp::Ordering::Less
2600 } else {
2601 std::cmp::Ordering::Equal
2602 }
2603 }) {
2604 Ok(idx) => idx,
2605 Err(idx) => idx.saturating_sub(1),
2606 };
2607
2608 let line = &lines[idx];
2609 let line_num = idx + 1;
2610 let byte_col = byte_offset.saturating_sub(line.byte_offset);
2611 let col = byte_to_char_count(line.content(content), byte_col) - 1;
2614
2615 (idx, line_num, col)
2616 }
2617
2618 #[inline]
2620 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
2621 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
2623
2624 if idx > 0 {
2626 let span = &code_spans[idx - 1];
2627 if offset >= span.byte_offset && offset < span.byte_end {
2628 return true;
2629 }
2630 }
2631
2632 false
2633 }
2634
2635 #[must_use]
2655 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
2656 ValidHeadingsIter::new(&self.lines)
2657 }
2658
2659 #[must_use]
2663 pub fn has_valid_headings(&self) -> bool {
2664 self.lines.iter().any(|line| line.heading.is_some())
2665 }
2666
2667 #[must_use]
2669 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
2670 ParsedListItemsIter::new(&self.lines)
2671 }
2672
2673 #[must_use]
2675 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
2676 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
2677 Some(ParsedListItem::new(
2678 line_num,
2679 line_info.list_item.as_deref()?,
2680 line_info,
2681 ))
2682 }
2683
2684 #[must_use]
2686 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
2687 ParsedListBlocks::new(&self.list_blocks, &self.lines)
2688 }
2689
2690 #[must_use]
2694 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
2695 list_blocks::item_lines_by_list(self.content, &self.lines, block)
2696 }
2697
2698 #[must_use]
2700 pub fn has_list_items(&self) -> bool {
2701 self.lines.iter().any(|line| line.list_item.is_some())
2702 }
2703
2704 #[must_use]
2706 pub fn has_unordered_list_items(&self) -> bool {
2707 self.lines
2708 .iter()
2709 .any(|line| line.list_item.as_ref().is_some_and(|item| !item.is_ordered))
2710 }
2711
2712 #[must_use]
2714 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
2715 let lists = self
2716 .commonmark_ordered_lists_cache
2717 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
2718 CommonMarkOrderedLists::new(lists, &self.lines)
2719 }
2720
2721 #[must_use]
2729 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
2730 ParsedHeadingsIter::new(&self.lines, &self.blockquote_headings)
2731 }
2732
2733 #[must_use]
2735 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
2736 let idx = line_num.checked_sub(1)?;
2737 let line_info = self.lines.get(idx)?;
2738 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
2739 Some(heading) => (heading, 0),
2740 None => (
2741 self.blockquote_headings.get(idx)?.as_deref()?,
2742 line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level),
2743 ),
2744 };
2745 Some(ParsedHeading {
2746 line_num,
2747 heading,
2748 line_info,
2749 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
2750 blockquote_depth,
2751 })
2752 }
2753}
2754
2755fn container_comment_range(
2767 opener: usize,
2768 containers: &flavor_detection::ContainerLines,
2769 lines: &[types::LineInfo],
2770 content: &str,
2771) -> Option<crate::utils::skip_context::ByteRange> {
2772 let line_index = lines
2773 .partition_point(|line| line.byte_offset <= opener)
2774 .checked_sub(1)?;
2775 let line = lines.get(line_index)?;
2776 if line.byte_offset + line.indent != opener {
2777 return None;
2778 }
2779 if !containers.is_container_body(line_index) {
2780 return None;
2781 }
2782 let end_line = lines.get(containers.body_end_line(line_index)?)?;
2783 Some(crate::utils::skip_context::ByteRange {
2784 start: opener,
2785 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
2786 })
2787}
2788
2789#[derive(Debug)]
2791struct DefinitionListLines {
2792 outer: Vec<(usize, usize)>,
2795 terms: Vec<(usize, usize)>,
2797 texts: Vec<DefinitionText>,
2799}
2800
2801impl DefinitionListLines {
2802 fn new(
2803 line_offsets: &[usize],
2804 items: &[(usize, usize)],
2805 terms: &[(usize, usize)],
2806 texts: &[crate::utils::code_block_utils::DefinitionTextDetail],
2807 ) -> Self {
2808 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
2811 let last_line_of = |start: usize, end: usize| line_of(end.saturating_sub(1).max(start));
2812
2813 let mut outer: Vec<(usize, usize)> = Vec::new();
2816 for &(start, end) in items {
2817 let (start_line, end_line) = (line_of(start), last_line_of(start, end));
2818 match outer.last_mut() {
2819 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
2820 _ => outer.push((start_line, end_line)),
2821 }
2822 }
2823 let terms = terms
2824 .iter()
2825 .map(|&(start, end)| (line_of(start), last_line_of(start, end)))
2826 .collect();
2827
2828 let texts = texts
2829 .iter()
2830 .map(|text| {
2831 let start_line = line_of(text.start);
2832 let line_start = line_offsets[start_line - 1];
2833 let on_marker_line = line_of(text.definition_start) == start_line;
2834 DefinitionText {
2835 start_line,
2836 end_line: last_line_of(text.start, text.end),
2837 marker_prefix_len: on_marker_line.then(|| text.start - line_start),
2838 }
2839 })
2840 .collect();
2841
2842 Self { outer, terms, texts }
2843 }
2844}
2845
2846fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
2855 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
2856
2857 if !content.contains("[^") {
2859 return;
2860 }
2861
2862 let options = crate::utils::rumdl_parser_options();
2863 let parser = Parser::new_ext(content, options).into_offset_iter();
2864
2865 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
2867 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
2868 let mut in_footnote = false;
2869
2870 for (event, range) in parser {
2871 match event {
2872 Event::Start(Tag::FootnoteDefinition(_)) => {
2873 in_footnote = true;
2874 footnote_ranges.push((range.start, range.end));
2875 }
2876 Event::End(TagEnd::FootnoteDefinition) => {
2877 in_footnote = false;
2878 }
2879 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
2880 fenced_code_ranges.push((range.start, range.end));
2881 }
2882 _ => {}
2883 }
2884 }
2885
2886 let byte_to_line = |byte_offset: usize| -> usize {
2887 line_offsets
2888 .partition_point(|&offset| offset <= byte_offset)
2889 .saturating_sub(1)
2890 };
2891
2892 for &(start, end) in &footnote_ranges {
2894 let start_line = byte_to_line(start);
2895 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2896
2897 for line in &mut lines[start_line..end_line] {
2898 line.in_footnote_definition = true;
2899 line.in_code_block = false;
2900 }
2901 }
2902
2903 for &(start, end) in &fenced_code_ranges {
2905 let start_line = byte_to_line(start);
2906 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2907
2908 for line in &mut lines[start_line..end_line] {
2909 line.in_code_block = true;
2910 }
2911 }
2912}