1pub mod types;
2
3pub(crate) use heading_detection::atx_display_text_for_proof;
4pub(crate) use heading_detection::is_paragraph_text_line;
5pub(crate) use link_parser::{image_pattern, link_pattern, reference_label_start};
6pub(crate) use list_blocks::{column_after, column_at};
7pub use types::*;
8
9mod anchors;
10mod bracket_math;
11mod candidates;
12pub(crate) use candidates::LineCandidateKind;
13pub(crate) mod document_parser;
14mod element_parsers;
15mod emphasis;
16pub(crate) use emphasis::EmphasisLine;
17mod flavor_detection;
18mod heading_detection;
19mod line_computation;
20mod line_facts;
21pub(crate) use line_facts::LineContextMask;
22mod link_destinations;
23mod link_parser;
24mod list_blocks;
25mod mdx;
26mod native_structure;
27pub(crate) use native_structure::{NativeListSource, NativePreparedStructure};
28mod scratch;
29mod source_positions;
30pub(crate) use mdx::{leaf_block_ranges as mdx_leaf_block_ranges, structure_signature as mdx_structure_signature};
31pub(crate) use source_positions::SourcePositions;
32mod table_confirm;
33#[cfg(test)]
34mod tests;
35
36use crate::config::MarkdownFlavor;
37use crate::inline_config::InlineConfig;
38use crate::rules::front_matter_utils::FrontMatterUtils;
39use crate::utils::code_block_utils::{CodeBlockDetail, CodeBlockUtils};
40use crate::utils::range_utils::byte_to_char_count;
41use std::collections::HashMap;
42use std::ops::Range;
43use std::path::{Path, PathBuf};
44
45#[derive(Debug, Clone)]
48pub struct LinkTargetPolicy {
49 supplied_paths: Arc<std::collections::HashSet<PathBuf>>,
50 supplied_dirs: Arc<std::collections::HashSet<PathBuf>>,
54 declared_paths: Arc<std::collections::HashSet<PathBuf>>,
57 declared_dirs: Arc<std::collections::HashSet<PathBuf>>,
58 allow_disk_fallback: bool,
59}
60
61impl LinkTargetPolicy {
62 pub fn open_world<I, P>(paths: I) -> Self
65 where
66 I: IntoIterator<Item = P>,
67 P: AsRef<Path>,
68 {
69 Self::from_paths(paths, true)
70 }
71
72 pub fn closed_world<I, P>(paths: I) -> Self
74 where
75 I: IntoIterator<Item = P>,
76 P: AsRef<Path>,
77 {
78 Self::from_paths(paths, false)
79 }
80
81 pub fn with_declared_targets<I, P, F, D>(paths: I, files: F, directories: D, allow_disk_fallback: bool) -> Self
86 where
87 I: IntoIterator<Item = P>,
88 P: AsRef<Path>,
89 F: IntoIterator<Item = PathBuf>,
90 D: IntoIterator<Item = PathBuf>,
91 {
92 Self::build(paths, files, directories, allow_disk_fallback, Self::working_roots())
93 }
94
95 fn working_roots() -> Vec<PathBuf> {
96 let mut roots = Vec::new();
97 if let Ok(cwd) = std::env::current_dir() {
98 if let Ok(canonical_cwd) = cwd.canonicalize()
99 && canonical_cwd != cwd
100 {
101 roots.push(canonical_cwd);
102 }
103 roots.push(cwd);
104 }
105 roots
106 }
107
108 fn from_paths<I, P>(paths: I, allow_disk_fallback: bool) -> Self
109 where
110 I: IntoIterator<Item = P>,
111 P: AsRef<Path>,
112 {
113 Self::from_paths_with_roots(paths, allow_disk_fallback, Self::working_roots())
114 }
115
116 fn from_paths_with_roots<I, P, R, Q>(paths: I, allow_disk_fallback: bool, roots: R) -> Self
117 where
118 I: IntoIterator<Item = P>,
119 P: AsRef<Path>,
120 R: IntoIterator<Item = Q>,
121 Q: AsRef<Path>,
122 {
123 Self::build(paths, Vec::new(), Vec::new(), allow_disk_fallback, roots)
124 }
125
126 fn build<I, P, F, D, R, Q>(paths: I, files: F, directories: D, allow_disk_fallback: bool, roots: R) -> Self
127 where
128 I: IntoIterator<Item = P>,
129 P: AsRef<Path>,
130 F: IntoIterator<Item = PathBuf>,
131 D: IntoIterator<Item = PathBuf>,
132 R: IntoIterator<Item = Q>,
133 Q: AsRef<Path>,
134 {
135 let roots: Vec<PathBuf> = roots
136 .into_iter()
137 .map(|root| crate::workspace_index::normalize_relative_path(root.as_ref()))
138 .collect();
139 let mut supplied_paths = std::collections::HashSet::new();
140 let add_spellings = |set: &mut std::collections::HashSet<PathBuf>, path: &Path| {
142 set.insert(crate::workspace_index::normalize_relative_path(path));
143
144 if path.is_relative() {
145 for root in &roots {
146 set.insert(crate::workspace_index::normalize_relative_path(&root.join(path)));
147 }
148 } else {
149 let resolved = crate::discovery::resolve_for_matching(path);
155 let relative = [path, resolved.as_path()].into_iter().find_map(|spelling| {
156 roots
157 .iter()
158 .find_map(|source_root| spelling.strip_prefix(source_root).ok())
159 });
160 if let Some(relative) = relative {
161 set.insert(crate::workspace_index::normalize_relative_path(relative));
162 for root in &roots {
163 set.insert(crate::workspace_index::normalize_relative_path(&root.join(relative)));
164 }
165 }
166 }
167 };
168 for path in paths {
169 add_spellings(&mut supplied_paths, path.as_ref());
170 }
171 let mut declared_paths = std::collections::HashSet::new();
172 for path in files {
173 add_spellings(&mut declared_paths, &path);
174 }
175 let mut declared_dirs = std::collections::HashSet::new();
176 for path in directories {
177 add_spellings(&mut declared_dirs, &path);
178 }
179 let declared_only = declared_paths.difference(&supplied_paths).cloned().collect();
181 let content_unknown_dirs = Self::implied_dirs(&declared_paths, &declared_dirs, &roots);
182 supplied_paths.extend(declared_paths);
183 let supplied_dirs = Self::implied_dirs(&supplied_paths, &declared_dirs, &roots);
184 Self {
185 supplied_paths: Arc::new(supplied_paths),
186 supplied_dirs: Arc::new(supplied_dirs),
187 declared_paths: Arc::new(declared_only),
188 declared_dirs: Arc::new(content_unknown_dirs),
189 allow_disk_fallback,
190 }
191 }
192
193 fn implied_dirs(
194 supplied_paths: &std::collections::HashSet<PathBuf>,
195 declared_dirs: &std::collections::HashSet<PathBuf>,
196 roots: &[PathBuf],
197 ) -> std::collections::HashSet<PathBuf> {
198 let escapes_roots = |path: &Path| {
207 if path.is_absolute() {
208 !roots.iter().any(|root| path.starts_with(root))
209 } else {
210 path.starts_with("..")
211 }
212 };
213 let outside_root = |absolute: bool| {
214 let file_parents = supplied_paths
216 .iter()
217 .filter(|path| path.is_absolute() == absolute && escapes_roots(path))
218 .filter_map(|path| path.parent());
219 let declared = declared_dirs
220 .iter()
221 .filter(|dir| dir.is_absolute() == absolute && escapes_roots(dir))
222 .map(PathBuf::as_path);
223 file_parents.chain(declared).fold(None::<&Path>, |common, dir| {
224 Some(common.map_or(dir, |common| {
225 common
226 .ancestors()
227 .find(|ancestor| dir.starts_with(ancestor))
228 .unwrap_or(common)
229 }))
230 })
231 };
232 let outside_roots = [outside_root(true), outside_root(false)];
233 let mut supplied_dirs = std::collections::HashSet::new();
234 for seed in supplied_paths
235 .iter()
236 .filter_map(|path| path.parent())
237 .chain(declared_dirs.iter().map(PathBuf::as_path))
238 {
239 for dir in seed.ancestors() {
240 if !escapes_roots(dir) || outside_roots.iter().flatten().any(|root| dir.starts_with(root)) {
241 if supplied_dirs.contains(dir) {
244 break;
245 }
246 supplied_dirs.insert(dir.to_path_buf());
247 }
248 }
249 }
250 supplied_dirs
251 }
252
253 pub fn declared_target_for(&self, path: &Path) -> Option<PathBuf> {
259 self.resolve_supplied(path)
260 .filter(|named| self.declared_paths.contains(named) || self.declared_dirs.contains(named))
261 }
262
263 pub fn contains(&self, path: &Path) -> bool {
264 self.supplied_paths
265 .contains(&crate::workspace_index::normalize_relative_path(path))
266 }
267
268 pub(crate) fn resolve_supplied(&self, path: &Path) -> Option<PathBuf> {
269 let normalized = crate::workspace_index::normalize_relative_path(path);
270 if self.supplied_paths.contains(&normalized) {
271 return Some(normalized);
272 }
273
274 if path.extension().is_none() {
275 for extension in ["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"] {
276 let candidate = crate::workspace_index::normalize_relative_path(&path.with_extension(extension));
277 if self.supplied_paths.contains(&candidate) {
278 return Some(candidate);
279 }
280 }
281 }
282
283 if self.supplied_dirs.contains(&normalized) {
286 return Some(normalized);
287 }
288
289 None
290 }
291
292 pub fn contains_with_markdown_extension(&self, path: &Path) -> bool {
293 self.resolve_supplied(path).is_some()
294 }
295
296 pub fn allow_disk_fallback(&self) -> bool {
297 self.allow_disk_fallback
298 }
299}
300
301#[cfg(not(target_arch = "wasm32"))]
303macro_rules! profile_section {
304 ($name:expr, $profile:expr, $code:expr) => {{
305 let start = $profile.then(std::time::Instant::now);
306 let result = $code;
307 if let Some(start) = start {
308 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
309 }
310 result
311 }};
312}
313
314fn build_commonmark_ordered_lists(
315 lines: &[LineInfo],
316 line_to_list: &crate::utils::code_block_utils::LineToListMap,
317 list_start_values: &crate::utils::code_block_utils::ListStartValues,
318) -> Vec<CommonMarkOrderedListInfo> {
319 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
320
321 for (&line_num, &list_id) in line_to_list {
322 let is_ordered_item = line_num
323 .checked_sub(1)
324 .and_then(|index| lines.get(index))
325 .and_then(|line| line.list_item.as_deref())
326 .is_some_and(|item| item.is_ordered);
327 if is_ordered_item {
328 grouped_lines.entry(list_id).or_default().push(line_num);
329 }
330 }
331
332 let mut lists: Vec<_> = grouped_lines
333 .into_iter()
334 .map(|(list_id, mut item_lines)| {
335 item_lines.sort_unstable();
336 CommonMarkOrderedListInfo {
337 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
338 item_lines,
339 }
340 })
341 .collect();
342 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
343 lists
344}
345
346#[cfg(target_arch = "wasm32")]
347macro_rules! profile_section {
348 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
349}
350
351pub(super) struct SkipByteRanges<'a> {
354 pub(super) front_matter_end: usize,
355 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
356 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
357 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
358 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
359}
360
361use std::sync::{Arc, OnceLock};
362
363pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
365
366pub(super) type ByteRanges = Vec<(usize, usize)>;
368
369pub(crate) struct NativeListSpacingPlan {
371 pub warnings: Vec<crate::rule::LintWarning>,
372 pub withheld: std::collections::HashSet<(usize, bool)>,
373 pub blocks: Vec<(usize, usize, String)>,
374 pub fallback: Vec<crate::rule::LintWarning>,
375}
376
377pub struct LintContext<'a> {
380 pub content: &'a str,
381
382 native_list_spacing_plan: OnceLock<NativeListSpacingPlan>,
383
384 list_guard_parse: OnceLock<crate::utils::list_fix_guard::Parsed<'static>>,
385 parser_blocks: OnceLock<document_parser::blocks::BlockFacts>,
386
387 parse_handoff: std::sync::Mutex<Option<document_parser::handoff::OwnedParsedDocument>>,
388 merge_conflicts: OnceLock<Vec<crate::merge_conflict::Marker>>,
389 content_lines: Vec<&'a str>, line_candidates: candidates::LineCandidates,
391 lexical_emphasis: OnceLock<emphasis::LexicalEmphasis>,
392 document_lines: OnceLock<line_facts::DocumentLines>,
393 pub line_offsets: Vec<usize>,
394 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>>>, document_anchors_cache: OnceLock<anchors::DocumentAnchors>,
405 link_destinations_cache: OnceLock<link_destinations::LinkDestinations>,
406 links: Vec<ParsedLink<'a>>, rendered_link_ends_cache: OnceLock<Vec<usize>>, parsed_link_ranges: Vec<(usize, usize)>, images: Vec<ParsedImage<'a>>, unresolved_links: Vec<ParsedLink<'a>>, unresolved_images: Vec<ParsedImage<'a>>, broken_links: Vec<BrokenLinkInfo>, footnote_refs: Vec<FootnoteRef>, reference_defs: Vec<ReferenceDef>, reference_defs_map: OnceLock<HashMap<String, usize>>, definition_candidates: Vec<link_parser::DefinitionCandidate>, body_definitions: OnceLock<Vec<usize>>, inline_code_ranges: Vec<(usize, usize)>,
419 code_spans_cache: OnceLock<Arc<Vec<CodeSpan>>>,
420 source_code_spans: OnceLock<Vec<SourceCodeSpan>>, math_spans_cache: OnceLock<Arc<Vec<MathSpan>>>, bracket_math_cache: OnceLock<bracket_math::BracketDisplayMathLines>,
423 math_byte_ranges_cache: OnceLock<Vec<(usize, usize)>>, parser_has_math: bool,
426 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>>>, source_emphasis: Vec<SourceEmphasisSpan>,
432 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_ranges: Vec<(usize, usize)>, html_block_lines: Vec<std::ops::Range<usize>>, pub table_blocks: Vec<crate::utils::table_utils::TableBlock>, parser_table_ranges: Vec<(usize, usize)>, jinja_ranges: Vec<(usize, usize)>, jinja_string_ranges_cache: OnceLock<Vec<(usize, usize)>>,
442 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, }
477
478pub struct CodeRanges {
480 pub blocks: Vec<(usize, usize)>,
482 pub spans: Vec<(usize, usize)>,
484}
485
486pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
497 let ctx = LintContext::new(content, flavor, None);
498 CodeRanges {
499 spans: ctx.inline_code_ranges.clone(),
500 blocks: ctx.code_blocks,
501 }
502}
503
504pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
506 code_spans
507 .iter()
508 .map(|span| (span.byte_offset, span.byte_end))
509 .collect()
510}
511
512impl<'a> LintContext<'a> {
513 pub(crate) fn native_list_spacing_plan(
514 &self,
515 prepare: impl FnOnce() -> NativeListSpacingPlan,
516 ) -> &NativeListSpacingPlan {
517 self.native_list_spacing_plan.get_or_init(prepare)
518 }
519
520 pub(crate) fn list_guard_parse(&self) -> &crate::utils::list_fix_guard::Parsed<'_> {
521 self.list_guard_parse
522 .get_or_init(|| crate::utils::list_fix_guard::Parsed::new(self.content).into_owned())
523 }
524
525 pub(crate) fn parser_blocks(&self) -> &document_parser::blocks::BlockFacts {
526 self.parser_blocks
527 .get_or_init(|| document_parser::blocks::BlockFacts::parse(self.content))
528 }
529
530 pub(crate) fn with_merge_conflicts(mut self, markers: &[crate::merge_conflict::Marker]) -> Self {
531 self.merge_conflicts = OnceLock::from(markers.to_vec());
532 self
533 }
534
535 pub(crate) fn merge_conflicts(&self) -> &[crate::merge_conflict::Marker] {
536 self.merge_conflicts
537 .get_or_init(|| crate::merge_conflict::scan_markers(self.content).collect())
538 }
539
540 pub fn source_file(&self) -> Option<&Path> {
545 self.source_file.as_deref()
546 }
547
548 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
549 self.link_target_policy.as_ref()
550 }
551
552 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
553 self.link_target_policy = Some(policy);
554 self
555 }
556
557 pub fn invalid_utf8(&self) -> Option<&'a [crate::encoding::InvalidSeq]> {
559 self.invalid_utf8
560 }
561
562 pub fn with_invalid_utf8(mut self, invalid: &'a [crate::encoding::InvalidSeq]) -> Self {
563 self.invalid_utf8 = Some(invalid);
564 self
565 }
566
567 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
568 Self::new_inner(content, flavor, source_file, None, None)
569 }
570
571 #[cfg(test)]
572 pub(crate) fn new_with_handoff(
573 content: &'a str,
574 flavor: MarkdownFlavor,
575 source_file: Option<PathBuf>,
576 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
577 ) -> Self {
578 Self::new_inner(
580 content,
581 flavor,
582 source_file,
583 handoff.filter(|parsed| parsed.matches(content)),
584 None,
585 )
586 }
587
588 pub(crate) fn store_parse_handoff(&self, parsed: document_parser::handoff::OwnedParsedDocument) {
589 if let Ok(mut slot) = self.parse_handoff.lock() {
590 *slot = Some(parsed);
591 }
592 }
593
594 pub(crate) fn certifies_list_guard(&self, content: &str, allowed: crate::utils::list_fix_guard::Allowed) -> bool {
595 self.parse_handoff
596 .lock()
597 .ok()
598 .and_then(|slot| {
599 slot.as_ref()
600 .map(|parsed| parsed.certifies_list_guard(content, allowed))
601 })
602 .unwrap_or(false)
603 }
604
605 pub(crate) fn take_parse_handoff(&self, content: &str) -> Option<document_parser::handoff::OwnedParsedDocument> {
606 self.parse_handoff
607 .lock()
608 .ok()?
609 .take()
610 .filter(|parsed| parsed.matches(content))
611 }
612
613 pub(crate) fn new_with_proof(
614 content: &'a str,
615 flavor: MarkdownFlavor,
616 source_file: Option<PathBuf>,
617 parsed: Option<document_parser::ParsedDocument<'a>>,
618 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
619 ) -> Self {
620 Self::new_inner(content, flavor, source_file, handoff, parsed)
621 }
622
623 fn new_inner(
624 content: &'a str,
625 flavor: MarkdownFlavor,
626 source_file: Option<PathBuf>,
627 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
628 proof_parsed: Option<document_parser::ParsedDocument<'a>>,
629 ) -> Self {
630 let (structure, finish) = Self::prepare_native_structure(content, flavor, source_file, handoff, proof_parsed);
631 finish(structure)
632 }
633
634 pub(crate) fn prepare_native_structure(
635 content: &'a str,
636 flavor: MarkdownFlavor,
637 source_file: Option<PathBuf>,
638 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
639 proof_parsed: Option<document_parser::ParsedDocument<'a>>,
640 ) -> (
641 NativePreparedStructure<'a>,
642 impl FnOnce(NativePreparedStructure<'a>) -> Self,
643 ) {
644 #[cfg(not(target_arch = "wasm32"))]
645 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
646
647 let line_computation::SourceLayout {
648 offsets: line_offsets,
649 lines: content_lines,
650 frequency: char_frequency,
651 candidates: line_candidates,
652 } = profile_section!(
653 "Source lines & markers",
654 profile,
655 line_computation::source_layout(content)
656 );
657
658 #[allow(clippy::disallowed_methods)]
662 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
663
664 let proof_parsed = proof_parsed.filter(|parsed| parsed.matches_source(content));
667 let handoff = handoff.filter(|document| document.matches(content));
668
669 let document_parser::ParsedDocument {
672 structure: parse_result,
673
674 has_math: parser_has_math,
675
676 parser_blocks,
677 pending_links,
678 pending_lines,
679 text_start_lines,
680 definitions: parser_definitions,
681 } = profile_section!("Shared Markdown parse", profile, {
682 proof_parsed.unwrap_or_else(|| {
683 handoff.map_or_else(
684 || document_parser::ParsedDocument::parse(content, &line_offsets),
685 |document| document.into_document(content),
686 )
687 })
688 });
689 let mut code_blocks = parse_result.code_blocks;
690 let parser_code_blocks = code_blocks.clone();
691 let table_ranges = parse_result.table_ranges;
692 let top_level_blocks = parse_result.top_level_blocks;
693 let rejected_definition_ranges = parse_result.rejected_definition_ranges;
694 let list_items = parse_result.list_items;
695 let mut code_span_ranges = parse_result.code_spans;
696 let code_block_details = parse_result.code_block_details;
697 let strong_spans = parse_result.strong_spans;
698 let line_to_list = parse_result.line_to_list;
699 let list_start_values = parse_result.list_start_values;
700 let html_blocks = parse_result.html_blocks;
701 let definition_lists = DefinitionListLines::new(
702 &line_offsets,
703 &parse_result.definition_items,
704 &parse_result.definition_terms,
705 &parse_result.definition_texts,
706 );
707
708 let mut in_fenced_code = scratch::LineFlags::new(content_lines.len());
713 for detail in code_block_details.iter().filter(|detail| detail.is_fenced) {
714 let start_line = line_offsets
715 .partition_point(|&offset| offset <= detail.start)
716 .saturating_sub(1);
717 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
718 for flag in in_fenced_code.iter_mut().take(end_line).skip(start_line) {
719 *flag = true;
720 }
721 }
722 let tag_hiding = if crate::utils::mkdocs_html_markdown::mentions_markdown(content) {
727 let comment_code_ranges: Vec<(usize, usize)> = code_block_details
728 .iter()
729 .map(|detail| (detail.start, detail.end))
730 .collect();
731 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
732 let comments = crate::utils::skip_context::scan_html_comments(
733 content,
734 &code_span_ranges,
735 &comment_code_ranges,
736 body_start,
737 )
738 .ranges;
739 let comments: Vec<(usize, usize)> = comments.iter().map(|range| (range.start, range.end)).collect();
740 let indented: Vec<(usize, usize)> = code_block_details
741 .iter()
742 .filter(|detail| !detail.is_fenced)
743 .map(|detail| (detail.start, detail.end))
744 .collect();
745 let containing = |ranges: &[(usize, usize)], at: usize| {
747 let idx = ranges.partition_point(|&(start, _)| start <= at);
748 idx.checked_sub(1).map(|i| ranges[i]).filter(|&(_, end)| at < end)
749 };
750 let mut text_resume: Vec<Option<usize>> = vec![None; content_lines.len()];
751 let mut outside_elements = vec![false; content_lines.len()];
752 for (i, line) in content_lines.iter().enumerate() {
753 if i < front_matter_end {
755 text_resume[i] = Some(line.len());
756 continue;
757 }
758 let at = line_offsets[i] + (line.len() - line.trim_start().len());
759 if let Some((_, text_end)) = containing(&comments, at) {
760 text_resume[i] = Some(text_end.min(line_offsets[i] + line.len()) - line_offsets[i]);
761 } else if let Some((_, text_end)) = containing(&code_span_ranges, at) {
762 let column = text_end.min(line_offsets[i] + line.len()) - line_offsets[i];
766 let closes = line
767 .get(column..)
768 .is_some_and(|rest| rest.trim_start().starts_with("</"));
769 text_resume[i] = Some(if closes { column } else { line.len() });
770 }
771 outside_elements[i] = containing(&indented, at).is_some();
772 }
773 flavor_detection::TagHiding {
774 text_resume,
775 outside_elements,
776 }
777 } else {
778 flavor_detection::TagHiding {
779 text_resume: Vec::new(),
780 outside_elements: Vec::new(),
781 }
782 };
783 let containers = profile_section!(
784 "Container lines",
785 profile,
786 flavor_detection::detect_container_lines(&content_lines, &in_fenced_code, &tag_hiding, flavor)
787 );
788
789 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
798 .iter()
799 .flat_map(|detail| {
800 if detail.is_fenced {
801 return vec![(detail.start, detail.end)];
802 }
803 let start_line = line_offsets
804 .partition_point(|&offset| offset <= detail.start)
805 .saturating_sub(1);
806 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
807 containers
808 .code_line_spans_in(start_line..end_line)
809 .into_iter()
810 .map(|span| {
811 let start = line_offsets[span.start].max(detail.start);
812 let end = line_offsets
813 .get(span.end)
814 .copied()
815 .unwrap_or(content.len())
816 .min(detail.end);
817 (start, end)
818 })
819 .collect()
820 })
821 .collect();
822 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
828 let html_comment_scan = profile_section!(
829 "HTML comment ranges",
830 profile,
831 crate::utils::skip_context::scan_html_comments(
832 content,
833 &code_span_ranges,
834 &comment_code_block_ranges,
835 body_start
836 )
837 );
838 let mut html_comment_ranges = html_comment_scan.ranges;
839 let unterminated_html_comment = html_comment_scan.unterminated;
840
841 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
845 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() || !content.contains(":::") {
846 Vec::new()
847 } else {
848 crate::utils::mkdocstrings_refs::autodoc_block_ranges_in_lines(&content_lines, flavor)
849 }
850 });
851
852 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
854 if flavor.is_pandoc_compatible() {
855 crate::utils::pandoc::detect_div_block_ranges(content)
856 } else {
857 Vec::new()
858 }
859 });
860
861 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
863 if flavor == MarkdownFlavor::MkDocs {
864 crate::utils::pymdown_blocks::detect_block_ranges(content)
865 } else {
866 Vec::new()
867 }
868 });
869
870 let skip_ranges = SkipByteRanges {
873 front_matter_end,
874 html_comment_ranges: &html_comment_ranges,
875 autodoc_ranges: &autodoc_ranges,
876 pandoc_div_ranges: &pandoc_div_ranges,
877 pymdown_block_ranges: &pymdown_block_ranges,
878 };
879 let (mut lines, emphasis_spans) = profile_section!(
880 "Basic line info",
881 profile,
882 line_computation::compute_basic_line_info(
883 content,
884 &content_lines,
885 &line_offsets,
886 &code_blocks,
887 flavor,
888 &skip_ranges,
889 pending_lines,
890 )
891 );
892
893 profile_section!(
895 "HTML blocks",
896 profile,
897 heading_detection::detect_html_blocks(content, &mut lines)
898 );
899
900 profile_section!(
902 "ESM blocks",
903 profile,
904 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
905 );
906
907 profile_section!(
909 "JSX block detection",
910 profile,
911 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
912 );
913
914 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
916 "JSX/MDX detection",
917 profile,
918 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
919 );
920
921 profile_section!(
926 "Markdown-in-HTML blocks",
927 profile,
928 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
929 );
930
931 let mdx_context = if flavor == MarkdownFlavor::MDX {
932 mdx::MdxContext::parse(content, &lines)
933 } else {
934 None
935 };
936 let mdx_esm_ranges = mdx_context.as_ref().map_or_else(Vec::new, |mdx| mdx.esm.clone());
937 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
938 mdx.apply_lines(&mut lines);
939 code_blocks.clone_from(&mdx.code_blocks);
940 code_span_ranges.clone_from(&mdx.code_spans);
941 (mdx.expressions.clone(), mdx.comments.clone())
942 } else {
943 (jsx_expression_ranges, mdx_comment_ranges)
944 };
945
946 if !flavor.is_pandoc_compatible() {
954 let mut spans = code_span_ranges.iter().peekable();
957 for (line, &offset) in lines.iter_mut().zip(&line_offsets) {
958 if line.is_div_marker {
959 let end = offset + content[offset..].find('\n').unwrap_or(content.len() - offset);
960 while spans.peek().is_some_and(|&&(_, stop)| stop <= end) {
961 spans.next();
962 }
963 line.is_div_marker = spans.peek().is_none_or(|&&(start, _)| start >= end);
964 }
965 }
966 }
967
968 let mut colon_definition_lines: Vec<(usize, usize)> = Vec::new();
973 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
974 for detail in &parse_result.colon_definitions {
975 let definition_line = line_of(detail.definition_start);
976 if flavor.is_pandoc_compatible() && lines[definition_line - 1].is_div_marker {
977 continue;
978 }
979 let (start_line, end_line) = (
980 line_of(detail.start),
981 line_of(detail.end.saturating_sub(1).max(detail.start)),
982 );
983 match colon_definition_lines.last_mut() {
984 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
985 _ => colon_definition_lines.push((start_line, end_line)),
986 }
987 }
988
989 profile_section!(
991 "MkDocs constructs",
992 profile,
993 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
994 );
995
996 let footnote_code_blocks = profile_section!(
1002 "Footnote definitions",
1003 profile,
1004 detect_footnote_definitions(content, &mut lines, &line_offsets)
1005 );
1006
1007 {
1010 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1011 for &(start, end) in &code_blocks {
1012 let start_line = line_offsets
1013 .partition_point(|&offset| offset <= start)
1014 .saturating_sub(1);
1015 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1016
1017 let mut sub_start: Option<usize> = None;
1018 for (i, &offset) in line_offsets[start_line..end_line]
1019 .iter()
1020 .enumerate()
1021 .map(|(j, o)| (j + start_line, o))
1022 {
1023 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1024 if is_real_code && sub_start.is_none() {
1025 let byte_start = if i == start_line { start } else { offset };
1026 sub_start = Some(byte_start);
1027 } else if !is_real_code && sub_start.is_some() {
1028 new_code_blocks.push((sub_start.unwrap(), offset));
1029 sub_start = None;
1030 }
1031 }
1032 if let Some(s) = sub_start {
1033 new_code_blocks.push((s, end));
1034 }
1035 }
1036 code_blocks = new_code_blocks;
1037 }
1038
1039 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
1047 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
1048 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1049 for &(start, end) in &code_blocks {
1050 let start_line = line_offsets
1051 .partition_point(|&offset| offset <= start)
1052 .saturating_sub(1);
1053 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1054
1055 let mut sub_start: Option<usize> = None;
1057 for (i, &offset) in line_offsets[start_line..end_line]
1058 .iter()
1059 .enumerate()
1060 .map(|(j, o)| (j + start_line, o))
1061 {
1062 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1063 if is_real_code && sub_start.is_none() {
1064 let byte_start = if i == start_line { start } else { offset };
1065 sub_start = Some(byte_start);
1066 } else if !is_real_code && sub_start.is_some() {
1067 new_code_blocks.push((sub_start.unwrap(), offset));
1068 sub_start = None;
1069 }
1070 }
1071 if let Some(s) = sub_start {
1072 new_code_blocks.push((s, end));
1073 }
1074 }
1075 code_blocks = new_code_blocks;
1076 }
1077
1078 if flavor.supports_jsx() {
1082 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1083 for &(start, end) in &code_blocks {
1084 let start_line = line_offsets
1085 .partition_point(|&offset| offset <= start)
1086 .saturating_sub(1);
1087 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1088
1089 let mut sub_start: Option<usize> = None;
1090 for (i, &offset) in line_offsets[start_line..end_line]
1091 .iter()
1092 .enumerate()
1093 .map(|(j, o)| (j + start_line, o))
1094 {
1095 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1096 if is_real_code && sub_start.is_none() {
1097 let byte_start = if i == start_line { start } else { offset };
1098 sub_start = Some(byte_start);
1099 } else if !is_real_code && sub_start.is_some() {
1100 new_code_blocks.push((sub_start.unwrap(), offset));
1101 sub_start = None;
1102 }
1103 }
1104 if let Some(s) = sub_start {
1105 new_code_blocks.push((s, end));
1106 }
1107 }
1108 code_blocks = new_code_blocks;
1109
1110 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
1117 let mut run: Option<(usize, usize)> = None;
1118 for line in &lines {
1119 if line.in_jsx_block && line.in_code_block {
1120 let line_end = line.byte_offset + line.byte_len;
1121 match &mut run {
1122 Some((_, end)) => *end = line_end,
1123 None => run = Some((line.byte_offset, line_end)),
1124 }
1125 } else if let Some(r) = run.take() {
1126 jsx_fence_ranges.push(r);
1127 }
1128 }
1129 if let Some(r) = run.take() {
1130 jsx_fence_ranges.push(r);
1131 }
1132 if !jsx_fence_ranges.is_empty() {
1133 code_blocks.extend(jsx_fence_ranges);
1134 code_blocks.sort_by_key(|&(start, _)| start);
1135 }
1136 }
1137
1138 let colon_fence_details = profile_section!(
1141 "Azure colon fence detection",
1142 profile,
1143 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
1144 );
1145 if !colon_fence_details.is_empty() {
1146 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
1147 code_blocks.sort_by_key(|&(start, _)| start);
1148 }
1149
1150 let myst_directive_ranges = profile_section!(
1153 "MyST colon directives",
1154 profile,
1155 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
1156 );
1157
1158 let myst_comment_ranges = profile_section!(
1160 "MyST comments",
1161 profile,
1162 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
1163 );
1164
1165 profile_section!(
1168 "MyST backtick directives",
1169 profile,
1170 flavor_detection::detect_myst_backtick_directives(
1171 content,
1172 &mut lines,
1173 flavor,
1174 &code_block_details,
1175 &line_offsets
1176 )
1177 );
1178
1179 if flavor.supports_myst_directives() {
1182 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1183 for &(start, end) in &code_blocks {
1184 let start_line = line_offsets
1185 .partition_point(|&offset| offset <= start)
1186 .saturating_sub(1);
1187 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1188
1189 let mut sub_start: Option<usize> = None;
1190 for (i, &offset) in line_offsets[start_line..end_line]
1191 .iter()
1192 .enumerate()
1193 .map(|(j, o)| (j + start_line, o))
1194 {
1195 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1196 if is_real_code && sub_start.is_none() {
1197 let byte_start = if i == start_line { start } else { offset };
1198 sub_start = Some(byte_start);
1199 } else if !is_real_code && sub_start.is_some() {
1200 new_code_blocks.push((sub_start.unwrap(), offset));
1201 sub_start = None;
1202 }
1203 }
1204 if let Some(s) = sub_start {
1205 new_code_blocks.push((s, end));
1206 }
1207 }
1208 code_blocks = new_code_blocks;
1209 }
1210
1211 profile_section!(
1213 "Kramdown constructs",
1214 profile,
1215 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
1216 );
1217
1218 for line in &mut lines {
1223 if line.in_kramdown_extension_block {
1224 line.list_item = None;
1225 line.is_horizontal_rule = false;
1226 line.blockquote = None;
1227 line.is_kramdown_block_ial = false;
1228 }
1229 }
1230
1231 let obsidian_comment_scan = profile_section!(
1233 "Obsidian comments",
1234 profile,
1235 flavor_detection::detect_obsidian_comments(
1236 content,
1237 &mut lines,
1238 flavor,
1239 &code_span_ranges,
1240 &html_comment_ranges,
1241 body_start
1242 )
1243 );
1244 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
1245 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
1246
1247 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
1252 unterminated_html_comment,
1253 &obsidian_comment_ranges,
1254 content,
1255 &code_span_ranges,
1256 &comment_code_block_ranges,
1257 body_start,
1258 );
1259
1260 if let Some(range) = unterminated_html_comment.and_then(|opener| {
1273 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
1274 .or_else(|| container_comment_range(opener, &containers, &lines, content))
1275 }) {
1276 html_comment_ranges.push(range);
1279
1280 for line in &mut lines {
1286 let text = line.content(content);
1287 let content_start = line.byte_offset + line.indent;
1288 let content_end = line.byte_offset + text.trim_end().len();
1289 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_sorted_html_comment(
1290 &html_comment_ranges,
1291 content_start,
1292 content_end,
1293 );
1294 line.in_obsidian_comment = false;
1295 }
1296
1297 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
1308 content,
1309 &mut lines,
1310 flavor,
1311 &code_span_ranges,
1312 &html_comment_ranges,
1313 body_start,
1314 );
1315 obsidian_comment_ranges = obsidian_rescan.ranges;
1316 unterminated_obsidian_comment = obsidian_rescan.unterminated;
1317 }
1318
1319 let myst_role_ranges = profile_section!(
1321 "MyST roles",
1322 profile,
1323 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
1324 );
1325
1326 let mut pulldown_result = profile_section!(
1330 "Links, images & link ranges",
1331 profile,
1332 if let Some(mdx) = &mdx_context {
1333 pending_links.finish_mdx(mdx, &lines)
1334 } else {
1335 pending_links.finish(&lines, &code_blocks, flavor, &html_comment_ranges)
1336 }
1337 );
1338
1339 heading_detection::mark_parser_html_blocks(content, &mut lines, &html_blocks, flavor);
1340 let (html_block_ranges, html_block_lines) = heading_detection::parser_html_blocks(&lines, &html_blocks, flavor);
1341
1342 profile_section!(
1345 "MyST targets",
1346 profile,
1347 flavor_detection::detect_myst_targets(content, &mut lines, flavor, &html_block_lines)
1348 );
1349
1350 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
1352 let heading_detection::HeadingAnalysis {
1353 mut blockquote_headings,
1354 lazy_table_text,
1355 } = profile_section!(
1356 "Headings & blockquotes",
1357 profile,
1358 heading_detection::detect_headings_and_blockquotes(
1359 &content_lines,
1360 &mut lines,
1361 flavor,
1362 &html_comment_ranges,
1363 &html_blocks,
1364 &code_blocks,
1365 &code_span_ranges,
1366 &pulldown_result.link_byte_ranges,
1367 front_matter_end,
1368 mdx_flow_lines.as_deref(),
1369 mdx_context
1370 .as_ref()
1371 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1372 )
1373 );
1374
1375 for line in &mut lines {
1377 if line.in_kramdown_extension_block {
1378 line.heading = None;
1379 }
1380 }
1381 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
1382 if line.in_kramdown_extension_block {
1383 *heading = None;
1384 }
1385 }
1386
1387 for range in &html_block_lines {
1404 for line in &mut lines[range.clone()] {
1405 if !line.in_mkdocs_html_markdown {
1406 line.is_horizontal_rule = false;
1407 }
1408 }
1409 }
1410 for &(start, end) in &footnote_code_blocks {
1411 let spanned = line_computation::spanned_lines(&lines, start, end);
1412 for line in &mut lines[spanned] {
1413 line.is_horizontal_rule = false;
1414 }
1415 }
1416 for line in &mut lines {
1417 if line.is_horizontal_rule
1418 && (line.in_code_block
1419 || line.in_html_block
1420 || line.in_html_comment
1421 || line.in_math_block
1422 || line.in_mdx_comment
1423 || line.in_obsidian_comment)
1424 {
1425 line.is_horizontal_rule = false;
1426 }
1427 }
1428
1429 let mut inline_code_ranges = code_span_ranges.clone();
1432
1433 if flavor == MarkdownFlavor::MkDocs {
1437 let extra = profile_section!(
1438 "MkDocs code spans",
1439 profile,
1440 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
1441 );
1442 if !extra.is_empty() {
1443 inline_code_ranges.extend(extra);
1444 inline_code_ranges.sort_by_key(|span| span.0);
1445 }
1446 }
1447
1448 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
1453 let extra = profile_section!(
1454 "MDX JSX code spans",
1455 profile,
1456 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
1457 );
1458 if !extra.is_empty() {
1459 inline_code_ranges.extend(extra);
1460 inline_code_ranges.sort_by_key(|span| span.0);
1461 }
1462 }
1463
1464 for &(start, end) in &inline_code_ranges {
1467 let first = lines.partition_point(|line| line.byte_offset <= start);
1468 let last = lines.partition_point(|line| line.byte_offset <= end);
1469 for line in &mut lines[first..last] {
1470 line.in_code_span_continuation = true;
1471 }
1472 }
1473
1474 let mut parsed_link_ranges = std::mem::take(&mut pulldown_result.link_byte_ranges);
1477 parsed_link_ranges.sort_unstable();
1478
1479 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1480 let list_blocks: Vec<_> = list_blocks
1481 .into_iter()
1482 .filter(|block| {
1483 !lines
1484 .get(block.start_line - 1)
1485 .is_some_and(|line| line.in_kramdown_extension_block)
1486 })
1487 .collect();
1488 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks, &inline_code_ranges);
1489 let code_spans_without_math_cache = OnceLock::new();
1490 let structure = NativePreparedStructure {
1491 content,
1492 flavor,
1493 line_offsets,
1494 code_blocks,
1495 code_block_details,
1496 lines,
1497 list_blocks,
1498 html_block_lines,
1499 inline_code_ranges,
1500 code_span_ranges,
1501 inline_config,
1502 front_matter_end,
1503 code_spans_without_math_cache,
1504
1505 parser_has_math,
1506 };
1507 let finish = move |structure: NativePreparedStructure<'a>| {
1511 let NativePreparedStructure {
1512 content,
1513 flavor,
1514 line_offsets,
1515 code_blocks,
1516 code_block_details,
1517 mut lines,
1518 list_blocks,
1519 html_block_lines,
1520 inline_code_ranges,
1521 code_span_ranges,
1522 inline_config,
1523 front_matter_end,
1524 code_spans_without_math_cache,
1525
1526 parser_has_math,
1527 } = structure;
1528 let link_parser::FinalizedLinks {
1530 links,
1531 images,
1532 unresolved_links,
1533 unresolved_images,
1534 broken_links,
1535 footnote_refs,
1536 } = profile_section!(
1537 "Links & images finalize",
1538 profile,
1539 link_parser::finalize_links_and_images(
1540 content,
1541 &lines,
1542 flavor,
1543 &link_parser::LinkExclusions {
1544 code_blocks: &code_blocks,
1545 code_spans: &inline_code_ranges,
1546 html_comment_ranges: &html_comment_ranges,
1547 mdx: mdx_context.as_ref(),
1548 },
1549 pulldown_result,
1550 )
1551 );
1552
1553 let reference_defs = profile_section!("Reference defs", profile, {
1554 if let Some(mdx) = &mdx_context {
1555 mdx.reference_defs(content)
1556 } else {
1557 link_parser::parse_reference_defs(content, &lines, flavor, &parser_definitions)
1558 }
1559 });
1560
1561 let mut lazy_text = if lines.iter().zip(&content_lines).any(|(line, text)| {
1565 text.contains('|')
1566 && (line.visual_indent >= 4
1567 || line
1568 .blockquote
1569 .as_deref()
1570 .is_some_and(|quote| heading_detection::prefix_may_hide_block("e.prefix)))
1571 }) {
1572 let mut definition_lines = vec![false; if mdx_context.is_some() { lines.len() } else { 0 }];
1575 let definitions = if mdx_context.is_some() {
1576 for definition in &reference_defs {
1577 let first = line_offsets.partition_point(|&offset| offset <= definition.byte_offset);
1578 let end = line_offsets
1579 .partition_point(|&offset| offset < definition.byte_end.max(definition.byte_offset + 1));
1580 for flag in
1581 &mut definition_lines[first.saturating_sub(1).min(lines.len())..end.min(lines.len())]
1582 {
1583 *flag = true;
1584 }
1585 }
1586 Some(heading_detection::Definitions::Lines(&definition_lines))
1587 } else {
1588 text_start_lines
1589 .as_deref()
1590 .map(heading_detection::Definitions::TextStarts)
1591 };
1592 if reference_defs.is_empty()
1597 && text_start_lines.is_none()
1598 && let Some(lazy_text) = lazy_table_text
1599 {
1600 lazy_text
1601 } else {
1602 heading_detection::indented_text_lines(
1603 &content_lines,
1604 &lines,
1605 flavor,
1606 &html_blocks,
1607 &code_blocks,
1608 &code_span_ranges,
1609 mdx_flow_lines.as_deref(),
1610 mdx_context
1611 .as_ref()
1612 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1613 definitions,
1614 )
1615 }
1616 } else {
1617 Vec::new()
1618 };
1619 if flavor == MarkdownFlavor::MDG {
1620 for (flag, line) in lazy_text.iter_mut().zip(&lines) {
1623 *flag &= line.blockquote.is_some();
1624 }
1625 }
1626
1627 let table_blocks = profile_section!(
1629 "Table blocks",
1630 profile,
1631 if char_frequency.pipe_count == 0 {
1632 Vec::new()
1633 } else {
1634 crate::utils::table_utils::TableUtils::find_table_blocks_with_layout(
1635 &content_lines,
1636 &line_offsets,
1637 &code_blocks,
1638 &inline_code_ranges,
1639 &html_comment_ranges,
1640 flavor,
1641 &lazy_text,
1642 &list_items,
1643 )
1644 }
1645 );
1646 let list_item_parent: Vec<Option<usize>> = {
1647 let mut open: Vec<usize> = Vec::new();
1648 list_items
1649 .iter()
1650 .enumerate()
1651 .map(|(index, &(start, _, _))| {
1652 while open.last().is_some_and(|&top| list_items[top].1 <= start) {
1653 open.pop();
1654 }
1655 let parent = open.last().copied();
1656 open.push(index);
1657 parent
1658 })
1659 .collect()
1660 };
1661 let table_blocks: Vec<_> = table_blocks
1665 .into_iter()
1666 .filter(|block| flavor == MarkdownFlavor::MDX || !lines[block.header_line].in_markdown_span)
1667 .collect();
1668 let table_blocks = table_confirm::confirm_table_blocks(
1669 table_blocks,
1670 &table_confirm::Evidence {
1671 content,
1672 lines: &lines,
1673 line_offsets: &line_offsets,
1674 flavor,
1675 table_ranges: &table_ranges,
1676 top_level_blocks: &top_level_blocks,
1677 rejected_definitions: &rejected_definition_ranges,
1678 list_items: &list_items,
1679 list_item_parent: &list_item_parent,
1680 parser_code_blocks: &parser_code_blocks,
1681 native_tables: mdx_context.as_ref().map_or(&[][..], |mdx| &mdx.tables[..]),
1682 html_above: std::cell::OnceCell::new(),
1683 opener_body: std::cell::OnceCell::new(),
1684 rejected_definition_reach: std::cell::OnceCell::new(),
1685 },
1686 );
1687
1688 let links = links
1691 .into_iter()
1692 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1693 .collect::<Vec<_>>();
1694 let images = images
1695 .into_iter()
1696 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1697 .collect::<Vec<_>>();
1698 let unresolved_links = unresolved_links
1699 .into_iter()
1700 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1701 .collect::<Vec<_>>();
1702 let unresolved_images = unresolved_images
1703 .into_iter()
1704 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1705 .collect::<Vec<_>>();
1706 let broken_links = broken_links
1707 .into_iter()
1708 .filter(|bl| {
1709 let line_idx = line_offsets
1711 .partition_point(|&offset| offset <= bl.span.start)
1712 .saturating_sub(1);
1713 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1714 })
1715 .collect::<Vec<_>>();
1716 let footnote_refs = footnote_refs
1717 .into_iter()
1718 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1719 .collect::<Vec<_>>();
1720 let reference_defs = reference_defs
1721 .into_iter()
1722 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1723 .collect::<Vec<_>>();
1724 let table_blocks = table_blocks
1725 .into_iter()
1726 .filter(|block| {
1727 !lines
1729 .get(block.start_line)
1730 .is_some_and(|l| l.in_kramdown_extension_block)
1731 })
1732 .collect::<Vec<_>>();
1733 let emphasis_spans = emphasis_spans
1734 .into_iter()
1735 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1736 .collect::<Vec<_>>();
1737
1738 for block in &list_blocks {
1742 for line_num in block.start_line..=block.end_line {
1744 if let Some(li) = lines.get_mut(line_num - 1) {
1745 li.in_list_block = true;
1746 }
1747 }
1748 }
1749 for block in &table_blocks {
1750 for idx in block.start_line..=block.end_line {
1752 if let Some(li) = lines.get_mut(idx) {
1753 li.in_table_block = true;
1754 }
1755 }
1756 }
1757
1758 let definition_candidates = link_parser::definition_candidates(
1762 content,
1763 if mdx_context.is_some() {
1764 Vec::new()
1765 } else {
1766 parser_definitions
1767 },
1768 &reference_defs,
1769 );
1770
1771 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1773 .iter()
1774 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1775 (Some(start), Some(end)) => Some((start, end)),
1776 _ => None,
1777 })
1778 .collect();
1779
1780 let jinja_ranges = profile_section!(
1782 "Jinja ranges",
1783 profile,
1784 crate::utils::jinja_utils::find_jinja_ranges(content)
1785 );
1786
1787 let citation_ranges = profile_section!("Citation ranges", profile, {
1789 if flavor.is_pandoc_compatible() {
1790 crate::utils::pandoc::find_citation_ranges(content)
1791 } else {
1792 Vec::new()
1793 }
1794 });
1795
1796 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1798 if flavor.is_pandoc_compatible() {
1799 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1800 } else {
1801 Vec::new()
1802 }
1803 });
1804
1805 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1807 if flavor.is_pandoc_compatible() {
1808 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1809 } else {
1810 std::collections::HashSet::new()
1811 }
1812 });
1813
1814 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1816 if flavor.is_pandoc_compatible() {
1817 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1818 } else {
1819 Vec::new()
1820 }
1821 });
1822
1823 let example_reference_ranges = profile_section!("Example references", profile, {
1825 if flavor.is_pandoc_compatible() {
1826 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1827 } else {
1828 Vec::new()
1829 }
1830 });
1831
1832 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1834 if flavor.is_pandoc_compatible() {
1835 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1836 } else {
1837 Vec::new()
1838 }
1839 });
1840
1841 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1843 if flavor.is_pandoc_compatible() {
1844 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1845 } else {
1846 Vec::new()
1847 }
1848 });
1849
1850 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1852 if flavor.is_pandoc_compatible() {
1853 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1854 } else {
1855 Vec::new()
1856 }
1857 });
1858
1859 let line_block_ranges = profile_section!("Line block ranges", profile, {
1861 if flavor.is_pandoc_compatible() {
1862 crate::utils::pandoc::detect_line_block_ranges(content)
1863 } else {
1864 Vec::new()
1865 }
1866 });
1867
1868 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1870 if flavor.is_pandoc_compatible() {
1871 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1872 } else {
1873 Vec::new()
1874 }
1875 });
1876
1877 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1879 if flavor.is_pandoc_compatible() {
1880 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1881 } else {
1882 Vec::new()
1883 }
1884 });
1885
1886 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1888 if flavor.is_pandoc_compatible() {
1889 crate::utils::pandoc::detect_grid_table_ranges(content)
1890 } else {
1891 Vec::new()
1892 }
1893 });
1894
1895 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1897 if flavor.is_pandoc_compatible() {
1898 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1899 } else {
1900 Vec::new()
1901 }
1902 });
1903
1904 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1906 crate::utils::shortcode_utils::shortcode_ranges(content).collect()
1907 });
1908
1909 let ctx = Self {
1910 content,
1911
1912 native_list_spacing_plan: OnceLock::new(),
1913
1914 list_guard_parse: OnceLock::new(),
1915
1916 parser_blocks: parser_blocks.map_or_else(OnceLock::new, OnceLock::from),
1917
1918 parse_handoff: std::sync::Mutex::new(None),
1919 merge_conflicts: OnceLock::new(),
1920 content_lines,
1921 line_candidates,
1922 lexical_emphasis: OnceLock::new(),
1923 document_lines: OnceLock::new(),
1924 line_offsets,
1925 code_blocks,
1926 code_block_details,
1927 strong_spans,
1928 line_to_list,
1929 list_start_values,
1930 definition_lists,
1931 colon_definition_lines,
1932 commonmark_ordered_lists_cache: OnceLock::new(),
1933 lines,
1934 blockquote_headings,
1935 document_anchors_cache: OnceLock::new(),
1936 link_destinations_cache: OnceLock::new(),
1937 links,
1938 rendered_link_ends_cache: OnceLock::new(),
1939 parsed_link_ranges,
1940 images,
1941 unresolved_links,
1942 unresolved_images,
1943 broken_links,
1944 footnote_refs,
1945 reference_defs,
1946 reference_defs_map: OnceLock::new(),
1947 definition_candidates,
1948 body_definitions: OnceLock::new(),
1949 inline_code_ranges,
1950 code_spans_cache: OnceLock::new(),
1951 source_code_spans: OnceLock::new(),
1952 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1954 math_byte_ranges_cache: OnceLock::new(), parser_has_math,
1957 code_spans_without_math_cache,
1958 list_blocks,
1959 char_frequency,
1960 html_tags_cache: OnceLock::new(),
1961 jsx_component_tags_cache: OnceLock::new(),
1962 source_emphasis: emphasis_spans,
1963 emphasis_spans_cache: OnceLock::new(),
1964 bare_urls_cache: OnceLock::new(),
1965 has_mixed_list_nesting_cache: OnceLock::new(),
1966 html_comment_ranges,
1967 html_block_lines,
1968 html_block_ranges,
1969 table_blocks,
1970 parser_table_ranges: table_ranges,
1971 jinja_ranges,
1972 jinja_string_ranges_cache: OnceLock::new(),
1973 flavor,
1974 source_file,
1975 link_target_policy: None,
1976 invalid_utf8: None,
1977 jsx_expression_ranges,
1978 mdx_esm_ranges,
1979 mdx_comment_ranges,
1980 citation_ranges,
1981 pandoc_div_ranges,
1982 colon_fence_details,
1983 inline_footnote_ranges,
1984 pandoc_header_slugs,
1985 example_list_marker_ranges,
1986 example_reference_ranges,
1987 sub_super_ranges,
1988 inline_code_attr_ranges,
1989 bracketed_span_ranges,
1990 line_block_ranges,
1991 pipe_table_caption_ranges,
1992 pandoc_metadata_ranges,
1993 grid_table_ranges,
1994 multi_line_table_ranges,
1995 shortcode_ranges,
1996 link_title_ranges,
1997 code_span_byte_ranges: code_span_ranges,
1998 inline_config,
1999 obsidian_comment_ranges,
2000 unterminated_html_comment,
2001 unterminated_obsidian_comment,
2002 lazy_cont_lines_cache: OnceLock::new(),
2003 myst_directive_ranges,
2004 myst_comment_ranges,
2005 myst_role_ranges,
2006 front_matter_end,
2007 };
2008 if let Some(mdx) = mdx_context {
2009 let (html_tags, jsx_component_tags) = mdx.html_tags(content, &ctx.lines);
2010 let _ = ctx.html_tags_cache.set(Arc::new(ctx.filter_kramdown_tags(html_tags)));
2011 let _ = ctx
2012 .jsx_component_tags_cache
2013 .set(Arc::new(ctx.filter_kramdown_tags(jsx_component_tags)));
2014 }
2015 ctx
2016 };
2017 (structure, finish)
2018 }
2019
2020 pub fn front_matter_end_line(&self) -> usize {
2025 self.front_matter_end
2026 }
2027
2028 #[inline]
2031 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
2032 let idx = ranges.partition_point(|&(start, _)| start <= pos);
2034 idx > 0 && pos < ranges[idx - 1].1
2036 }
2037
2038 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
2040 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
2041 }
2042
2043 pub fn is_in_code_span_byte_with_or_without_math(&self, pos: usize) -> bool {
2049 self.native_list_source().is_in_code_span_byte_with_or_without_math(pos)
2050 }
2051
2052 pub fn is_code_span_text_byte(&self, pos: usize) -> bool {
2058 self.is_byte_offset_in_code_span(pos) || self.is_in_code_span_byte_with_or_without_math(pos)
2059 }
2060
2061 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
2066 let line = &self.lines[line_number - 1];
2067 heading_detection::ends_with_hard_break(
2068 line.content(self.content),
2069 line.byte_offset,
2070 &self.code_span_byte_ranges,
2071 )
2072 }
2073
2074 pub fn is_in_link(&self, pos: usize) -> bool {
2076 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
2077 }
2078
2079 pub fn is_in_bare_url(&self, pos: usize) -> bool {
2081 let bare_urls = self.bare_urls();
2082 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
2084 idx > 0 && pos < bare_urls[idx - 1].byte_end
2085 }
2086
2087 pub fn inline_config(&self) -> &InlineConfig {
2089 &self.inline_config
2090 }
2091
2092 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
2097 &self.colon_fence_details
2098 }
2099
2100 pub fn raw_lines(&self) -> &[&'a str] {
2104 &self.content_lines
2105 }
2106
2107 pub(crate) fn candidate_line_indices(&self, kind: LineCandidateKind) -> &[usize] {
2110 self.line_candidates.lines(kind)
2111 }
2112
2113 pub(crate) fn lexical_emphasis(&self) -> impl Iterator<Item = EmphasisLine<'_>> {
2116 self.lexical_emphasis
2117 .get_or_init(|| emphasis::collect(self))
2118 .lines(self.content)
2119 }
2120
2121 fn document_lines(&self) -> &line_facts::DocumentLines {
2122 self.document_lines
2123 .get_or_init(|| line_facts::DocumentLines::new(&self.lines, &self.blockquote_headings))
2124 }
2125
2126 pub(crate) fn line_context_masks(&self) -> &[LineContextMask] {
2127 &self.document_lines().contexts
2128 }
2129
2130 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
2135 self.inline_config.is_rule_disabled(rule_name, line_number)
2136 }
2137
2138 pub(crate) fn inline_code_ranges(&self) -> &[(usize, usize)] {
2140 &self.inline_code_ranges
2141 }
2142
2143 pub(crate) fn source_code_spans(&self) -> &[SourceCodeSpan] {
2144 self.source_code_spans.get_or_init(|| {
2145 element_parsers::build_code_spans_from_ranges(self.content, &self.lines, &self.inline_code_ranges)
2146 })
2147 }
2148
2149 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
2151 Arc::clone(self.code_spans_cache.get_or_init(|| {
2152 Arc::new(
2153 self.source_code_spans()
2154 .iter()
2155 .map(|span| span.materialize(self.content))
2156 .collect(),
2157 )
2158 }))
2159 }
2160
2161 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
2165 self.math_byte_ranges_cache
2166 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
2167 }
2168
2169 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
2171 Arc::clone(
2172 self.math_spans_cache
2173 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
2174 )
2175 }
2176
2177 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
2179 self.bracket_math_cache
2180 .get_or_init(|| bracket_math::parse(self.content, &self.lines, self.source_code_spans(), &self.list_blocks))
2181 }
2182
2183 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
2185 let math_spans = self.math_spans();
2186 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
2188 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
2189 }
2190
2191 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2193 &self.html_comment_ranges
2194 }
2195
2196 pub fn unterminated_html_comment(&self) -> Option<usize> {
2201 self.unterminated_html_comment
2202 }
2203
2204 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
2208 self.unterminated_obsidian_comment
2209 }
2210
2211 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
2215 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
2216 }
2217
2218 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
2223 if self.obsidian_comment_ranges.is_empty() {
2224 return false;
2225 }
2226
2227 let byte_pos = self.line_index().line_col_to_byte_range(line_num, col).start;
2229 self.is_in_obsidian_comment(byte_pos)
2230 }
2231
2232 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
2234 &self.myst_directive_ranges
2235 }
2236
2237 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
2239 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
2240 }
2241
2242 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
2244 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
2245 }
2246
2247 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
2254 if !self.flavor.supports_myst_directives() {
2255 return false;
2256 }
2257 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
2258 info.in_myst_directive
2259 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
2260 })
2261 }
2262
2263 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
2265 tags.into_iter()
2266 .filter(|tag| {
2267 !self
2268 .lines
2269 .get(tag.line - 1)
2270 .is_some_and(|l| l.in_kramdown_extension_block)
2271 })
2272 .collect()
2273 }
2274
2275 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
2282 Arc::clone(self.html_tags_shared())
2283 }
2284
2285 fn html_tags_shared(&self) -> &Arc<Vec<HtmlTag>> {
2286 self.html_tags_cache.get_or_init(|| {
2287 let (html_tags, jsx_component_tags) =
2288 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
2289 let _ = self
2291 .jsx_component_tags_cache
2292 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
2293 Arc::new(self.filter_kramdown_tags(html_tags))
2294 })
2295 }
2296
2297 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
2300 Arc::clone(self.jsx_component_tags_shared())
2301 }
2302
2303 fn jsx_component_tags_shared(&self) -> &Arc<Vec<HtmlTag>> {
2304 if let Some(cached) = self.jsx_component_tags_cache.get() {
2305 return cached;
2306 }
2307 let _ = self.html_tags_shared();
2309 self.jsx_component_tags_cache
2310 .get()
2311 .expect("HTML parse populates jsx_component_tags_cache")
2312 }
2313
2314 pub(crate) fn source_emphasis_spans(&self) -> &[SourceEmphasisSpan] {
2315 &self.source_emphasis
2316 }
2317
2318 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
2320 Arc::clone(self.emphasis_spans_cache.get_or_init(|| {
2321 Arc::new(
2322 self.source_emphasis
2323 .iter()
2324 .map(|span| span.materialize(self.content))
2325 .collect(),
2326 )
2327 }))
2328 }
2329
2330 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
2332 Arc::clone(self.bare_urls_cache.get_or_init(|| {
2333 Arc::new(element_parsers::parse_bare_urls(
2334 self.content,
2335 &self.lines,
2336 &self.code_blocks,
2337 ))
2338 }))
2339 }
2340
2341 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
2343 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
2344 Arc::new(element_parsers::detect_lazy_continuation_lines(
2345 self.content,
2346 &self.lines,
2347 &self.line_offsets,
2348 ))
2349 }))
2350 }
2351
2352 pub fn has_mixed_list_nesting(&self) -> bool {
2356 *self
2357 .has_mixed_list_nesting_cache
2358 .get_or_init(|| self.compute_mixed_list_nesting())
2359 }
2360
2361 fn compute_mixed_list_nesting(&self) -> bool {
2363 let mut stack: Vec<(usize, bool)> = Vec::new();
2368 let mut last_was_blank = false;
2369
2370 for line_info in &self.lines {
2371 if line_info.in_code_block
2373 || line_info.in_front_matter
2374 || line_info.in_mkdocstrings
2375 || line_info.in_html_comment
2376 || line_info.in_mdx_comment
2377 || line_info.in_esm_block
2378 {
2379 continue;
2380 }
2381
2382 if line_info.is_blank {
2384 last_was_blank = true;
2385 continue;
2386 }
2387
2388 if let Some(list_item) = &line_info.list_item {
2389 let current_pos = if list_item.marker_column == 1 {
2391 0
2392 } else {
2393 list_item.marker_column
2394 };
2395
2396 if last_was_blank && current_pos == 0 {
2398 stack.clear();
2399 }
2400 last_was_blank = false;
2401
2402 while let Some(&(pos, _)) = stack.last() {
2404 if pos >= current_pos {
2405 stack.pop();
2406 } else {
2407 break;
2408 }
2409 }
2410
2411 if let Some(&(_, parent_is_ordered)) = stack.last()
2413 && parent_is_ordered != list_item.is_ordered
2414 {
2415 return true; }
2417
2418 stack.push((current_pos, list_item.is_ordered));
2419 } else {
2420 last_was_blank = false;
2422 }
2423 }
2424
2425 false
2426 }
2427
2428 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
2434 native_structure::offset_to_line_col(self.content, &self.line_offsets, offset)
2435 }
2436
2437 pub(crate) fn line_number_at_offset(&self, offset: usize) -> usize {
2440 self.line_offsets.partition_point(|&start| start <= offset)
2441 }
2442
2443 pub fn html_block_range_at(&self, byte_offset: usize) -> Option<(usize, usize)> {
2446 let index = self
2447 .html_block_ranges
2448 .partition_point(|&(start, _)| start <= byte_offset);
2449 self.html_block_ranges
2450 .get(index.checked_sub(1)?)
2451 .copied()
2452 .filter(|&(_, end)| byte_offset < end)
2453 }
2454
2455 fn line_index(&self) -> crate::utils::range_utils::LineIndex<'_> {
2459 crate::utils::range_utils::LineIndex::for_positions(self.content, &self.line_offsets)
2460 }
2461
2462 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
2468 line_number
2469 .checked_sub(1)
2470 .and_then(|index| self.line_offsets.get(index).copied())
2471 }
2472
2473 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
2479 self.line_index().line_col_to_byte_range(line_number, column)
2480 }
2481
2482 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
2487 self.line_index()
2488 .line_col_to_byte_range_with_length(line_number, column, length)
2489 }
2490
2491 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
2494 self.line_index().whole_line_range(line_number)
2495 }
2496
2497 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
2502 self.line_index().line_text_range(line_number, start_column, end_column)
2503 }
2504
2505 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
2508 self.line_index().line_content_range(line_number)
2509 }
2510
2511 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
2513 self.line_index().multi_line_range(start_line, end_line)
2514 }
2515
2516 pub(crate) fn parsed_table_range_at_line(&self, line: usize) -> Option<(usize, usize)> {
2520 let span = self.line_content_byte_range(line);
2521 let index = self
2522 .parser_table_ranges
2523 .partition_point(|&(start, _)| start < span.start);
2524 self.parser_table_ranges
2525 .get(index)
2526 .copied()
2527 .filter(|&(start, _)| start < span.end)
2528 }
2529
2530 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
2532 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
2534 return true;
2535 }
2536
2537 self.is_byte_offset_in_code_span(pos)
2539 }
2540
2541 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
2543 if line_num > 0 {
2544 self.lines.get(line_num - 1)
2545 } else {
2546 None
2547 }
2548 }
2549
2550 pub(crate) fn document_anchors(&self) -> &anchors::DocumentAnchors {
2553 self.document_anchors_cache
2554 .get_or_init(|| anchors::DocumentAnchors::new(self))
2555 }
2556
2557 pub fn links(&self) -> &[ParsedLink<'a>] {
2561 &self.links
2562 }
2563
2564 pub fn unresolved_links(&self) -> &[ParsedLink<'a>] {
2569 &self.unresolved_links
2570 }
2571
2572 pub fn unresolved_link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2574 self.unresolved_links
2575 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
2576 .ok()
2577 .map(|index| &self.unresolved_links[index])
2578 }
2579
2580 #[inline]
2583 pub(crate) fn is_in_parsed_link(&self, pos: usize) -> bool {
2584 Self::binary_search_ranges(&self.parsed_link_ranges, pos)
2585 }
2586
2587 pub(crate) fn has_parsed_link_start_in(&self, start: usize, end: usize) -> bool {
2590 let index = self.parsed_link_ranges.partition_point(|&(open, _)| open < start);
2591 self.parsed_link_ranges.get(index).is_some_and(|&(open, _)| open < end)
2592 }
2593
2594 pub fn images(&self) -> &[ParsedImage<'a>] {
2597 &self.images
2598 }
2599
2600 pub fn unresolved_images(&self) -> &[ParsedImage<'a>] {
2602 &self.unresolved_images
2603 }
2604
2605 pub fn unresolved_image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2607 self.unresolved_images
2608 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
2609 .ok()
2610 .map(|index| &self.unresolved_images[index])
2611 }
2612
2613 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
2615 &self.broken_links
2616 }
2617
2618 pub fn footnote_references(&self) -> &[FootnoteRef] {
2620 &self.footnote_refs
2621 }
2622
2623 pub fn reference_definitions(&self) -> &[ReferenceDef] {
2625 &self.reference_defs
2626 }
2627
2628 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
2630 let start = self.links.partition_point(|link| link.line < line_number);
2631 let end = self.links.partition_point(|link| link.line <= line_number);
2632 &self.links[start..end]
2633 }
2634
2635 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
2637 let start = self.images.partition_point(|image| image.line < line_number);
2638 let end = self.images.partition_point(|image| image.line <= line_number);
2639 &self.images[start..end]
2640 }
2641
2642 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2644 self.links
2645 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
2646 .ok()
2647 .map(|index| &self.links[index])
2648 }
2649
2650 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2652 self.images
2653 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
2654 .ok()
2655 .map(|index| &self.images[index])
2656 }
2657
2658 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2660 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2661 self.links
2662 .get(index.checked_sub(1)?)
2663 .filter(|link| byte_offset < link.byte_end)
2664 }
2665
2666 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2668 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
2669 self.images
2670 .get(index.checked_sub(1)?)
2671 .filter(|image| byte_offset < image.byte_end)
2672 }
2673
2674 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
2676 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2677 &self.links[..end]
2678 }
2679
2680 pub(crate) fn is_in_unresolved_autoref_identifier(&self, byte_pos: usize) -> bool {
2687 use pulldown_cmark::LinkType;
2688 if self.flavor != MarkdownFlavor::MkDocs {
2689 return false;
2690 }
2691 let links = &self.unresolved_links;
2692 let end = links.partition_point(|link| link.byte_offset <= byte_pos);
2693 links[..end].iter().rev().take(8).any(|link| {
2694 if byte_pos >= link.byte_end {
2695 return false;
2696 }
2697 match link.link_type {
2698 LinkType::Reference | LinkType::ReferenceUnknown => {
2699 let source = &self.content[link.byte_offset..link.byte_end];
2700 if source.ends_with("[]") {
2703 return byte_pos < link.byte_end.saturating_sub(2);
2704 }
2705 link_parser::reference_label_start(source).is_some_and(|open| byte_pos > link.byte_offset + open)
2706 }
2707 LinkType::Collapsed | LinkType::CollapsedUnknown => byte_pos < link.byte_end.saturating_sub(2),
2708 _ => false,
2709 }
2710 })
2711 }
2712
2713 pub(crate) fn is_range_in_rendered_link(&self, range: &Range<usize>) -> bool {
2718 let count = self.links.partition_point(|link| link.byte_offset <= range.start);
2719 if count == 0 {
2720 return false;
2721 }
2722 let ends = self.rendered_link_ends_cache.get_or_init(|| {
2723 let mut end = 0;
2724 self.links
2725 .iter()
2726 .map(|link| {
2727 end = end.max(link.byte_end);
2728 end
2729 })
2730 .collect()
2731 });
2732 ends[count - 1] != 0 && ends[count - 1] >= range.end
2733 }
2734
2735 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
2742 let normalized_id = ref_id.to_lowercase();
2743 self.reference_defs_map
2744 .get_or_init(|| {
2745 let mut chosen: HashMap<String, (usize, bool)> = HashMap::with_capacity(self.reference_defs.len());
2746 for (idx, def) in self.reference_defs.iter().enumerate() {
2747 let in_body =
2748 self.is_body_markdown(def.line, link_parser::definition_label_start(self.content, def));
2749 match chosen.entry(def.id.to_lowercase()) {
2750 std::collections::hash_map::Entry::Vacant(entry) => {
2751 entry.insert((idx, in_body));
2752 }
2753 std::collections::hash_map::Entry::Occupied(mut entry) => {
2754 if in_body && !entry.get().1 {
2755 entry.insert((idx, true));
2756 }
2757 }
2758 }
2759 }
2760 chosen.into_iter().map(|(id, (idx, _))| (id, idx)).collect()
2761 })
2762 .get(&normalized_id)
2763 .map(|&index| &self.reference_defs[index])
2764 }
2765
2766 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
2768 self.reference_definition(ref_id)
2769 .map(|definition| definition.url.as_str())
2770 }
2771
2772 pub fn heading_container(&self, line_num: usize) -> HeadingContainer {
2780 self.line_info(line_num)
2781 .and_then(|line| line.heading_container)
2782 .unwrap_or(HeadingContainer::Unknown)
2783 }
2784
2785 pub fn is_in_list_block(&self, line_num: usize) -> bool {
2787 if line_num == 0 || line_num > self.lines.len() {
2788 return false;
2789 }
2790 self.lines[line_num - 1].in_list_block
2791 }
2792
2793 pub fn is_in_definition_list(&self, line_num: usize) -> bool {
2801 let lists = &self.definition_lists.outer;
2802 let idx = lists.partition_point(|&(start, _)| start <= line_num);
2803 idx > 0 && line_num <= lists[idx - 1].1
2804 }
2805
2806 pub fn is_definition_term(&self, line_num: usize) -> bool {
2808 let terms = &self.definition_lists.terms;
2809 let idx = terms.partition_point(|&(_, end)| end < line_num);
2810 terms.get(idx).is_some_and(|&(start, _)| start <= line_num)
2811 }
2812
2813 pub fn in_colon_definition(&self, line_num: usize) -> bool {
2821 let ranges = &self.colon_definition_lines;
2822 let idx = ranges.partition_point(|&(_, end)| end < line_num);
2823 ranges.get(idx).is_some_and(|&(start, _)| start <= line_num)
2824 }
2825
2826 pub fn definition_text_at(&self, line_num: usize) -> Option<&DefinitionText> {
2828 let texts = &self.definition_lists.texts;
2829 let idx = texts.partition_point(|text| text.end_line < line_num);
2830 texts.get(idx).filter(|text| text.start_line <= line_num)
2831 }
2832
2833 pub fn line_holds_html_block(&self, line_idx: usize) -> bool {
2838 self.native_list_source().line_holds_html_block(line_idx)
2839 }
2840
2841 pub fn is_in_html_block(&self, line_num: usize) -> bool {
2843 if line_num == 0 || line_num > self.lines.len() {
2844 return false;
2845 }
2846 self.lines[line_num - 1].in_html_block
2847 }
2848
2849 pub fn is_in_table_block(&self, line_num: usize) -> bool {
2855 if line_num == 0 || line_num > self.lines.len() {
2856 return false;
2857 }
2858 self.lines[line_num - 1].in_table_block
2859 }
2860
2861 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
2863 if line_num == 0 || line_num > self.lines.len() {
2864 return false;
2865 }
2866
2867 let col_0indexed = if col > 0 { col - 1 } else { 0 };
2871 let code_spans = self.source_code_spans();
2872 code_spans.iter().any(|span| {
2873 if line_num < span.line || line_num > span.end_line {
2875 return false;
2876 }
2877
2878 if span.line == span.end_line {
2879 col_0indexed >= span.start_col && col_0indexed < span.end_col
2881 } else if line_num == span.line {
2882 col_0indexed >= span.start_col
2884 } else if line_num == span.end_line {
2885 col_0indexed < span.end_col
2887 } else {
2888 true
2890 }
2891 })
2892 }
2893
2894 #[inline]
2896 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
2897 Self::binary_search_ranges(&self.inline_code_ranges, byte_offset)
2898 }
2899
2900 #[inline]
2902 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
2903 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
2904 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
2905 }
2906
2907 #[inline]
2909 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
2910 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
2911 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
2912 }
2913
2914 #[inline]
2917 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
2918 let tags = self.html_tags_shared();
2919
2920 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2921 idx > 0 && byte_pos < tags[idx - 1].byte_end
2922 }
2923
2924 #[inline]
2928 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
2929 if !self.flavor.supports_jsx() {
2930 return false;
2931 }
2932
2933 let tags = self.jsx_component_tags_shared();
2934
2935 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2936 idx > 0 && byte_pos < tags[idx - 1].byte_end
2937 }
2938
2939 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
2941 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
2942 }
2943
2944 #[inline]
2947 pub(crate) fn overlaps_template_code(&self, start: usize, end: usize) -> bool {
2948 if start >= end {
2949 return false;
2950 }
2951 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2952 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2953 idx > 0 && ranges[idx - 1].1 > start
2954 })
2955 }
2956
2957 #[inline]
2960 pub(crate) fn is_inside_template_code(&self, pos: usize) -> bool {
2961 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2962 let idx = ranges.partition_point(|&(start, _)| start < pos);
2963 idx > 0 && ranges[idx - 1].1 > pos
2964 })
2965 }
2966
2967 #[inline]
2969 pub(crate) fn is_in_jinja_string(&self, byte_pos: usize) -> bool {
2970 let ranges = self
2971 .jinja_string_ranges_cache
2972 .get_or_init(|| crate::utils::jinja_utils::find_jinja_string_ranges(self.content, &self.jinja_ranges));
2973 Self::binary_search_ranges(ranges, byte_pos)
2974 }
2975
2976 #[inline]
2978 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
2979 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
2980 }
2981
2982 #[inline]
2984 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
2985 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
2986 }
2987
2988 pub(crate) fn mdx_comment_ranges(&self) -> &[(usize, usize)] {
2990 &self.mdx_comment_ranges
2991 }
2992
2993 pub(crate) fn mdx_inline_code_ranges(&self, start: usize, end: usize) -> impl Iterator<Item = (usize, usize)> + '_ {
2995 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2996 .into_iter()
2997 .flat_map(move |ranges| {
2998 let first = ranges.partition_point(|&(_, range_end)| range_end <= start);
2999 ranges[first..]
3000 .iter()
3001 .copied()
3002 .take_while(move |&(range_start, _)| start < end && range_start < end)
3003 })
3004 }
3005
3006 #[inline]
3009 pub(crate) fn overlaps_mdx_inline_code(&self, start: usize, end: usize) -> bool {
3010 if start >= end {
3011 return false;
3012 }
3013 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
3014 .into_iter()
3015 .any(|ranges| {
3016 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
3017 idx > 0 && ranges[idx - 1].1 > start
3018 })
3019 }
3020
3021 pub(crate) fn is_inside_mdx_code(&self, pos: usize) -> bool {
3024 if self.flavor != MarkdownFlavor::MDX {
3025 return false;
3026 }
3027 if [
3028 &self.mdx_esm_ranges,
3029 &self.jsx_expression_ranges,
3030 &self.mdx_comment_ranges,
3031 ]
3032 .into_iter()
3033 .any(|ranges| {
3034 let idx = ranges.partition_point(|&(start, _)| start < pos);
3035 idx > 0 && ranges[idx - 1].1 > pos
3036 }) {
3037 return true;
3038 }
3039
3040 let tags = [self.html_tags_shared(), self.jsx_component_tags_shared()];
3041
3042 tags.into_iter().any(|tags| {
3043 let idx = tags.partition_point(|tag| tag.byte_offset < pos);
3044 idx > 0 && tags[idx - 1].byte_end > pos
3045 })
3046 }
3047
3048 pub(crate) fn overlaps_mdx_code(&self, start: usize, end: usize) -> bool {
3050 if start >= end || self.flavor != MarkdownFlavor::MDX {
3051 return false;
3052 }
3053 if [
3054 &self.mdx_esm_ranges,
3055 &self.jsx_expression_ranges,
3056 &self.mdx_comment_ranges,
3057 ]
3058 .into_iter()
3059 .any(|ranges| {
3060 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
3061 idx > 0 && ranges[idx - 1].1 > start
3062 }) {
3063 return true;
3064 }
3065
3066 let tags = [self.html_tags_shared(), self.jsx_component_tags_shared()];
3067
3068 tags.into_iter().any(|tags| {
3069 let idx = tags.partition_point(|tag| tag.byte_offset < end);
3070 idx > 0 && tags[idx - 1].byte_end > start
3071 })
3072 }
3073
3074 #[inline]
3077 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
3078 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
3079 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
3080 }
3081
3082 #[inline]
3084 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
3085 &self.citation_ranges
3086 }
3087
3088 #[inline]
3091 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
3092 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
3093 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
3094 }
3095
3096 #[inline]
3099 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
3100 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
3101 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
3102 }
3103
3104 #[inline]
3107 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
3108 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
3109 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
3110 }
3111
3112 #[inline]
3115 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
3116 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
3117 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
3118 }
3119
3120 #[inline]
3123 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
3124 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
3125 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
3126 }
3127
3128 #[inline]
3132 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
3133 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
3134 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
3135 }
3136
3137 #[inline]
3140 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
3141 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
3142 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
3143 }
3144
3145 #[inline]
3148 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
3149 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
3150 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
3151 }
3152
3153 #[inline]
3157 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
3158 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
3159 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
3160 }
3161
3162 #[inline]
3165 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
3166 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
3167 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
3168 }
3169
3170 #[inline]
3173 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
3174 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
3175 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
3176 }
3177
3178 #[inline]
3181 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
3182 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
3183 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
3184 }
3185
3186 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
3191 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
3192 self.pandoc_header_slugs.contains(&slug)
3193 }
3194
3195 #[inline]
3201 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
3202 self.pandoc_header_slugs.contains(slug)
3203 }
3204
3205 #[inline]
3207 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
3208 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
3209 }
3210
3211 #[inline]
3213 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
3214 &self.shortcode_ranges
3215 }
3216
3217 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
3219 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
3220 }
3221
3222 pub fn has_char(&self, ch: char) -> bool {
3224 match ch {
3225 '#' => self.char_frequency.hash_count > 0,
3226 '*' => self.char_frequency.asterisk_count > 0,
3227 '_' => self.char_frequency.underscore_count > 0,
3228 '-' => self.char_frequency.hyphen_count > 0,
3229 '+' => self.char_frequency.plus_count > 0,
3230 '>' => self.char_frequency.gt_count > 0,
3231 '|' => self.char_frequency.pipe_count > 0,
3232 '[' => self.char_frequency.bracket_count > 0,
3233 '`' => self.char_frequency.backtick_count > 0,
3234 '<' => self.char_frequency.lt_count > 0,
3235 '!' => self.char_frequency.exclamation_count > 0,
3236 '\n' => self.char_frequency.newline_count > 0,
3237 '$' => self.line_candidates.has_signal(candidates::DOLLAR),
3238 '=' => self.line_candidates.has_signal(candidates::EQUALS),
3239 _ => self.content.contains(ch), }
3241 }
3242
3243 pub fn char_count(&self, ch: char) -> usize {
3245 match ch {
3246 '#' => self.char_frequency.hash_count,
3247 '*' => self.char_frequency.asterisk_count,
3248 '_' => self.char_frequency.underscore_count,
3249 '-' => self.char_frequency.hyphen_count,
3250 '+' => self.char_frequency.plus_count,
3251 '>' => self.char_frequency.gt_count,
3252 '|' => self.char_frequency.pipe_count,
3253 '[' => self.char_frequency.bracket_count,
3254 '`' => self.char_frequency.backtick_count,
3255 '<' => self.char_frequency.lt_count,
3256 '!' => self.char_frequency.exclamation_count,
3257 '\n' => self.char_frequency.newline_count,
3258 _ => self.content.matches(ch).count(), }
3260 }
3261
3262 pub fn likely_has_headings(&self) -> bool {
3265 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.has_char('=')
3266 }
3267
3268 pub fn likely_has_lists(&self) -> bool {
3272 self.char_frequency.asterisk_count > 0
3273 || self.char_frequency.hyphen_count > 0
3274 || self.char_frequency.plus_count > 0
3275 }
3276
3277 pub fn likely_has_emphasis(&self) -> bool {
3279 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
3280 }
3281
3282 pub fn likely_has_tables(&self) -> bool {
3284 self.char_frequency.pipe_count > 2
3285 }
3286
3287 pub fn likely_has_blockquotes(&self) -> bool {
3289 self.char_frequency.gt_count > 0
3290 }
3291
3292 pub fn likely_has_code(&self) -> bool {
3294 self.char_frequency.backtick_count > 0
3295 }
3296
3297 pub fn likely_has_links_or_images(&self) -> bool {
3299 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
3300 }
3301
3302 pub fn likely_has_html(&self) -> bool {
3304 self.char_frequency.lt_count > 0
3305 }
3306
3307 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
3312 if let Some(line_info) = self.lines.get(line_idx)
3313 && let Some(ref bq) = line_info.blockquote
3314 {
3315 bq.prefix.trim_end().to_string()
3316 } else {
3317 String::new()
3318 }
3319 }
3320
3321 #[cfg(test)]
3332 #[inline]
3333 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
3334 let idx = source_positions::line_index(lines, byte_offset);
3335
3336 let line = &lines[idx];
3337 let line_num = idx + 1;
3338 let byte_col = byte_offset.saturating_sub(line.byte_offset);
3339 let col = byte_to_char_count(line.content(content), byte_col) - 1;
3342
3343 (idx, line_num, col)
3344 }
3345
3346 #[must_use]
3366 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
3367 ValidHeadingsIter::new(&self.lines, &self.document_lines().headings)
3368 }
3369
3370 #[must_use]
3374 pub fn has_valid_headings(&self) -> bool {
3375 !self.document_lines().headings.is_empty()
3376 }
3377
3378 #[must_use]
3380 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
3381 ParsedListItemsIter::new(&self.lines, &self.document_lines().list_items)
3382 }
3383
3384 #[must_use]
3386 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
3387 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
3388 Some(ParsedListItem::new(
3389 line_num,
3390 line_info.list_item.as_deref()?,
3391 line_info,
3392 ))
3393 }
3394
3395 #[must_use]
3397 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
3398 ParsedListBlocks::new(&self.list_blocks, &self.lines)
3399 }
3400
3401 #[must_use]
3405 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
3406 list_blocks::item_lines_by_list(self.content, &self.lines, block)
3407 }
3408
3409 #[must_use]
3411 pub fn has_list_items(&self) -> bool {
3412 !self.document_lines().list_items.is_empty()
3413 }
3414
3415 #[must_use]
3417 pub fn has_unordered_list_items(&self) -> bool {
3418 self.document_lines().has_unordered_items
3419 }
3420
3421 #[must_use]
3423 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
3424 let lists = self
3425 .commonmark_ordered_lists_cache
3426 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
3427 CommonMarkOrderedLists::new(lists, &self.lines)
3428 }
3429
3430 #[must_use]
3438 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
3439 ParsedHeadingsIter::new(
3440 &self.lines,
3441 &self.blockquote_headings,
3442 &self.document_lines().parsed_headings,
3443 )
3444 }
3445
3446 #[must_use]
3448 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
3449 let idx = line_num.checked_sub(1)?;
3450 let line_info = self.lines.get(idx)?;
3451 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
3452 Some(heading) => (heading, 0),
3453 None => (
3454 self.blockquote_headings.get(idx)?.as_deref()?,
3455 line_info.blockquote.as_ref().map_or(1, |bq| bq.nesting_level),
3456 ),
3457 };
3458 Some(ParsedHeading {
3459 line_num,
3460 heading,
3461 line_info,
3462 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
3463 blockquote_depth,
3464 })
3465 }
3466}
3467
3468fn container_comment_range(
3480 opener: usize,
3481 containers: &flavor_detection::ContainerLines,
3482 lines: &[types::LineInfo],
3483 content: &str,
3484) -> Option<crate::utils::skip_context::ByteRange> {
3485 let line_index = lines
3486 .partition_point(|line| line.byte_offset <= opener)
3487 .checked_sub(1)?;
3488 let line = lines.get(line_index)?;
3489 if line.byte_offset + line.indent != opener {
3490 return None;
3491 }
3492 if !containers.is_container_body(line_index) {
3493 return None;
3494 }
3495 let end_line = lines.get(containers.body_end_line(line_index)?)?;
3496 Some(crate::utils::skip_context::ByteRange {
3497 start: opener,
3498 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
3499 })
3500}
3501
3502#[derive(Debug)]
3504struct DefinitionListLines {
3505 outer: Vec<(usize, usize)>,
3508 terms: Vec<(usize, usize)>,
3510 texts: Vec<DefinitionText>,
3512}
3513
3514impl DefinitionListLines {
3515 fn new(
3516 line_offsets: &[usize],
3517 items: &[(usize, usize)],
3518 terms: &[(usize, usize)],
3519 texts: &[crate::utils::code_block_utils::DefinitionTextDetail],
3520 ) -> Self {
3521 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
3524 let last_line_of = |start: usize, end: usize| line_of(end.saturating_sub(1).max(start));
3525
3526 let mut outer: Vec<(usize, usize)> = Vec::new();
3529 for &(start, end) in items {
3530 let (start_line, end_line) = (line_of(start), last_line_of(start, end));
3531 match outer.last_mut() {
3532 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
3533 _ => outer.push((start_line, end_line)),
3534 }
3535 }
3536 let terms = terms
3537 .iter()
3538 .map(|&(start, end)| (line_of(start), last_line_of(start, end)))
3539 .collect();
3540
3541 let texts = texts
3542 .iter()
3543 .map(|text| {
3544 let start_line = line_of(text.start);
3545 let line_start = line_offsets[start_line - 1];
3546 let on_marker_line = line_of(text.definition_start) == start_line;
3547 DefinitionText {
3548 start_line,
3549 end_line: last_line_of(text.start, text.end),
3550 marker_prefix_len: on_marker_line.then(|| text.start - line_start),
3551 }
3552 })
3553 .collect();
3554
3555 Self { outer, terms, texts }
3556 }
3557}
3558
3559fn detect_footnote_definitions(
3572 content: &str,
3573 lines: &mut [types::LineInfo],
3574 line_offsets: &[usize],
3575) -> Vec<(usize, usize)> {
3576 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
3577
3578 if !content.contains("[^") {
3580 return Vec::new();
3581 }
3582
3583 let options = crate::utils::parser_options::rumdl_parser_options_for(content);
3584 let parser = Parser::new_ext(content, options).into_offset_iter();
3585
3586 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
3588 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
3589 let mut indented_code_ranges: Vec<(usize, usize)> = Vec::new();
3590 let mut in_footnote = false;
3591
3592 for (event, range) in parser {
3593 match event {
3594 Event::Start(Tag::FootnoteDefinition(_)) => {
3595 in_footnote = true;
3596 footnote_ranges.push((range.start, range.end));
3597 }
3598 Event::End(TagEnd::FootnoteDefinition) => {
3599 in_footnote = false;
3600 }
3601 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
3602 fenced_code_ranges.push((range.start, range.end));
3603 }
3604 Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) if in_footnote => {
3605 indented_code_ranges.push((range.start, range.end));
3606 }
3607 _ => {}
3608 }
3609 }
3610
3611 let byte_to_line = |byte_offset: usize| -> usize {
3612 line_offsets
3613 .partition_point(|&offset| offset <= byte_offset)
3614 .saturating_sub(1)
3615 };
3616
3617 for &(start, end) in &footnote_ranges {
3619 let start_line = byte_to_line(start);
3620 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
3621
3622 for line in &mut lines[start_line..end_line] {
3623 line.in_footnote_definition = true;
3624 line.in_code_block = false;
3625 }
3626 }
3627
3628 for &(start, end) in &fenced_code_ranges {
3630 let start_line = byte_to_line(start);
3631 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
3632
3633 for line in &mut lines[start_line..end_line] {
3634 line.in_code_block = true;
3635 }
3636 }
3637 indented_code_ranges
3638}