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 supplied_dirs: Arc<std::collections::HashSet<PathBuf>>,
35 allow_disk_fallback: bool,
36}
37
38impl LinkTargetPolicy {
39 pub fn open_world<I, P>(paths: I) -> Self
42 where
43 I: IntoIterator<Item = P>,
44 P: AsRef<Path>,
45 {
46 Self::from_paths(paths, true)
47 }
48
49 pub fn closed_world<I, P>(paths: I) -> Self
51 where
52 I: IntoIterator<Item = P>,
53 P: AsRef<Path>,
54 {
55 Self::from_paths(paths, false)
56 }
57
58 fn from_paths<I, P>(paths: I, allow_disk_fallback: bool) -> Self
59 where
60 I: IntoIterator<Item = P>,
61 P: AsRef<Path>,
62 {
63 let mut roots = Vec::new();
64 if let Ok(cwd) = std::env::current_dir() {
65 if let Ok(canonical_cwd) = cwd.canonicalize()
66 && canonical_cwd != cwd
67 {
68 roots.push(canonical_cwd);
69 }
70 roots.push(cwd);
71 }
72 Self::from_paths_with_roots(paths, allow_disk_fallback, roots)
73 }
74
75 fn from_paths_with_roots<I, P, R, Q>(paths: I, allow_disk_fallback: bool, roots: R) -> Self
76 where
77 I: IntoIterator<Item = P>,
78 P: AsRef<Path>,
79 R: IntoIterator<Item = Q>,
80 Q: AsRef<Path>,
81 {
82 let roots: Vec<PathBuf> = roots
83 .into_iter()
84 .map(|root| crate::workspace_index::normalize_relative_path(root.as_ref()))
85 .collect();
86 let mut supplied_paths = std::collections::HashSet::new();
87 for path in paths {
88 let path = path.as_ref();
89 supplied_paths.insert(crate::workspace_index::normalize_relative_path(path));
90
91 if path.is_relative() {
92 for root in &roots {
93 supplied_paths.insert(crate::workspace_index::normalize_relative_path(&root.join(path)));
94 }
95 } else {
96 let resolved = crate::discovery::resolve_for_matching(path);
102 let relative = [path, resolved.as_path()].into_iter().find_map(|spelling| {
103 roots
104 .iter()
105 .find_map(|source_root| spelling.strip_prefix(source_root).ok())
106 });
107 if let Some(relative) = relative {
108 supplied_paths.insert(crate::workspace_index::normalize_relative_path(relative));
109 for root in &roots {
110 supplied_paths.insert(crate::workspace_index::normalize_relative_path(&root.join(relative)));
111 }
112 }
113 }
114 }
115 let escapes_roots = |path: &Path| {
124 if path.is_absolute() {
125 !roots.iter().any(|root| path.starts_with(root))
126 } else {
127 path.starts_with("..")
128 }
129 };
130 let outside_root = |absolute: bool| {
131 supplied_paths
132 .iter()
133 .filter(|path| path.is_absolute() == absolute && escapes_roots(path))
134 .filter_map(|path| path.parent())
135 .fold(None::<&Path>, |common, dir| {
136 Some(common.map_or(dir, |common| {
137 common
138 .ancestors()
139 .find(|ancestor| dir.starts_with(ancestor))
140 .unwrap_or(common)
141 }))
142 })
143 };
144 let outside_roots = [outside_root(true), outside_root(false)];
145 let supplied_dirs = supplied_paths
146 .iter()
147 .flat_map(|path| path.ancestors().skip(1))
148 .filter(|dir| {
149 !escapes_roots(dir)
150 || outside_roots
151 .iter()
152 .flatten()
153 .any(|outside_root| dir.starts_with(outside_root))
154 })
155 .map(Path::to_path_buf)
156 .collect();
157 Self {
158 supplied_paths: Arc::new(supplied_paths),
159 supplied_dirs: Arc::new(supplied_dirs),
160 allow_disk_fallback,
161 }
162 }
163
164 pub fn contains(&self, path: &Path) -> bool {
165 self.supplied_paths
166 .contains(&crate::workspace_index::normalize_relative_path(path))
167 }
168
169 pub(crate) fn resolve_supplied(&self, path: &Path) -> Option<PathBuf> {
170 let normalized = crate::workspace_index::normalize_relative_path(path);
171 if self.supplied_paths.contains(&normalized) {
172 return Some(normalized);
173 }
174
175 if path.extension().is_none() {
176 for extension in ["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"] {
177 let candidate = crate::workspace_index::normalize_relative_path(&path.with_extension(extension));
178 if self.supplied_paths.contains(&candidate) {
179 return Some(candidate);
180 }
181 }
182 }
183
184 if self.supplied_dirs.contains(&normalized) {
187 return Some(normalized);
188 }
189
190 None
191 }
192
193 pub fn contains_with_markdown_extension(&self, path: &Path) -> bool {
194 self.resolve_supplied(path).is_some()
195 }
196
197 pub fn allow_disk_fallback(&self) -> bool {
198 self.allow_disk_fallback
199 }
200}
201
202#[cfg(not(target_arch = "wasm32"))]
204macro_rules! profile_section {
205 ($name:expr, $profile:expr, $code:expr) => {{
206 let start = std::time::Instant::now();
207 let result = $code;
208 if $profile {
209 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
210 }
211 result
212 }};
213}
214
215fn build_commonmark_ordered_lists(
216 lines: &[LineInfo],
217 line_to_list: &crate::utils::code_block_utils::LineToListMap,
218 list_start_values: &crate::utils::code_block_utils::ListStartValues,
219) -> Vec<CommonMarkOrderedListInfo> {
220 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
221
222 for (&line_num, &list_id) in line_to_list {
223 let is_ordered_item = line_num
224 .checked_sub(1)
225 .and_then(|index| lines.get(index))
226 .and_then(|line| line.list_item.as_deref())
227 .is_some_and(|item| item.is_ordered);
228 if is_ordered_item {
229 grouped_lines.entry(list_id).or_default().push(line_num);
230 }
231 }
232
233 let mut lists: Vec<_> = grouped_lines
234 .into_iter()
235 .map(|(list_id, mut item_lines)| {
236 item_lines.sort_unstable();
237 CommonMarkOrderedListInfo {
238 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
239 item_lines,
240 }
241 })
242 .collect();
243 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
244 lists
245}
246
247#[cfg(target_arch = "wasm32")]
248macro_rules! profile_section {
249 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
250}
251
252pub(super) struct SkipByteRanges<'a> {
255 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
256 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
257 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
258 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
259}
260
261use std::sync::{Arc, OnceLock};
262
263pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
265
266pub(super) type ByteRanges = Vec<(usize, usize)>;
268
269pub struct LintContext<'a> {
270 pub content: &'a str,
271 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
273 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>,
291 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, }
337
338pub struct CodeRanges {
340 pub blocks: Vec<(usize, usize)>,
342 pub spans: Vec<(usize, usize)>,
344}
345
346pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
357 let ctx = LintContext::new(content, flavor, None);
358 CodeRanges {
359 spans: code_span_byte_ranges(&ctx.code_spans()),
360 blocks: ctx.code_blocks,
361 }
362}
363
364pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
366 code_spans
367 .iter()
368 .map(|span| (span.byte_offset, span.byte_end))
369 .collect()
370}
371
372impl<'a> LintContext<'a> {
373 pub fn source_file(&self) -> Option<&Path> {
378 self.source_file.as_deref()
379 }
380
381 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
382 self.link_target_policy.as_ref()
383 }
384
385 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
386 self.link_target_policy = Some(policy);
387 self
388 }
389
390 pub fn invalid_utf8(&self) -> Option<&'a [crate::encoding::InvalidSeq]> {
392 self.invalid_utf8
393 }
394
395 pub fn with_invalid_utf8(mut self, invalid: &'a [crate::encoding::InvalidSeq]) -> Self {
396 self.invalid_utf8 = Some(invalid);
397 self
398 }
399
400 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
401 #[cfg(not(target_arch = "wasm32"))]
402 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
403
404 let line_offsets = profile_section!("Line offsets", profile, {
405 let mut offsets = vec![0];
406 for (i, c) in content.char_indices() {
407 if c == '\n' {
408 offsets.push(i + 1);
409 }
410 }
411 offsets
412 });
413
414 let content_lines: Vec<&str> = content.lines().collect();
416
417 #[allow(clippy::disallowed_methods)]
421 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
422
423 let parse_result = profile_section!(
425 "Code blocks",
426 profile,
427 CodeBlockUtils::detect_code_blocks_and_spans(content)
428 );
429 let mut code_blocks = parse_result.code_blocks;
430 let mut code_span_ranges = parse_result.code_spans;
431 let code_block_details = parse_result.code_block_details;
432 let strong_spans = parse_result.strong_spans;
433 let line_to_list = parse_result.line_to_list;
434 let list_start_values = parse_result.list_start_values;
435 let html_blocks = parse_result.html_blocks;
436 let definition_lists = DefinitionListLines::new(
437 &line_offsets,
438 &parse_result.definition_items,
439 &parse_result.definition_terms,
440 &parse_result.definition_texts,
441 );
442
443 let containers = profile_section!(
446 "Container lines",
447 profile,
448 flavor_detection::detect_container_lines(&content_lines, flavor)
449 );
450
451 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
460 .iter()
461 .flat_map(|detail| {
462 if detail.is_fenced {
463 return vec![(detail.start, detail.end)];
464 }
465 let start_line = line_offsets
466 .partition_point(|&offset| offset <= detail.start)
467 .saturating_sub(1);
468 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
469 containers
470 .code_line_spans_in(start_line..end_line)
471 .into_iter()
472 .map(|span| {
473 let start = line_offsets[span.start].max(detail.start);
474 let end = line_offsets
475 .get(span.end)
476 .copied()
477 .unwrap_or(content.len())
478 .min(detail.end);
479 (start, end)
480 })
481 .collect()
482 })
483 .collect();
484 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
490 let html_comment_scan = profile_section!(
491 "HTML comment ranges",
492 profile,
493 crate::utils::skip_context::scan_html_comments(
494 content,
495 &code_span_ranges,
496 &comment_code_block_ranges,
497 body_start
498 )
499 );
500 let mut html_comment_ranges = html_comment_scan.ranges;
501 let unterminated_html_comment = html_comment_scan.unterminated;
502
503 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
507 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
508 Vec::new()
509 } else {
510 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content, flavor)
511 }
512 });
513
514 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
516 if flavor.is_pandoc_compatible() {
517 crate::utils::pandoc::detect_div_block_ranges(content)
518 } else {
519 Vec::new()
520 }
521 });
522
523 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
525 if flavor == MarkdownFlavor::MkDocs {
526 crate::utils::pymdown_blocks::detect_block_ranges(content)
527 } else {
528 Vec::new()
529 }
530 });
531
532 let skip_ranges = SkipByteRanges {
535 html_comment_ranges: &html_comment_ranges,
536 autodoc_ranges: &autodoc_ranges,
537 pandoc_div_ranges: &pandoc_div_ranges,
538 pymdown_block_ranges: &pymdown_block_ranges,
539 };
540 let (mut lines, emphasis_spans) = profile_section!(
541 "Basic line info",
542 profile,
543 line_computation::compute_basic_line_info(
544 content,
545 &content_lines,
546 &line_offsets,
547 &code_blocks,
548 flavor,
549 &skip_ranges,
550 front_matter_end,
551 )
552 );
553
554 profile_section!(
556 "HTML blocks",
557 profile,
558 heading_detection::detect_html_blocks(content, &mut lines)
559 );
560
561 profile_section!(
563 "ESM blocks",
564 profile,
565 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
566 );
567
568 profile_section!(
570 "JSX block detection",
571 profile,
572 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
573 );
574
575 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
577 "JSX/MDX detection",
578 profile,
579 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
580 );
581
582 profile_section!(
587 "Markdown-in-HTML blocks",
588 profile,
589 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
590 );
591
592 let mdx_context = if flavor == MarkdownFlavor::MDX {
593 mdx::MdxContext::parse(content, &lines)
594 } else {
595 None
596 };
597 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
598 mdx.apply_lines(&mut lines);
599 code_blocks.clone_from(&mdx.code_blocks);
600 code_span_ranges.clone_from(&mdx.code_spans);
601 (mdx.expressions.clone(), mdx.comments.clone())
602 } else {
603 (jsx_expression_ranges, mdx_comment_ranges)
604 };
605
606 profile_section!(
608 "MkDocs constructs",
609 profile,
610 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
611 );
612
613 profile_section!(
618 "Footnote definitions",
619 profile,
620 detect_footnote_definitions(content, &mut lines, &line_offsets)
621 );
622
623 {
626 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
627 for &(start, end) in &code_blocks {
628 let start_line = line_offsets
629 .partition_point(|&offset| offset <= start)
630 .saturating_sub(1);
631 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
632
633 let mut sub_start: Option<usize> = None;
634 for (i, &offset) in line_offsets[start_line..end_line]
635 .iter()
636 .enumerate()
637 .map(|(j, o)| (j + start_line, o))
638 {
639 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
640 if is_real_code && sub_start.is_none() {
641 let byte_start = if i == start_line { start } else { offset };
642 sub_start = Some(byte_start);
643 } else if !is_real_code && sub_start.is_some() {
644 new_code_blocks.push((sub_start.unwrap(), offset));
645 sub_start = None;
646 }
647 }
648 if let Some(s) = sub_start {
649 new_code_blocks.push((s, end));
650 }
651 }
652 code_blocks = new_code_blocks;
653 }
654
655 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
663 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
664 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
665 for &(start, end) in &code_blocks {
666 let start_line = line_offsets
667 .partition_point(|&offset| offset <= start)
668 .saturating_sub(1);
669 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
670
671 let mut sub_start: Option<usize> = None;
673 for (i, &offset) in line_offsets[start_line..end_line]
674 .iter()
675 .enumerate()
676 .map(|(j, o)| (j + start_line, o))
677 {
678 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
679 if is_real_code && sub_start.is_none() {
680 let byte_start = if i == start_line { start } else { offset };
681 sub_start = Some(byte_start);
682 } else if !is_real_code && sub_start.is_some() {
683 new_code_blocks.push((sub_start.unwrap(), offset));
684 sub_start = None;
685 }
686 }
687 if let Some(s) = sub_start {
688 new_code_blocks.push((s, end));
689 }
690 }
691 code_blocks = new_code_blocks;
692 }
693
694 if flavor.supports_jsx() {
698 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
699 for &(start, end) in &code_blocks {
700 let start_line = line_offsets
701 .partition_point(|&offset| offset <= start)
702 .saturating_sub(1);
703 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
704
705 let mut sub_start: Option<usize> = None;
706 for (i, &offset) in line_offsets[start_line..end_line]
707 .iter()
708 .enumerate()
709 .map(|(j, o)| (j + start_line, o))
710 {
711 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
712 if is_real_code && sub_start.is_none() {
713 let byte_start = if i == start_line { start } else { offset };
714 sub_start = Some(byte_start);
715 } else if !is_real_code && sub_start.is_some() {
716 new_code_blocks.push((sub_start.unwrap(), offset));
717 sub_start = None;
718 }
719 }
720 if let Some(s) = sub_start {
721 new_code_blocks.push((s, end));
722 }
723 }
724 code_blocks = new_code_blocks;
725
726 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
733 let mut run: Option<(usize, usize)> = None;
734 for line in &lines {
735 if line.in_jsx_block && line.in_code_block {
736 let line_end = line.byte_offset + line.byte_len;
737 match &mut run {
738 Some((_, end)) => *end = line_end,
739 None => run = Some((line.byte_offset, line_end)),
740 }
741 } else if let Some(r) = run.take() {
742 jsx_fence_ranges.push(r);
743 }
744 }
745 if let Some(r) = run.take() {
746 jsx_fence_ranges.push(r);
747 }
748 if !jsx_fence_ranges.is_empty() {
749 code_blocks.extend(jsx_fence_ranges);
750 code_blocks.sort_by_key(|&(start, _)| start);
751 }
752 }
753
754 let colon_fence_details = profile_section!(
757 "Azure colon fence detection",
758 profile,
759 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
760 );
761 if !colon_fence_details.is_empty() {
762 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
763 code_blocks.sort_by_key(|&(start, _)| start);
764 }
765
766 let myst_directive_ranges = profile_section!(
769 "MyST colon directives",
770 profile,
771 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
772 );
773
774 let myst_comment_ranges = profile_section!(
776 "MyST comments",
777 profile,
778 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
779 );
780
781 profile_section!(
784 "MyST backtick directives",
785 profile,
786 flavor_detection::detect_myst_backtick_directives(
787 content,
788 &mut lines,
789 flavor,
790 &code_block_details,
791 &line_offsets
792 )
793 );
794
795 if flavor.supports_myst_directives() {
798 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
799 for &(start, end) in &code_blocks {
800 let start_line = line_offsets
801 .partition_point(|&offset| offset <= start)
802 .saturating_sub(1);
803 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
804
805 let mut sub_start: Option<usize> = None;
806 for (i, &offset) in line_offsets[start_line..end_line]
807 .iter()
808 .enumerate()
809 .map(|(j, o)| (j + start_line, o))
810 {
811 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
812 if is_real_code && sub_start.is_none() {
813 let byte_start = if i == start_line { start } else { offset };
814 sub_start = Some(byte_start);
815 } else if !is_real_code && sub_start.is_some() {
816 new_code_blocks.push((sub_start.unwrap(), offset));
817 sub_start = None;
818 }
819 }
820 if let Some(s) = sub_start {
821 new_code_blocks.push((s, end));
822 }
823 }
824 code_blocks = new_code_blocks;
825 }
826
827 profile_section!(
829 "Kramdown constructs",
830 profile,
831 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
832 );
833
834 for line in &mut lines {
839 if line.in_kramdown_extension_block {
840 line.list_item = None;
841 line.is_horizontal_rule = false;
842 line.blockquote = None;
843 line.is_kramdown_block_ial = false;
844 }
845 }
846
847 let obsidian_comment_scan = profile_section!(
849 "Obsidian comments",
850 profile,
851 flavor_detection::detect_obsidian_comments(
852 content,
853 &mut lines,
854 flavor,
855 &code_span_ranges,
856 &html_comment_ranges,
857 body_start
858 )
859 );
860 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
861 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
862
863 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
868 unterminated_html_comment,
869 &obsidian_comment_ranges,
870 content,
871 &code_span_ranges,
872 &comment_code_block_ranges,
873 body_start,
874 );
875
876 if let Some(range) = unterminated_html_comment.and_then(|opener| {
889 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
890 .or_else(|| container_comment_range(opener, &containers, &lines, content))
891 }) {
892 html_comment_ranges.push(range);
895
896 for line in &mut lines {
902 let text = line.content(content);
903 let content_start = line.byte_offset + line.indent;
904 let content_end = line.byte_offset + text.trim_end().len();
905 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
906 &html_comment_ranges,
907 content_start,
908 content_end,
909 );
910 line.in_obsidian_comment = false;
911 }
912
913 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
924 content,
925 &mut lines,
926 flavor,
927 &code_span_ranges,
928 &html_comment_ranges,
929 body_start,
930 );
931 obsidian_comment_ranges = obsidian_rescan.ranges;
932 unterminated_obsidian_comment = obsidian_rescan.unterminated;
933 }
934
935 let myst_role_ranges = profile_section!(
937 "MyST roles",
938 profile,
939 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
940 );
941
942 let mut pulldown_result = profile_section!(
946 "Links, images & link ranges",
947 profile,
948 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
949 );
950
951 if let Some(mdx) = &mdx_context {
952 let (links, images) = mdx.links_and_images(content, &lines);
953 pulldown_result.link_byte_ranges = links.iter().map(|link| (link.byte_offset, link.byte_end)).collect();
954 pulldown_result.link_found_positions = links.iter().map(|link| link.byte_offset).collect();
955 pulldown_result.image_found_positions = images.iter().map(|image| image.byte_offset).collect();
956 pulldown_result.links = links;
957 pulldown_result.images = images;
958 pulldown_result.footnote_refs = mdx.footnote_refs();
959 pulldown_result
960 .broken_links
961 .retain(|link| mdx.contains_text(link.span.start, link.span.end));
962 }
963
964 heading_detection::mark_parser_html_blocks(content, &mut lines, &html_blocks, flavor);
965
966 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
968 let mut blockquote_headings = profile_section!(
969 "Headings & blockquotes",
970 profile,
971 heading_detection::detect_headings_and_blockquotes(
972 &content_lines,
973 &mut lines,
974 flavor,
975 &html_comment_ranges,
976 &html_blocks,
977 &code_blocks,
978 &code_span_ranges,
979 &pulldown_result.link_byte_ranges,
980 front_matter_end,
981 mdx_flow_lines.as_deref(),
982 )
983 );
984
985 for line in &mut lines {
987 if line.in_kramdown_extension_block {
988 line.heading = None;
989 }
990 }
991 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
992 if line.in_kramdown_extension_block {
993 *heading = None;
994 }
995 }
996
997 for line in &mut lines {
1008 if line.is_horizontal_rule
1009 && (line.in_code_block
1010 || line.in_html_block
1011 || line.in_html_comment
1012 || line.in_math_block
1013 || line.in_mdx_comment
1014 || line.in_obsidian_comment)
1015 {
1016 line.is_horizontal_rule = false;
1017 }
1018 }
1019
1020 let mut code_spans = profile_section!(
1022 "Code spans",
1023 profile,
1024 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
1025 );
1026
1027 if flavor == MarkdownFlavor::MkDocs {
1031 let extra = profile_section!(
1032 "MkDocs code spans",
1033 profile,
1034 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
1035 );
1036 if !extra.is_empty() {
1037 code_spans.extend(extra);
1038 code_spans.sort_by_key(|span| span.byte_offset);
1039 }
1040 }
1041
1042 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
1047 let extra = profile_section!(
1048 "MDX JSX code spans",
1049 profile,
1050 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
1051 );
1052 if !extra.is_empty() {
1053 code_spans.extend(extra);
1054 code_spans.sort_by_key(|span| span.byte_offset);
1055 }
1056 }
1057
1058 for span in &code_spans {
1061 if span.end_line > span.line {
1062 for line_num in (span.line + 1)..=span.end_line {
1064 if let Some(line_info) = lines.get_mut(line_num - 1) {
1065 line_info.in_code_span_continuation = true;
1066 }
1067 }
1068 }
1069 }
1070
1071 let (links, images, broken_links, footnote_refs) = profile_section!(
1073 "Links & images finalize",
1074 profile,
1075 link_parser::finalize_links_and_images(
1076 content,
1077 &lines,
1078 flavor,
1079 &link_parser::LinkExclusions {
1080 code_blocks: &code_blocks,
1081 code_spans: &code_spans,
1082 html_comment_ranges: &html_comment_ranges,
1083 mdx: mdx_context.as_ref(),
1084 },
1085 pulldown_result,
1086 )
1087 );
1088
1089 let reference_defs = profile_section!("Reference defs", profile, {
1090 if let Some(mdx) = &mdx_context {
1091 mdx.reference_defs(content)
1092 } else {
1093 link_parser::parse_reference_defs(content, &lines)
1094 }
1095 });
1096
1097 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1098
1099 let char_frequency = profile_section!(
1101 "Char frequency",
1102 profile,
1103 line_computation::compute_char_frequency(content)
1104 );
1105
1106 let table_blocks = profile_section!(
1108 "Table blocks",
1109 profile,
1110 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
1111 content,
1112 &code_blocks,
1113 &code_spans,
1114 &html_comment_ranges,
1115 flavor,
1116 )
1117 );
1118
1119 let links = links
1122 .into_iter()
1123 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1124 .collect::<Vec<_>>();
1125 let images = images
1126 .into_iter()
1127 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1128 .collect::<Vec<_>>();
1129 let broken_links = broken_links
1130 .into_iter()
1131 .filter(|bl| {
1132 let line_idx = line_offsets
1134 .partition_point(|&offset| offset <= bl.span.start)
1135 .saturating_sub(1);
1136 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1137 })
1138 .collect::<Vec<_>>();
1139 let footnote_refs = footnote_refs
1140 .into_iter()
1141 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1142 .collect::<Vec<_>>();
1143 let reference_defs = reference_defs
1144 .into_iter()
1145 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1146 .collect::<Vec<_>>();
1147 let list_blocks = list_blocks
1148 .into_iter()
1149 .filter(|block| {
1150 !lines
1151 .get(block.start_line - 1)
1152 .is_some_and(|l| l.in_kramdown_extension_block)
1153 })
1154 .collect::<Vec<_>>();
1155 let table_blocks = table_blocks
1156 .into_iter()
1157 .filter(|block| {
1158 !lines
1160 .get(block.start_line)
1161 .is_some_and(|l| l.in_kramdown_extension_block)
1162 })
1163 .collect::<Vec<_>>();
1164 let emphasis_spans = emphasis_spans
1165 .into_iter()
1166 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1167 .collect::<Vec<_>>();
1168
1169 for block in &list_blocks {
1173 for line_num in block.start_line..=block.end_line {
1175 if let Some(li) = lines.get_mut(line_num - 1) {
1176 li.in_list_block = true;
1177 }
1178 }
1179 }
1180 for block in &table_blocks {
1181 for idx in block.start_line..=block.end_line {
1183 if let Some(li) = lines.get_mut(idx) {
1184 li.in_table_block = true;
1185 }
1186 }
1187 }
1188
1189 let reference_defs_map: HashMap<String, usize> = reference_defs
1191 .iter()
1192 .enumerate()
1193 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1194 .collect();
1195
1196 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1198 .iter()
1199 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1200 (Some(start), Some(end)) => Some((start, end)),
1201 _ => None,
1202 })
1203 .collect();
1204
1205 let line_index = profile_section!(
1207 "Line index",
1208 profile,
1209 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
1210 content,
1211 line_offsets.clone(),
1212 &code_blocks,
1213 )
1214 );
1215
1216 let jinja_ranges = profile_section!(
1218 "Jinja ranges",
1219 profile,
1220 crate::utils::jinja_utils::find_jinja_ranges(content)
1221 );
1222
1223 let citation_ranges = profile_section!("Citation ranges", profile, {
1225 if flavor.is_pandoc_compatible() {
1226 crate::utils::pandoc::find_citation_ranges(content)
1227 } else {
1228 Vec::new()
1229 }
1230 });
1231
1232 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1234 if flavor.is_pandoc_compatible() {
1235 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1236 } else {
1237 Vec::new()
1238 }
1239 });
1240
1241 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1243 if flavor.is_pandoc_compatible() {
1244 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1245 } else {
1246 std::collections::HashSet::new()
1247 }
1248 });
1249
1250 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1252 if flavor.is_pandoc_compatible() {
1253 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1254 } else {
1255 Vec::new()
1256 }
1257 });
1258
1259 let example_reference_ranges = profile_section!("Example references", profile, {
1261 if flavor.is_pandoc_compatible() {
1262 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1263 } else {
1264 Vec::new()
1265 }
1266 });
1267
1268 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1270 if flavor.is_pandoc_compatible() {
1271 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1272 } else {
1273 Vec::new()
1274 }
1275 });
1276
1277 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1279 if flavor.is_pandoc_compatible() {
1280 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1281 } else {
1282 Vec::new()
1283 }
1284 });
1285
1286 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1288 if flavor.is_pandoc_compatible() {
1289 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1290 } else {
1291 Vec::new()
1292 }
1293 });
1294
1295 let line_block_ranges = profile_section!("Line block ranges", profile, {
1297 if flavor.is_pandoc_compatible() {
1298 crate::utils::pandoc::detect_line_block_ranges(content)
1299 } else {
1300 Vec::new()
1301 }
1302 });
1303
1304 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1306 if flavor.is_pandoc_compatible() {
1307 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1308 } else {
1309 Vec::new()
1310 }
1311 });
1312
1313 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1315 if flavor.is_pandoc_compatible() {
1316 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1317 } else {
1318 Vec::new()
1319 }
1320 });
1321
1322 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1324 if flavor.is_pandoc_compatible() {
1325 crate::utils::pandoc::detect_grid_table_ranges(content)
1326 } else {
1327 Vec::new()
1328 }
1329 });
1330
1331 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1333 if flavor.is_pandoc_compatible() {
1334 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1335 } else {
1336 Vec::new()
1337 }
1338 });
1339
1340 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1342 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
1343 let mut ranges = Vec::new();
1344 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
1345 ranges.push((mat.start(), mat.end()));
1346 }
1347 ranges
1348 });
1349
1350 let inline_config =
1351 InlineConfig::from_content_with_code_blocks(content, &code_blocks, &code_span_byte_ranges(&code_spans));
1352 Self {
1353 content,
1354 content_lines,
1355 line_offsets,
1356 code_blocks,
1357 code_block_details,
1358 strong_spans,
1359 line_to_list,
1360 list_start_values,
1361 definition_lists,
1362 commonmark_ordered_lists_cache: OnceLock::new(),
1363 lines,
1364 blockquote_headings,
1365 links,
1366 images,
1367 broken_links,
1368 footnote_refs,
1369 reference_defs,
1370 reference_defs_map,
1371 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1372 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1374 math_byte_ranges_cache: OnceLock::new(), list_blocks,
1376 char_frequency,
1377 html_tags_cache: OnceLock::new(),
1378 jsx_component_tags_cache: OnceLock::new(),
1379 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1380 bare_urls_cache: OnceLock::new(),
1381 has_mixed_list_nesting_cache: OnceLock::new(),
1382 html_comment_ranges,
1383 table_blocks,
1384 line_index,
1385 jinja_ranges,
1386 flavor,
1387 source_file,
1388 link_target_policy: None,
1389 invalid_utf8: None,
1390 jsx_expression_ranges,
1391 mdx_comment_ranges,
1392 citation_ranges,
1393 pandoc_div_ranges,
1394 colon_fence_details,
1395 inline_footnote_ranges,
1396 pandoc_header_slugs,
1397 example_list_marker_ranges,
1398 example_reference_ranges,
1399 sub_super_ranges,
1400 inline_code_attr_ranges,
1401 bracketed_span_ranges,
1402 line_block_ranges,
1403 pipe_table_caption_ranges,
1404 pandoc_metadata_ranges,
1405 grid_table_ranges,
1406 multi_line_table_ranges,
1407 shortcode_ranges,
1408 link_title_ranges,
1409 code_span_byte_ranges: code_span_ranges,
1410 inline_config,
1411 obsidian_comment_ranges,
1412 unterminated_html_comment,
1413 unterminated_obsidian_comment,
1414 lazy_cont_lines_cache: OnceLock::new(),
1415 myst_directive_ranges,
1416 myst_comment_ranges,
1417 myst_role_ranges,
1418 front_matter_end,
1419 }
1420 }
1421
1422 pub fn front_matter_end_line(&self) -> usize {
1427 self.front_matter_end
1428 }
1429
1430 #[inline]
1433 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1434 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1436 idx > 0 && pos < ranges[idx - 1].1
1438 }
1439
1440 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1442 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1443 }
1444
1445 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
1450 let line = &self.lines[line_number - 1];
1451 heading_detection::ends_with_hard_break(
1452 line.content(self.content),
1453 line.byte_offset,
1454 &self.code_span_byte_ranges,
1455 )
1456 }
1457
1458 pub fn is_in_link(&self, pos: usize) -> bool {
1460 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
1461 }
1462
1463 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1465 let bare_urls = self.bare_urls();
1466 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1468 idx > 0 && pos < bare_urls[idx - 1].byte_end
1469 }
1470
1471 pub fn inline_config(&self) -> &InlineConfig {
1473 &self.inline_config
1474 }
1475
1476 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1481 &self.colon_fence_details
1482 }
1483
1484 pub fn raw_lines(&self) -> &[&'a str] {
1488 &self.content_lines
1489 }
1490
1491 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1496 self.inline_config.is_rule_disabled(rule_name, line_number)
1497 }
1498
1499 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1501 Arc::clone(
1502 self.code_spans_cache
1503 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1504 )
1505 }
1506
1507 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1511 self.math_byte_ranges_cache
1512 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1513 }
1514
1515 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1517 Arc::clone(
1518 self.math_spans_cache
1519 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1520 )
1521 }
1522
1523 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
1525 self.bracket_math_cache
1526 .get_or_init(|| bracket_math::parse(self.content, &self.lines, &self.code_spans(), &self.list_blocks))
1527 }
1528
1529 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1531 let math_spans = self.math_spans();
1532 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1534 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1535 }
1536
1537 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1539 &self.html_comment_ranges
1540 }
1541
1542 pub fn unterminated_html_comment(&self) -> Option<usize> {
1547 self.unterminated_html_comment
1548 }
1549
1550 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1554 self.unterminated_obsidian_comment
1555 }
1556
1557 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1561 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1562 }
1563
1564 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1569 if self.obsidian_comment_ranges.is_empty() {
1570 return false;
1571 }
1572
1573 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1575 self.is_in_obsidian_comment(byte_pos)
1576 }
1577
1578 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1580 &self.myst_directive_ranges
1581 }
1582
1583 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1585 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1586 }
1587
1588 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1590 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1591 }
1592
1593 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1600 if !self.flavor.supports_myst_directives() {
1601 return false;
1602 }
1603 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1604 info.in_myst_directive
1605 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1606 })
1607 }
1608
1609 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1611 tags.into_iter()
1612 .filter(|tag| {
1613 !self
1614 .lines
1615 .get(tag.line - 1)
1616 .is_some_and(|l| l.in_kramdown_extension_block)
1617 })
1618 .collect()
1619 }
1620
1621 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1627 Arc::clone(self.html_tags_cache.get_or_init(|| {
1628 let (html_tags, jsx_component_tags) =
1629 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1630 let _ = self
1632 .jsx_component_tags_cache
1633 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1634 Arc::new(self.filter_kramdown_tags(html_tags))
1635 }))
1636 }
1637
1638 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1641 if let Some(cached) = self.jsx_component_tags_cache.get() {
1642 return Arc::clone(cached);
1643 }
1644 let _ = self.html_tags();
1646 Arc::clone(
1647 self.jsx_component_tags_cache
1648 .get()
1649 .expect("html_tags() populates jsx_component_tags_cache"),
1650 )
1651 }
1652
1653 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1655 Arc::clone(
1656 self.emphasis_spans_cache
1657 .get()
1658 .expect("emphasis_spans_cache initialized during construction"),
1659 )
1660 }
1661
1662 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1664 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1665 Arc::new(element_parsers::parse_bare_urls(
1666 self.content,
1667 &self.lines,
1668 &self.code_blocks,
1669 ))
1670 }))
1671 }
1672
1673 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1675 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1676 Arc::new(element_parsers::detect_lazy_continuation_lines(
1677 self.content,
1678 &self.lines,
1679 &self.line_offsets,
1680 ))
1681 }))
1682 }
1683
1684 pub fn has_mixed_list_nesting(&self) -> bool {
1688 *self
1689 .has_mixed_list_nesting_cache
1690 .get_or_init(|| self.compute_mixed_list_nesting())
1691 }
1692
1693 fn compute_mixed_list_nesting(&self) -> bool {
1695 let mut stack: Vec<(usize, bool)> = Vec::new();
1700 let mut last_was_blank = false;
1701
1702 for line_info in &self.lines {
1703 if line_info.in_code_block
1705 || line_info.in_front_matter
1706 || line_info.in_mkdocstrings
1707 || line_info.in_html_comment
1708 || line_info.in_mdx_comment
1709 || line_info.in_esm_block
1710 {
1711 continue;
1712 }
1713
1714 if line_info.is_blank {
1716 last_was_blank = true;
1717 continue;
1718 }
1719
1720 if let Some(list_item) = &line_info.list_item {
1721 let current_pos = if list_item.marker_column == 1 {
1723 0
1724 } else {
1725 list_item.marker_column
1726 };
1727
1728 if last_was_blank && current_pos == 0 {
1730 stack.clear();
1731 }
1732 last_was_blank = false;
1733
1734 while let Some(&(pos, _)) = stack.last() {
1736 if pos >= current_pos {
1737 stack.pop();
1738 } else {
1739 break;
1740 }
1741 }
1742
1743 if let Some(&(_, parent_is_ordered)) = stack.last()
1745 && parent_is_ordered != list_item.is_ordered
1746 {
1747 return true; }
1749
1750 stack.push((current_pos, list_item.is_ordered));
1751 } else {
1752 last_was_blank = false;
1754 }
1755 }
1756
1757 false
1758 }
1759
1760 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1766 match self.line_offsets.binary_search(&offset) {
1767 Ok(line) => (line + 1, 1),
1768 Err(line) => {
1769 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1770 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1772 (line, col)
1773 }
1774 }
1775 }
1776
1777 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
1783 self.line_index.get_line_start_byte(line_number)
1784 }
1785
1786 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
1792 self.line_index.line_col_to_byte_range(line_number, column)
1793 }
1794
1795 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
1800 self.line_index
1801 .line_col_to_byte_range_with_length(line_number, column, length)
1802 }
1803
1804 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
1807 self.line_index.whole_line_range(line_number)
1808 }
1809
1810 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
1815 self.line_index.line_text_range(line_number, start_column, end_column)
1816 }
1817
1818 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
1821 self.line_index.line_content_range(line_number)
1822 }
1823
1824 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
1826 self.line_index.multi_line_range(start_line, end_line)
1827 }
1828
1829 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1831 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1833 return true;
1834 }
1835
1836 self.is_byte_offset_in_code_span(pos)
1838 }
1839
1840 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1842 if line_num > 0 {
1843 self.lines.get(line_num - 1)
1844 } else {
1845 None
1846 }
1847 }
1848
1849 pub fn links(&self) -> &[ParsedLink<'a>] {
1851 &self.links
1852 }
1853
1854 pub fn images(&self) -> &[ParsedImage<'a>] {
1856 &self.images
1857 }
1858
1859 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
1861 &self.broken_links
1862 }
1863
1864 pub fn footnote_references(&self) -> &[FootnoteRef] {
1866 &self.footnote_refs
1867 }
1868
1869 pub fn reference_definitions(&self) -> &[ReferenceDef] {
1871 &self.reference_defs
1872 }
1873
1874 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
1876 let start = self.links.partition_point(|link| link.line < line_number);
1877 let end = self.links.partition_point(|link| link.line <= line_number);
1878 &self.links[start..end]
1879 }
1880
1881 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
1883 let start = self.images.partition_point(|image| image.line < line_number);
1884 let end = self.images.partition_point(|image| image.line <= line_number);
1885 &self.images[start..end]
1886 }
1887
1888 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1890 self.links
1891 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
1892 .ok()
1893 .map(|index| &self.links[index])
1894 }
1895
1896 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1898 self.images
1899 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
1900 .ok()
1901 .map(|index| &self.images[index])
1902 }
1903
1904 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1906 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1907 self.links
1908 .get(index.checked_sub(1)?)
1909 .filter(|link| byte_offset < link.byte_end)
1910 }
1911
1912 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1914 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
1915 self.images
1916 .get(index.checked_sub(1)?)
1917 .filter(|image| byte_offset < image.byte_end)
1918 }
1919
1920 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
1922 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1923 &self.links[..end]
1924 }
1925
1926 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
1928 let normalized_id = ref_id.to_lowercase();
1929 self.reference_defs_map
1930 .get(&normalized_id)
1931 .map(|&index| &self.reference_defs[index])
1932 }
1933
1934 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1936 self.reference_definition(ref_id)
1937 .map(|definition| definition.url.as_str())
1938 }
1939
1940 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1942 if line_num == 0 || line_num > self.lines.len() {
1943 return false;
1944 }
1945 self.lines[line_num - 1].in_list_block
1946 }
1947
1948 pub fn is_in_definition_list(&self, line_num: usize) -> bool {
1956 let lists = &self.definition_lists.outer;
1957 let idx = lists.partition_point(|&(start, _)| start <= line_num);
1958 idx > 0 && line_num <= lists[idx - 1].1
1959 }
1960
1961 pub fn is_definition_term(&self, line_num: usize) -> bool {
1963 let terms = &self.definition_lists.terms;
1964 let idx = terms.partition_point(|&(_, end)| end < line_num);
1965 terms.get(idx).is_some_and(|&(start, _)| start <= line_num)
1966 }
1967
1968 pub fn definition_text_at(&self, line_num: usize) -> Option<&DefinitionText> {
1970 let texts = &self.definition_lists.texts;
1971 let idx = texts.partition_point(|text| text.end_line < line_num);
1972 texts.get(idx).filter(|text| text.start_line <= line_num)
1973 }
1974
1975 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1977 if line_num == 0 || line_num > self.lines.len() {
1978 return false;
1979 }
1980 self.lines[line_num - 1].in_html_block
1981 }
1982
1983 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1989 if line_num == 0 || line_num > self.lines.len() {
1990 return false;
1991 }
1992 self.lines[line_num - 1].in_table_block
1993 }
1994
1995 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1997 if line_num == 0 || line_num > self.lines.len() {
1998 return false;
1999 }
2000
2001 let col_0indexed = if col > 0 { col - 1 } else { 0 };
2005 let code_spans = self.code_spans();
2006 code_spans.iter().any(|span| {
2007 if line_num < span.line || line_num > span.end_line {
2009 return false;
2010 }
2011
2012 if span.line == span.end_line {
2013 col_0indexed >= span.start_col && col_0indexed < span.end_col
2015 } else if line_num == span.line {
2016 col_0indexed >= span.start_col
2018 } else if line_num == span.end_line {
2019 col_0indexed < span.end_col
2021 } else {
2022 true
2024 }
2025 })
2026 }
2027
2028 #[inline]
2030 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
2031 let code_spans = self.code_spans();
2032 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
2033 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
2034 }
2035
2036 #[inline]
2038 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
2039 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
2040 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
2041 }
2042
2043 #[inline]
2045 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
2046 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
2047 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
2048 }
2049
2050 #[inline]
2053 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
2054 let tags = self.html_tags();
2055 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2056 idx > 0 && byte_pos < tags[idx - 1].byte_end
2057 }
2058
2059 #[inline]
2063 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
2064 if !self.flavor.supports_jsx() {
2065 return false;
2066 }
2067 let tags = self.jsx_component_tags();
2068 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2069 idx > 0 && byte_pos < tags[idx - 1].byte_end
2070 }
2071
2072 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
2074 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
2075 }
2076
2077 #[inline]
2079 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
2080 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
2081 }
2082
2083 #[inline]
2085 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
2086 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
2087 }
2088
2089 #[inline]
2092 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
2093 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
2094 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
2095 }
2096
2097 #[inline]
2099 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2100 &self.citation_ranges
2101 }
2102
2103 #[inline]
2106 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
2107 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
2108 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
2109 }
2110
2111 #[inline]
2114 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
2115 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
2116 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
2117 }
2118
2119 #[inline]
2122 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2123 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2124 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2125 }
2126
2127 #[inline]
2130 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2131 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2132 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2133 }
2134
2135 #[inline]
2138 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2139 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
2140 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
2141 }
2142
2143 #[inline]
2147 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
2148 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
2149 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
2150 }
2151
2152 #[inline]
2155 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
2156 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
2157 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
2158 }
2159
2160 #[inline]
2163 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
2164 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
2165 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
2166 }
2167
2168 #[inline]
2172 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
2173 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
2174 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
2175 }
2176
2177 #[inline]
2180 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
2181 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
2182 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
2183 }
2184
2185 #[inline]
2188 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
2189 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
2190 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
2191 }
2192
2193 #[inline]
2196 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
2197 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
2198 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
2199 }
2200
2201 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
2206 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
2207 self.pandoc_header_slugs.contains(&slug)
2208 }
2209
2210 #[inline]
2216 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
2217 self.pandoc_header_slugs.contains(slug)
2218 }
2219
2220 #[inline]
2222 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
2223 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
2224 }
2225
2226 #[inline]
2228 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
2229 &self.shortcode_ranges
2230 }
2231
2232 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
2234 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
2235 }
2236
2237 pub fn has_char(&self, ch: char) -> bool {
2239 match ch {
2240 '#' => self.char_frequency.hash_count > 0,
2241 '*' => self.char_frequency.asterisk_count > 0,
2242 '_' => self.char_frequency.underscore_count > 0,
2243 '-' => self.char_frequency.hyphen_count > 0,
2244 '+' => self.char_frequency.plus_count > 0,
2245 '>' => self.char_frequency.gt_count > 0,
2246 '|' => self.char_frequency.pipe_count > 0,
2247 '[' => self.char_frequency.bracket_count > 0,
2248 '`' => self.char_frequency.backtick_count > 0,
2249 '<' => self.char_frequency.lt_count > 0,
2250 '!' => self.char_frequency.exclamation_count > 0,
2251 '\n' => self.char_frequency.newline_count > 0,
2252 _ => self.content.contains(ch), }
2254 }
2255
2256 pub fn char_count(&self, ch: char) -> usize {
2258 match ch {
2259 '#' => self.char_frequency.hash_count,
2260 '*' => self.char_frequency.asterisk_count,
2261 '_' => self.char_frequency.underscore_count,
2262 '-' => self.char_frequency.hyphen_count,
2263 '+' => self.char_frequency.plus_count,
2264 '>' => self.char_frequency.gt_count,
2265 '|' => self.char_frequency.pipe_count,
2266 '[' => self.char_frequency.bracket_count,
2267 '`' => self.char_frequency.backtick_count,
2268 '<' => self.char_frequency.lt_count,
2269 '!' => self.char_frequency.exclamation_count,
2270 '\n' => self.char_frequency.newline_count,
2271 _ => self.content.matches(ch).count(), }
2273 }
2274
2275 pub fn likely_has_headings(&self) -> bool {
2278 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.content.contains('=')
2279 }
2280
2281 pub fn likely_has_lists(&self) -> bool {
2285 self.char_frequency.asterisk_count > 0
2286 || self.char_frequency.hyphen_count > 0
2287 || self.char_frequency.plus_count > 0
2288 }
2289
2290 pub fn likely_has_emphasis(&self) -> bool {
2292 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
2293 }
2294
2295 pub fn likely_has_tables(&self) -> bool {
2297 self.char_frequency.pipe_count > 2
2298 }
2299
2300 pub fn likely_has_blockquotes(&self) -> bool {
2302 self.char_frequency.gt_count > 0
2303 }
2304
2305 pub fn likely_has_code(&self) -> bool {
2307 self.char_frequency.backtick_count > 0
2308 }
2309
2310 pub fn likely_has_links_or_images(&self) -> bool {
2312 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
2313 }
2314
2315 pub fn likely_has_html(&self) -> bool {
2317 self.char_frequency.lt_count > 0
2318 }
2319
2320 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
2325 if let Some(line_info) = self.lines.get(line_idx)
2326 && let Some(ref bq) = line_info.blockquote
2327 {
2328 bq.prefix.trim_end().to_string()
2329 } else {
2330 String::new()
2331 }
2332 }
2333
2334 #[inline]
2345 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
2346 let idx = match lines.binary_search_by(|line| {
2348 if byte_offset < line.byte_offset {
2349 std::cmp::Ordering::Greater
2350 } else if byte_offset > line.byte_offset + line.byte_len {
2351 std::cmp::Ordering::Less
2352 } else {
2353 std::cmp::Ordering::Equal
2354 }
2355 }) {
2356 Ok(idx) => idx,
2357 Err(idx) => idx.saturating_sub(1),
2358 };
2359
2360 let line = &lines[idx];
2361 let line_num = idx + 1;
2362 let byte_col = byte_offset.saturating_sub(line.byte_offset);
2363 let col = byte_to_char_count(line.content(content), byte_col) - 1;
2366
2367 (idx, line_num, col)
2368 }
2369
2370 #[inline]
2372 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
2373 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
2375
2376 if idx > 0 {
2378 let span = &code_spans[idx - 1];
2379 if offset >= span.byte_offset && offset < span.byte_end {
2380 return true;
2381 }
2382 }
2383
2384 false
2385 }
2386
2387 #[must_use]
2407 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
2408 ValidHeadingsIter::new(&self.lines)
2409 }
2410
2411 #[must_use]
2415 pub fn has_valid_headings(&self) -> bool {
2416 self.lines.iter().any(|line| line.heading.is_some())
2417 }
2418
2419 #[must_use]
2421 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
2422 ParsedListItemsIter::new(&self.lines)
2423 }
2424
2425 #[must_use]
2427 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
2428 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
2429 Some(ParsedListItem::new(
2430 line_num,
2431 line_info.list_item.as_deref()?,
2432 line_info,
2433 ))
2434 }
2435
2436 #[must_use]
2438 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
2439 ParsedListBlocks::new(&self.list_blocks, &self.lines)
2440 }
2441
2442 #[must_use]
2446 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
2447 list_blocks::item_lines_by_list(self.content, &self.lines, block)
2448 }
2449
2450 #[must_use]
2452 pub fn has_list_items(&self) -> bool {
2453 self.lines.iter().any(|line| line.list_item.is_some())
2454 }
2455
2456 #[must_use]
2458 pub fn has_unordered_list_items(&self) -> bool {
2459 self.lines
2460 .iter()
2461 .any(|line| line.list_item.as_ref().is_some_and(|item| !item.is_ordered))
2462 }
2463
2464 #[must_use]
2466 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
2467 let lists = self
2468 .commonmark_ordered_lists_cache
2469 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
2470 CommonMarkOrderedLists::new(lists, &self.lines)
2471 }
2472
2473 #[must_use]
2481 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
2482 ParsedHeadingsIter::new(&self.lines, &self.blockquote_headings)
2483 }
2484
2485 #[must_use]
2487 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
2488 let idx = line_num.checked_sub(1)?;
2489 let line_info = self.lines.get(idx)?;
2490 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
2491 Some(heading) => (heading, 0),
2492 None => (
2493 self.blockquote_headings.get(idx)?.as_deref()?,
2494 line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level),
2495 ),
2496 };
2497 Some(ParsedHeading {
2498 line_num,
2499 heading,
2500 line_info,
2501 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
2502 blockquote_depth,
2503 })
2504 }
2505}
2506
2507fn container_comment_range(
2519 opener: usize,
2520 containers: &flavor_detection::ContainerLines,
2521 lines: &[types::LineInfo],
2522 content: &str,
2523) -> Option<crate::utils::skip_context::ByteRange> {
2524 let line_index = lines
2525 .partition_point(|line| line.byte_offset <= opener)
2526 .checked_sub(1)?;
2527 let line = lines.get(line_index)?;
2528 if line.byte_offset + line.indent != opener {
2529 return None;
2530 }
2531 if !containers.is_container_body(line_index) {
2532 return None;
2533 }
2534 let end_line = lines.get(containers.body_end_line(line_index)?)?;
2535 Some(crate::utils::skip_context::ByteRange {
2536 start: opener,
2537 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
2538 })
2539}
2540
2541#[derive(Debug)]
2543struct DefinitionListLines {
2544 outer: Vec<(usize, usize)>,
2547 terms: Vec<(usize, usize)>,
2549 texts: Vec<DefinitionText>,
2551}
2552
2553impl DefinitionListLines {
2554 fn new(
2555 line_offsets: &[usize],
2556 items: &[(usize, usize)],
2557 terms: &[(usize, usize)],
2558 texts: &[crate::utils::code_block_utils::DefinitionTextDetail],
2559 ) -> Self {
2560 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
2563 let last_line_of = |start: usize, end: usize| line_of(end.saturating_sub(1).max(start));
2564
2565 let mut outer: Vec<(usize, usize)> = Vec::new();
2568 for &(start, end) in items {
2569 let (start_line, end_line) = (line_of(start), last_line_of(start, end));
2570 match outer.last_mut() {
2571 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
2572 _ => outer.push((start_line, end_line)),
2573 }
2574 }
2575 let terms = terms
2576 .iter()
2577 .map(|&(start, end)| (line_of(start), last_line_of(start, end)))
2578 .collect();
2579
2580 let texts = texts
2581 .iter()
2582 .map(|text| {
2583 let start_line = line_of(text.start);
2584 let line_start = line_offsets[start_line - 1];
2585 let on_marker_line = line_of(text.definition_start) == start_line;
2586 DefinitionText {
2587 start_line,
2588 end_line: last_line_of(text.start, text.end),
2589 marker_prefix_len: on_marker_line.then(|| text.start - line_start),
2590 }
2591 })
2592 .collect();
2593
2594 Self { outer, terms, texts }
2595 }
2596}
2597
2598fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
2607 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
2608
2609 let options = crate::utils::rumdl_parser_options();
2610 let parser = Parser::new_ext(content, options).into_offset_iter();
2611
2612 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
2614 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
2615 let mut in_footnote = false;
2616
2617 for (event, range) in parser {
2618 match event {
2619 Event::Start(Tag::FootnoteDefinition(_)) => {
2620 in_footnote = true;
2621 footnote_ranges.push((range.start, range.end));
2622 }
2623 Event::End(TagEnd::FootnoteDefinition) => {
2624 in_footnote = false;
2625 }
2626 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
2627 fenced_code_ranges.push((range.start, range.end));
2628 }
2629 _ => {}
2630 }
2631 }
2632
2633 let byte_to_line = |byte_offset: usize| -> usize {
2634 line_offsets
2635 .partition_point(|&offset| offset <= byte_offset)
2636 .saturating_sub(1)
2637 };
2638
2639 for &(start, end) in &footnote_ranges {
2641 let start_line = byte_to_line(start);
2642 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2643
2644 for line in &mut lines[start_line..end_line] {
2645 line.in_footnote_definition = true;
2646 line.in_code_block = false;
2647 }
2648 }
2649
2650 for &(start, end) in &fenced_code_ranges {
2652 let start_line = byte_to_line(start);
2653 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2654
2655 for line in &mut lines[start_line..end_line] {
2656 line.in_code_block = true;
2657 }
2658 }
2659}