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};
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>>, broken_links: Vec<BrokenLinkInfo>, footnote_refs: Vec<FootnoteRef>, reference_defs: Vec<ReferenceDef>, reference_defs_map: HashMap<String, usize>, inline_code_ranges: Vec<(usize, usize)>,
415 code_spans_cache: OnceLock<Arc<Vec<CodeSpan>>>,
416 source_code_spans: OnceLock<Vec<SourceCodeSpan>>, math_spans_cache: OnceLock<Arc<Vec<MathSpan>>>, bracket_math_cache: OnceLock<bracket_math::BracketDisplayMathLines>,
419 math_byte_ranges_cache: OnceLock<Vec<(usize, usize)>>, parser_has_math: bool,
422 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>,
428 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)>>,
438 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, }
473
474pub struct CodeRanges {
476 pub blocks: Vec<(usize, usize)>,
478 pub spans: Vec<(usize, usize)>,
480}
481
482pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
493 let ctx = LintContext::new(content, flavor, None);
494 CodeRanges {
495 spans: ctx.inline_code_ranges.clone(),
496 blocks: ctx.code_blocks,
497 }
498}
499
500pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
502 code_spans
503 .iter()
504 .map(|span| (span.byte_offset, span.byte_end))
505 .collect()
506}
507
508impl<'a> LintContext<'a> {
509 pub(crate) fn native_list_spacing_plan(
510 &self,
511 prepare: impl FnOnce() -> NativeListSpacingPlan,
512 ) -> &NativeListSpacingPlan {
513 self.native_list_spacing_plan.get_or_init(prepare)
514 }
515
516 pub(crate) fn list_guard_parse(&self) -> &crate::utils::list_fix_guard::Parsed<'_> {
517 self.list_guard_parse
518 .get_or_init(|| crate::utils::list_fix_guard::Parsed::new(self.content).into_owned())
519 }
520
521 pub(crate) fn parser_blocks(&self) -> &document_parser::blocks::BlockFacts {
522 self.parser_blocks
523 .get_or_init(|| document_parser::blocks::BlockFacts::parse(self.content))
524 }
525
526 pub(crate) fn with_merge_conflicts(mut self, markers: &[crate::merge_conflict::Marker]) -> Self {
527 self.merge_conflicts = OnceLock::from(markers.to_vec());
528 self
529 }
530
531 pub(crate) fn merge_conflicts(&self) -> &[crate::merge_conflict::Marker] {
532 self.merge_conflicts
533 .get_or_init(|| crate::merge_conflict::scan_markers(self.content).collect())
534 }
535
536 pub fn source_file(&self) -> Option<&Path> {
541 self.source_file.as_deref()
542 }
543
544 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
545 self.link_target_policy.as_ref()
546 }
547
548 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
549 self.link_target_policy = Some(policy);
550 self
551 }
552
553 pub fn invalid_utf8(&self) -> Option<&'a [crate::encoding::InvalidSeq]> {
555 self.invalid_utf8
556 }
557
558 pub fn with_invalid_utf8(mut self, invalid: &'a [crate::encoding::InvalidSeq]) -> Self {
559 self.invalid_utf8 = Some(invalid);
560 self
561 }
562
563 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
564 Self::new_inner(content, flavor, source_file, None, None)
565 }
566
567 #[cfg(test)]
568 pub(crate) fn new_with_handoff(
569 content: &'a str,
570 flavor: MarkdownFlavor,
571 source_file: Option<PathBuf>,
572 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
573 ) -> Self {
574 Self::new_inner(
576 content,
577 flavor,
578 source_file,
579 handoff.filter(|parsed| parsed.matches(content)),
580 None,
581 )
582 }
583
584 pub(crate) fn store_parse_handoff(&self, parsed: document_parser::handoff::OwnedParsedDocument) {
585 if let Ok(mut slot) = self.parse_handoff.lock() {
586 *slot = Some(parsed);
587 }
588 }
589
590 pub(crate) fn certifies_list_guard(&self, content: &str, allowed: crate::utils::list_fix_guard::Allowed) -> bool {
591 self.parse_handoff
592 .lock()
593 .ok()
594 .and_then(|slot| {
595 slot.as_ref()
596 .map(|parsed| parsed.certifies_list_guard(content, allowed))
597 })
598 .unwrap_or(false)
599 }
600
601 pub(crate) fn take_parse_handoff(&self, content: &str) -> Option<document_parser::handoff::OwnedParsedDocument> {
602 self.parse_handoff
603 .lock()
604 .ok()?
605 .take()
606 .filter(|parsed| parsed.matches(content))
607 }
608
609 pub(crate) fn new_with_proof(
610 content: &'a str,
611 flavor: MarkdownFlavor,
612 source_file: Option<PathBuf>,
613 parsed: Option<document_parser::ParsedDocument<'a>>,
614 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
615 ) -> Self {
616 Self::new_inner(content, flavor, source_file, handoff, parsed)
617 }
618
619 fn new_inner(
620 content: &'a str,
621 flavor: MarkdownFlavor,
622 source_file: Option<PathBuf>,
623 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
624 proof_parsed: Option<document_parser::ParsedDocument<'a>>,
625 ) -> Self {
626 let (structure, finish) = Self::prepare_native_structure(content, flavor, source_file, handoff, proof_parsed);
627 finish(structure)
628 }
629
630 pub(crate) fn prepare_native_structure(
631 content: &'a str,
632 flavor: MarkdownFlavor,
633 source_file: Option<PathBuf>,
634 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
635 proof_parsed: Option<document_parser::ParsedDocument<'a>>,
636 ) -> (
637 NativePreparedStructure<'a>,
638 impl FnOnce(NativePreparedStructure<'a>) -> Self,
639 ) {
640 #[cfg(not(target_arch = "wasm32"))]
641 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
642
643 let line_computation::SourceLayout {
644 offsets: line_offsets,
645 lines: content_lines,
646 frequency: char_frequency,
647 candidates: line_candidates,
648 } = profile_section!(
649 "Source lines & markers",
650 profile,
651 line_computation::source_layout(content)
652 );
653
654 #[allow(clippy::disallowed_methods)]
658 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
659
660 let proof_parsed = proof_parsed.filter(|parsed| parsed.matches_source(content));
663 let handoff = handoff.filter(|document| document.matches(content));
664
665 let document_parser::ParsedDocument {
668 structure: parse_result,
669
670 has_math: parser_has_math,
671
672 parser_blocks,
673 pending_links,
674 pending_lines,
675 text_start_lines,
676 } = profile_section!("Shared Markdown parse", profile, {
677 proof_parsed.unwrap_or_else(|| {
678 handoff.map_or_else(
679 || document_parser::ParsedDocument::parse(content, &line_offsets),
680 |document| document.into_document(content),
681 )
682 })
683 });
684 let mut code_blocks = parse_result.code_blocks;
685 let parser_code_blocks = code_blocks.clone();
686 let table_ranges = parse_result.table_ranges;
687 let top_level_blocks = parse_result.top_level_blocks;
688 let rejected_definition_ranges = parse_result.rejected_definition_ranges;
689 let list_items = parse_result.list_items;
690 let mut code_span_ranges = parse_result.code_spans;
691 let code_block_details = parse_result.code_block_details;
692 let strong_spans = parse_result.strong_spans;
693 let line_to_list = parse_result.line_to_list;
694 let list_start_values = parse_result.list_start_values;
695 let html_blocks = parse_result.html_blocks;
696 let definition_lists = DefinitionListLines::new(
697 &line_offsets,
698 &parse_result.definition_items,
699 &parse_result.definition_terms,
700 &parse_result.definition_texts,
701 );
702
703 let mut in_fenced_code = scratch::LineFlags::new(content_lines.len());
708 for detail in code_block_details.iter().filter(|detail| detail.is_fenced) {
709 let start_line = line_offsets
710 .partition_point(|&offset| offset <= detail.start)
711 .saturating_sub(1);
712 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
713 for flag in in_fenced_code.iter_mut().take(end_line).skip(start_line) {
714 *flag = true;
715 }
716 }
717 let tag_hiding = if crate::utils::mkdocs_html_markdown::mentions_markdown(content) {
722 let comment_code_ranges: Vec<(usize, usize)> = code_block_details
723 .iter()
724 .map(|detail| (detail.start, detail.end))
725 .collect();
726 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
727 let comments = crate::utils::skip_context::scan_html_comments(
728 content,
729 &code_span_ranges,
730 &comment_code_ranges,
731 body_start,
732 )
733 .ranges;
734 let comments: Vec<(usize, usize)> = comments.iter().map(|range| (range.start, range.end)).collect();
735 let indented: Vec<(usize, usize)> = code_block_details
736 .iter()
737 .filter(|detail| !detail.is_fenced)
738 .map(|detail| (detail.start, detail.end))
739 .collect();
740 let containing = |ranges: &[(usize, usize)], at: usize| {
742 let idx = ranges.partition_point(|&(start, _)| start <= at);
743 idx.checked_sub(1).map(|i| ranges[i]).filter(|&(_, end)| at < end)
744 };
745 let mut text_resume: Vec<Option<usize>> = vec![None; content_lines.len()];
746 let mut outside_elements = vec![false; content_lines.len()];
747 for (i, line) in content_lines.iter().enumerate() {
748 if i < front_matter_end {
750 text_resume[i] = Some(line.len());
751 continue;
752 }
753 let at = line_offsets[i] + (line.len() - line.trim_start().len());
754 if let Some((_, text_end)) = containing(&comments, at) {
755 text_resume[i] = Some(text_end.min(line_offsets[i] + line.len()) - line_offsets[i]);
756 } else if let Some((_, text_end)) = containing(&code_span_ranges, at) {
757 let column = text_end.min(line_offsets[i] + line.len()) - line_offsets[i];
761 let closes = line
762 .get(column..)
763 .is_some_and(|rest| rest.trim_start().starts_with("</"));
764 text_resume[i] = Some(if closes { column } else { line.len() });
765 }
766 outside_elements[i] = containing(&indented, at).is_some();
767 }
768 flavor_detection::TagHiding {
769 text_resume,
770 outside_elements,
771 }
772 } else {
773 flavor_detection::TagHiding {
774 text_resume: Vec::new(),
775 outside_elements: Vec::new(),
776 }
777 };
778 let containers = profile_section!(
779 "Container lines",
780 profile,
781 flavor_detection::detect_container_lines(&content_lines, &in_fenced_code, &tag_hiding, flavor)
782 );
783
784 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
793 .iter()
794 .flat_map(|detail| {
795 if detail.is_fenced {
796 return vec![(detail.start, detail.end)];
797 }
798 let start_line = line_offsets
799 .partition_point(|&offset| offset <= detail.start)
800 .saturating_sub(1);
801 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
802 containers
803 .code_line_spans_in(start_line..end_line)
804 .into_iter()
805 .map(|span| {
806 let start = line_offsets[span.start].max(detail.start);
807 let end = line_offsets
808 .get(span.end)
809 .copied()
810 .unwrap_or(content.len())
811 .min(detail.end);
812 (start, end)
813 })
814 .collect()
815 })
816 .collect();
817 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
823 let html_comment_scan = profile_section!(
824 "HTML comment ranges",
825 profile,
826 crate::utils::skip_context::scan_html_comments(
827 content,
828 &code_span_ranges,
829 &comment_code_block_ranges,
830 body_start
831 )
832 );
833 let mut html_comment_ranges = html_comment_scan.ranges;
834 let unterminated_html_comment = html_comment_scan.unterminated;
835
836 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
840 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() || !content.contains(":::") {
841 Vec::new()
842 } else {
843 crate::utils::mkdocstrings_refs::autodoc_block_ranges_in_lines(&content_lines, flavor)
844 }
845 });
846
847 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
849 if flavor.is_pandoc_compatible() {
850 crate::utils::pandoc::detect_div_block_ranges(content)
851 } else {
852 Vec::new()
853 }
854 });
855
856 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
858 if flavor == MarkdownFlavor::MkDocs {
859 crate::utils::pymdown_blocks::detect_block_ranges(content)
860 } else {
861 Vec::new()
862 }
863 });
864
865 let skip_ranges = SkipByteRanges {
868 front_matter_end,
869 html_comment_ranges: &html_comment_ranges,
870 autodoc_ranges: &autodoc_ranges,
871 pandoc_div_ranges: &pandoc_div_ranges,
872 pymdown_block_ranges: &pymdown_block_ranges,
873 };
874 let (mut lines, emphasis_spans) = profile_section!(
875 "Basic line info",
876 profile,
877 line_computation::compute_basic_line_info(
878 content,
879 &content_lines,
880 &line_offsets,
881 &code_blocks,
882 flavor,
883 &skip_ranges,
884 pending_lines,
885 )
886 );
887
888 profile_section!(
890 "HTML blocks",
891 profile,
892 heading_detection::detect_html_blocks(content, &mut lines)
893 );
894
895 profile_section!(
897 "ESM blocks",
898 profile,
899 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
900 );
901
902 profile_section!(
904 "JSX block detection",
905 profile,
906 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
907 );
908
909 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
911 "JSX/MDX detection",
912 profile,
913 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
914 );
915
916 profile_section!(
921 "Markdown-in-HTML blocks",
922 profile,
923 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
924 );
925
926 let mdx_context = if flavor == MarkdownFlavor::MDX {
927 mdx::MdxContext::parse(content, &lines)
928 } else {
929 None
930 };
931 let mdx_esm_ranges = mdx_context.as_ref().map_or_else(Vec::new, |mdx| mdx.esm.clone());
932 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
933 mdx.apply_lines(&mut lines);
934 code_blocks.clone_from(&mdx.code_blocks);
935 code_span_ranges.clone_from(&mdx.code_spans);
936 (mdx.expressions.clone(), mdx.comments.clone())
937 } else {
938 (jsx_expression_ranges, mdx_comment_ranges)
939 };
940
941 if !flavor.is_pandoc_compatible() {
949 let mut spans = code_span_ranges.iter().peekable();
952 for (line, &offset) in lines.iter_mut().zip(&line_offsets) {
953 if line.is_div_marker {
954 let end = offset + content[offset..].find('\n').unwrap_or(content.len() - offset);
955 while spans.peek().is_some_and(|&&(_, stop)| stop <= end) {
956 spans.next();
957 }
958 line.is_div_marker = spans.peek().is_none_or(|&&(start, _)| start >= end);
959 }
960 }
961 }
962
963 let mut colon_definition_lines: Vec<(usize, usize)> = Vec::new();
968 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
969 for detail in &parse_result.colon_definitions {
970 let definition_line = line_of(detail.definition_start);
971 if flavor.is_pandoc_compatible() && lines[definition_line - 1].is_div_marker {
972 continue;
973 }
974 let (start_line, end_line) = (
975 line_of(detail.start),
976 line_of(detail.end.saturating_sub(1).max(detail.start)),
977 );
978 match colon_definition_lines.last_mut() {
979 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
980 _ => colon_definition_lines.push((start_line, end_line)),
981 }
982 }
983
984 profile_section!(
986 "MkDocs constructs",
987 profile,
988 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
989 );
990
991 let footnote_code_blocks = profile_section!(
997 "Footnote definitions",
998 profile,
999 detect_footnote_definitions(content, &mut lines, &line_offsets)
1000 );
1001
1002 {
1005 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1006 for &(start, end) in &code_blocks {
1007 let start_line = line_offsets
1008 .partition_point(|&offset| offset <= start)
1009 .saturating_sub(1);
1010 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1011
1012 let mut sub_start: Option<usize> = None;
1013 for (i, &offset) in line_offsets[start_line..end_line]
1014 .iter()
1015 .enumerate()
1016 .map(|(j, o)| (j + start_line, o))
1017 {
1018 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1019 if is_real_code && sub_start.is_none() {
1020 let byte_start = if i == start_line { start } else { offset };
1021 sub_start = Some(byte_start);
1022 } else if !is_real_code && sub_start.is_some() {
1023 new_code_blocks.push((sub_start.unwrap(), offset));
1024 sub_start = None;
1025 }
1026 }
1027 if let Some(s) = sub_start {
1028 new_code_blocks.push((s, end));
1029 }
1030 }
1031 code_blocks = new_code_blocks;
1032 }
1033
1034 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
1042 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
1043 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1044 for &(start, end) in &code_blocks {
1045 let start_line = line_offsets
1046 .partition_point(|&offset| offset <= start)
1047 .saturating_sub(1);
1048 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1049
1050 let mut sub_start: Option<usize> = None;
1052 for (i, &offset) in line_offsets[start_line..end_line]
1053 .iter()
1054 .enumerate()
1055 .map(|(j, o)| (j + start_line, o))
1056 {
1057 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1058 if is_real_code && sub_start.is_none() {
1059 let byte_start = if i == start_line { start } else { offset };
1060 sub_start = Some(byte_start);
1061 } else if !is_real_code && sub_start.is_some() {
1062 new_code_blocks.push((sub_start.unwrap(), offset));
1063 sub_start = None;
1064 }
1065 }
1066 if let Some(s) = sub_start {
1067 new_code_blocks.push((s, end));
1068 }
1069 }
1070 code_blocks = new_code_blocks;
1071 }
1072
1073 if flavor.supports_jsx() {
1077 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1078 for &(start, end) in &code_blocks {
1079 let start_line = line_offsets
1080 .partition_point(|&offset| offset <= start)
1081 .saturating_sub(1);
1082 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1083
1084 let mut sub_start: Option<usize> = None;
1085 for (i, &offset) in line_offsets[start_line..end_line]
1086 .iter()
1087 .enumerate()
1088 .map(|(j, o)| (j + start_line, o))
1089 {
1090 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1091 if is_real_code && sub_start.is_none() {
1092 let byte_start = if i == start_line { start } else { offset };
1093 sub_start = Some(byte_start);
1094 } else if !is_real_code && sub_start.is_some() {
1095 new_code_blocks.push((sub_start.unwrap(), offset));
1096 sub_start = None;
1097 }
1098 }
1099 if let Some(s) = sub_start {
1100 new_code_blocks.push((s, end));
1101 }
1102 }
1103 code_blocks = new_code_blocks;
1104
1105 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
1112 let mut run: Option<(usize, usize)> = None;
1113 for line in &lines {
1114 if line.in_jsx_block && line.in_code_block {
1115 let line_end = line.byte_offset + line.byte_len;
1116 match &mut run {
1117 Some((_, end)) => *end = line_end,
1118 None => run = Some((line.byte_offset, line_end)),
1119 }
1120 } else if let Some(r) = run.take() {
1121 jsx_fence_ranges.push(r);
1122 }
1123 }
1124 if let Some(r) = run.take() {
1125 jsx_fence_ranges.push(r);
1126 }
1127 if !jsx_fence_ranges.is_empty() {
1128 code_blocks.extend(jsx_fence_ranges);
1129 code_blocks.sort_by_key(|&(start, _)| start);
1130 }
1131 }
1132
1133 let colon_fence_details = profile_section!(
1136 "Azure colon fence detection",
1137 profile,
1138 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
1139 );
1140 if !colon_fence_details.is_empty() {
1141 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
1142 code_blocks.sort_by_key(|&(start, _)| start);
1143 }
1144
1145 let myst_directive_ranges = profile_section!(
1148 "MyST colon directives",
1149 profile,
1150 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
1151 );
1152
1153 let myst_comment_ranges = profile_section!(
1155 "MyST comments",
1156 profile,
1157 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
1158 );
1159
1160 profile_section!(
1163 "MyST backtick directives",
1164 profile,
1165 flavor_detection::detect_myst_backtick_directives(
1166 content,
1167 &mut lines,
1168 flavor,
1169 &code_block_details,
1170 &line_offsets
1171 )
1172 );
1173
1174 if flavor.supports_myst_directives() {
1177 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1178 for &(start, end) in &code_blocks {
1179 let start_line = line_offsets
1180 .partition_point(|&offset| offset <= start)
1181 .saturating_sub(1);
1182 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1183
1184 let mut sub_start: Option<usize> = None;
1185 for (i, &offset) in line_offsets[start_line..end_line]
1186 .iter()
1187 .enumerate()
1188 .map(|(j, o)| (j + start_line, o))
1189 {
1190 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1191 if is_real_code && sub_start.is_none() {
1192 let byte_start = if i == start_line { start } else { offset };
1193 sub_start = Some(byte_start);
1194 } else if !is_real_code && sub_start.is_some() {
1195 new_code_blocks.push((sub_start.unwrap(), offset));
1196 sub_start = None;
1197 }
1198 }
1199 if let Some(s) = sub_start {
1200 new_code_blocks.push((s, end));
1201 }
1202 }
1203 code_blocks = new_code_blocks;
1204 }
1205
1206 profile_section!(
1208 "Kramdown constructs",
1209 profile,
1210 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
1211 );
1212
1213 for line in &mut lines {
1218 if line.in_kramdown_extension_block {
1219 line.list_item = None;
1220 line.is_horizontal_rule = false;
1221 line.blockquote = None;
1222 line.is_kramdown_block_ial = false;
1223 }
1224 }
1225
1226 let obsidian_comment_scan = profile_section!(
1228 "Obsidian comments",
1229 profile,
1230 flavor_detection::detect_obsidian_comments(
1231 content,
1232 &mut lines,
1233 flavor,
1234 &code_span_ranges,
1235 &html_comment_ranges,
1236 body_start
1237 )
1238 );
1239 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
1240 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
1241
1242 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
1247 unterminated_html_comment,
1248 &obsidian_comment_ranges,
1249 content,
1250 &code_span_ranges,
1251 &comment_code_block_ranges,
1252 body_start,
1253 );
1254
1255 if let Some(range) = unterminated_html_comment.and_then(|opener| {
1268 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
1269 .or_else(|| container_comment_range(opener, &containers, &lines, content))
1270 }) {
1271 html_comment_ranges.push(range);
1274
1275 for line in &mut lines {
1281 let text = line.content(content);
1282 let content_start = line.byte_offset + line.indent;
1283 let content_end = line.byte_offset + text.trim_end().len();
1284 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_sorted_html_comment(
1285 &html_comment_ranges,
1286 content_start,
1287 content_end,
1288 );
1289 line.in_obsidian_comment = false;
1290 }
1291
1292 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
1303 content,
1304 &mut lines,
1305 flavor,
1306 &code_span_ranges,
1307 &html_comment_ranges,
1308 body_start,
1309 );
1310 obsidian_comment_ranges = obsidian_rescan.ranges;
1311 unterminated_obsidian_comment = obsidian_rescan.unterminated;
1312 }
1313
1314 let myst_role_ranges = profile_section!(
1316 "MyST roles",
1317 profile,
1318 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
1319 );
1320
1321 let mut pulldown_result = profile_section!(
1325 "Links, images & link ranges",
1326 profile,
1327 if let Some(mdx) = &mdx_context {
1328 pending_links.finish_mdx(mdx, &lines)
1329 } else {
1330 pending_links.finish(&lines, &code_blocks, flavor, &html_comment_ranges)
1331 }
1332 );
1333
1334 heading_detection::mark_parser_html_blocks(content, &mut lines, &html_blocks, flavor);
1335 let (html_block_ranges, html_block_lines) = heading_detection::parser_html_blocks(&lines, &html_blocks, flavor);
1336
1337 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
1339 let heading_detection::HeadingAnalysis {
1340 mut blockquote_headings,
1341 lazy_table_text,
1342 } = profile_section!(
1343 "Headings & blockquotes",
1344 profile,
1345 heading_detection::detect_headings_and_blockquotes(
1346 &content_lines,
1347 &mut lines,
1348 flavor,
1349 &html_comment_ranges,
1350 &html_blocks,
1351 &code_blocks,
1352 &code_span_ranges,
1353 &pulldown_result.link_byte_ranges,
1354 front_matter_end,
1355 mdx_flow_lines.as_deref(),
1356 mdx_context
1357 .as_ref()
1358 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1359 )
1360 );
1361
1362 for line in &mut lines {
1364 if line.in_kramdown_extension_block {
1365 line.heading = None;
1366 }
1367 }
1368 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
1369 if line.in_kramdown_extension_block {
1370 *heading = None;
1371 }
1372 }
1373
1374 for range in &html_block_lines {
1391 for line in &mut lines[range.clone()] {
1392 if !line.in_mkdocs_html_markdown {
1393 line.is_horizontal_rule = false;
1394 }
1395 }
1396 }
1397 for &(start, end) in &footnote_code_blocks {
1398 let spanned = line_computation::spanned_lines(&lines, start, end);
1399 for line in &mut lines[spanned] {
1400 line.is_horizontal_rule = false;
1401 }
1402 }
1403 for line in &mut lines {
1404 if line.is_horizontal_rule
1405 && (line.in_code_block
1406 || line.in_html_block
1407 || line.in_html_comment
1408 || line.in_math_block
1409 || line.in_mdx_comment
1410 || line.in_obsidian_comment)
1411 {
1412 line.is_horizontal_rule = false;
1413 }
1414 }
1415
1416 let mut inline_code_ranges = code_span_ranges.clone();
1419
1420 if flavor == MarkdownFlavor::MkDocs {
1424 let extra = profile_section!(
1425 "MkDocs code spans",
1426 profile,
1427 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
1428 );
1429 if !extra.is_empty() {
1430 inline_code_ranges.extend(extra);
1431 inline_code_ranges.sort_by_key(|span| span.0);
1432 }
1433 }
1434
1435 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
1440 let extra = profile_section!(
1441 "MDX JSX code spans",
1442 profile,
1443 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
1444 );
1445 if !extra.is_empty() {
1446 inline_code_ranges.extend(extra);
1447 inline_code_ranges.sort_by_key(|span| span.0);
1448 }
1449 }
1450
1451 for &(start, end) in &inline_code_ranges {
1454 let first = lines.partition_point(|line| line.byte_offset <= start);
1455 let last = lines.partition_point(|line| line.byte_offset <= end);
1456 for line in &mut lines[first..last] {
1457 line.in_code_span_continuation = true;
1458 }
1459 }
1460
1461 let mut parsed_link_ranges = std::mem::take(&mut pulldown_result.link_byte_ranges);
1464 parsed_link_ranges.sort_unstable();
1465
1466 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1467 let list_blocks: Vec<_> = list_blocks
1468 .into_iter()
1469 .filter(|block| {
1470 !lines
1471 .get(block.start_line - 1)
1472 .is_some_and(|line| line.in_kramdown_extension_block)
1473 })
1474 .collect();
1475 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks, &inline_code_ranges);
1476 let code_spans_without_math_cache = OnceLock::new();
1477 let structure = NativePreparedStructure {
1478 content,
1479 flavor,
1480 line_offsets,
1481 code_blocks,
1482 code_block_details,
1483 lines,
1484 list_blocks,
1485 html_block_lines,
1486 inline_code_ranges,
1487 code_span_ranges,
1488 inline_config,
1489 front_matter_end,
1490 code_spans_without_math_cache,
1491
1492 parser_has_math,
1493 };
1494 let finish = move |structure: NativePreparedStructure<'a>| {
1498 let NativePreparedStructure {
1499 content,
1500 flavor,
1501 line_offsets,
1502 code_blocks,
1503 code_block_details,
1504 mut lines,
1505 list_blocks,
1506 html_block_lines,
1507 inline_code_ranges,
1508 code_span_ranges,
1509 inline_config,
1510 front_matter_end,
1511 code_spans_without_math_cache,
1512
1513 parser_has_math,
1514 } = structure;
1515 let (links, images, broken_links, footnote_refs) = profile_section!(
1517 "Links & images finalize",
1518 profile,
1519 link_parser::finalize_links_and_images(
1520 content,
1521 &lines,
1522 flavor,
1523 &link_parser::LinkExclusions {
1524 code_blocks: &code_blocks,
1525 code_spans: &inline_code_ranges,
1526 html_comment_ranges: &html_comment_ranges,
1527 mdx: mdx_context.as_ref(),
1528 },
1529 pulldown_result,
1530 )
1531 );
1532
1533 let reference_defs = profile_section!("Reference defs", profile, {
1534 if let Some(mdx) = &mdx_context {
1535 mdx.reference_defs(content)
1536 } else {
1537 link_parser::parse_reference_defs(content, &lines)
1538 }
1539 });
1540
1541 let mut lazy_text = if lines.iter().zip(&content_lines).any(|(line, text)| {
1545 text.contains('|')
1546 && (line.visual_indent >= 4
1547 || line
1548 .blockquote
1549 .as_deref()
1550 .is_some_and(|quote| heading_detection::prefix_may_hide_block("e.prefix)))
1551 }) {
1552 let mut definition_lines = vec![false; if mdx_context.is_some() { lines.len() } else { 0 }];
1555 let definitions = if mdx_context.is_some() {
1556 for definition in &reference_defs {
1557 let first = line_offsets.partition_point(|&offset| offset <= definition.byte_offset);
1558 let end = line_offsets
1559 .partition_point(|&offset| offset < definition.byte_end.max(definition.byte_offset + 1));
1560 for flag in
1561 &mut definition_lines[first.saturating_sub(1).min(lines.len())..end.min(lines.len())]
1562 {
1563 *flag = true;
1564 }
1565 }
1566 Some(heading_detection::Definitions::Lines(&definition_lines))
1567 } else {
1568 text_start_lines
1569 .as_deref()
1570 .map(heading_detection::Definitions::TextStarts)
1571 };
1572 if reference_defs.is_empty()
1577 && text_start_lines.is_none()
1578 && let Some(lazy_text) = lazy_table_text
1579 {
1580 lazy_text
1581 } else {
1582 heading_detection::indented_text_lines(
1583 &content_lines,
1584 &lines,
1585 flavor,
1586 &html_blocks,
1587 &code_blocks,
1588 &code_span_ranges,
1589 mdx_flow_lines.as_deref(),
1590 mdx_context
1591 .as_ref()
1592 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1593 definitions,
1594 )
1595 }
1596 } else {
1597 Vec::new()
1598 };
1599 if flavor == MarkdownFlavor::MDG {
1600 for (flag, line) in lazy_text.iter_mut().zip(&lines) {
1603 *flag &= line.blockquote.is_some();
1604 }
1605 }
1606
1607 let table_blocks = profile_section!(
1609 "Table blocks",
1610 profile,
1611 if char_frequency.pipe_count == 0 {
1612 Vec::new()
1613 } else {
1614 crate::utils::table_utils::TableUtils::find_table_blocks_with_layout(
1615 &content_lines,
1616 &line_offsets,
1617 &code_blocks,
1618 &inline_code_ranges,
1619 &html_comment_ranges,
1620 flavor,
1621 &lazy_text,
1622 &list_items,
1623 )
1624 }
1625 );
1626 let list_item_parent: Vec<Option<usize>> = {
1627 let mut open: Vec<usize> = Vec::new();
1628 list_items
1629 .iter()
1630 .enumerate()
1631 .map(|(index, &(start, _, _))| {
1632 while open.last().is_some_and(|&top| list_items[top].1 <= start) {
1633 open.pop();
1634 }
1635 let parent = open.last().copied();
1636 open.push(index);
1637 parent
1638 })
1639 .collect()
1640 };
1641 let table_blocks: Vec<_> = table_blocks
1645 .into_iter()
1646 .filter(|block| flavor == MarkdownFlavor::MDX || !lines[block.header_line].in_markdown_span)
1647 .collect();
1648 let table_blocks = table_confirm::confirm_table_blocks(
1649 table_blocks,
1650 &table_confirm::Evidence {
1651 content,
1652 lines: &lines,
1653 line_offsets: &line_offsets,
1654 flavor,
1655 table_ranges: &table_ranges,
1656 top_level_blocks: &top_level_blocks,
1657 rejected_definitions: &rejected_definition_ranges,
1658 list_items: &list_items,
1659 list_item_parent: &list_item_parent,
1660 parser_code_blocks: &parser_code_blocks,
1661 native_tables: mdx_context.as_ref().map_or(&[][..], |mdx| &mdx.tables[..]),
1662 html_above: std::cell::OnceCell::new(),
1663 opener_body: std::cell::OnceCell::new(),
1664 rejected_definition_reach: std::cell::OnceCell::new(),
1665 },
1666 );
1667
1668 let links = links
1671 .into_iter()
1672 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1673 .collect::<Vec<_>>();
1674 let images = images
1675 .into_iter()
1676 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1677 .collect::<Vec<_>>();
1678 let broken_links = broken_links
1679 .into_iter()
1680 .filter(|bl| {
1681 let line_idx = line_offsets
1683 .partition_point(|&offset| offset <= bl.span.start)
1684 .saturating_sub(1);
1685 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1686 })
1687 .collect::<Vec<_>>();
1688 let footnote_refs = footnote_refs
1689 .into_iter()
1690 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1691 .collect::<Vec<_>>();
1692 let reference_defs = reference_defs
1693 .into_iter()
1694 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1695 .collect::<Vec<_>>();
1696 let table_blocks = table_blocks
1697 .into_iter()
1698 .filter(|block| {
1699 !lines
1701 .get(block.start_line)
1702 .is_some_and(|l| l.in_kramdown_extension_block)
1703 })
1704 .collect::<Vec<_>>();
1705 let emphasis_spans = emphasis_spans
1706 .into_iter()
1707 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1708 .collect::<Vec<_>>();
1709
1710 for block in &list_blocks {
1714 for line_num in block.start_line..=block.end_line {
1716 if let Some(li) = lines.get_mut(line_num - 1) {
1717 li.in_list_block = true;
1718 }
1719 }
1720 }
1721 for block in &table_blocks {
1722 for idx in block.start_line..=block.end_line {
1724 if let Some(li) = lines.get_mut(idx) {
1725 li.in_table_block = true;
1726 }
1727 }
1728 }
1729
1730 let reference_defs_map: HashMap<String, usize> = reference_defs
1732 .iter()
1733 .enumerate()
1734 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1735 .collect();
1736
1737 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1739 .iter()
1740 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1741 (Some(start), Some(end)) => Some((start, end)),
1742 _ => None,
1743 })
1744 .collect();
1745
1746 let jinja_ranges = profile_section!(
1748 "Jinja ranges",
1749 profile,
1750 crate::utils::jinja_utils::find_jinja_ranges(content)
1751 );
1752
1753 let citation_ranges = profile_section!("Citation ranges", profile, {
1755 if flavor.is_pandoc_compatible() {
1756 crate::utils::pandoc::find_citation_ranges(content)
1757 } else {
1758 Vec::new()
1759 }
1760 });
1761
1762 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1764 if flavor.is_pandoc_compatible() {
1765 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1766 } else {
1767 Vec::new()
1768 }
1769 });
1770
1771 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1773 if flavor.is_pandoc_compatible() {
1774 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1775 } else {
1776 std::collections::HashSet::new()
1777 }
1778 });
1779
1780 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1782 if flavor.is_pandoc_compatible() {
1783 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1784 } else {
1785 Vec::new()
1786 }
1787 });
1788
1789 let example_reference_ranges = profile_section!("Example references", profile, {
1791 if flavor.is_pandoc_compatible() {
1792 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1793 } else {
1794 Vec::new()
1795 }
1796 });
1797
1798 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1800 if flavor.is_pandoc_compatible() {
1801 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1802 } else {
1803 Vec::new()
1804 }
1805 });
1806
1807 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1809 if flavor.is_pandoc_compatible() {
1810 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1811 } else {
1812 Vec::new()
1813 }
1814 });
1815
1816 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1818 if flavor.is_pandoc_compatible() {
1819 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1820 } else {
1821 Vec::new()
1822 }
1823 });
1824
1825 let line_block_ranges = profile_section!("Line block ranges", profile, {
1827 if flavor.is_pandoc_compatible() {
1828 crate::utils::pandoc::detect_line_block_ranges(content)
1829 } else {
1830 Vec::new()
1831 }
1832 });
1833
1834 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1836 if flavor.is_pandoc_compatible() {
1837 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1838 } else {
1839 Vec::new()
1840 }
1841 });
1842
1843 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1845 if flavor.is_pandoc_compatible() {
1846 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1847 } else {
1848 Vec::new()
1849 }
1850 });
1851
1852 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1854 if flavor.is_pandoc_compatible() {
1855 crate::utils::pandoc::detect_grid_table_ranges(content)
1856 } else {
1857 Vec::new()
1858 }
1859 });
1860
1861 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1863 if flavor.is_pandoc_compatible() {
1864 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1865 } else {
1866 Vec::new()
1867 }
1868 });
1869
1870 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1872 crate::utils::shortcode_utils::shortcode_ranges(content).collect()
1873 });
1874
1875 let ctx = Self {
1876 content,
1877
1878 native_list_spacing_plan: OnceLock::new(),
1879
1880 list_guard_parse: OnceLock::new(),
1881
1882 parser_blocks: parser_blocks.map_or_else(OnceLock::new, OnceLock::from),
1883
1884 parse_handoff: std::sync::Mutex::new(None),
1885 merge_conflicts: OnceLock::new(),
1886 content_lines,
1887 line_candidates,
1888 lexical_emphasis: OnceLock::new(),
1889 document_lines: OnceLock::new(),
1890 line_offsets,
1891 code_blocks,
1892 code_block_details,
1893 strong_spans,
1894 line_to_list,
1895 list_start_values,
1896 definition_lists,
1897 colon_definition_lines,
1898 commonmark_ordered_lists_cache: OnceLock::new(),
1899 lines,
1900 blockquote_headings,
1901 document_anchors_cache: OnceLock::new(),
1902 link_destinations_cache: OnceLock::new(),
1903 links,
1904 rendered_link_ends_cache: OnceLock::new(),
1905 parsed_link_ranges,
1906 images,
1907 broken_links,
1908 footnote_refs,
1909 reference_defs,
1910 reference_defs_map,
1911 inline_code_ranges,
1912 code_spans_cache: OnceLock::new(),
1913 source_code_spans: OnceLock::new(),
1914 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1916 math_byte_ranges_cache: OnceLock::new(), parser_has_math,
1919 code_spans_without_math_cache,
1920 list_blocks,
1921 char_frequency,
1922 html_tags_cache: OnceLock::new(),
1923 jsx_component_tags_cache: OnceLock::new(),
1924 source_emphasis: emphasis_spans,
1925 emphasis_spans_cache: OnceLock::new(),
1926 bare_urls_cache: OnceLock::new(),
1927 has_mixed_list_nesting_cache: OnceLock::new(),
1928 html_comment_ranges,
1929 html_block_lines,
1930 html_block_ranges,
1931 table_blocks,
1932 parser_table_ranges: table_ranges,
1933 jinja_ranges,
1934 jinja_string_ranges_cache: OnceLock::new(),
1935 flavor,
1936 source_file,
1937 link_target_policy: None,
1938 invalid_utf8: None,
1939 jsx_expression_ranges,
1940 mdx_esm_ranges,
1941 mdx_comment_ranges,
1942 citation_ranges,
1943 pandoc_div_ranges,
1944 colon_fence_details,
1945 inline_footnote_ranges,
1946 pandoc_header_slugs,
1947 example_list_marker_ranges,
1948 example_reference_ranges,
1949 sub_super_ranges,
1950 inline_code_attr_ranges,
1951 bracketed_span_ranges,
1952 line_block_ranges,
1953 pipe_table_caption_ranges,
1954 pandoc_metadata_ranges,
1955 grid_table_ranges,
1956 multi_line_table_ranges,
1957 shortcode_ranges,
1958 link_title_ranges,
1959 code_span_byte_ranges: code_span_ranges,
1960 inline_config,
1961 obsidian_comment_ranges,
1962 unterminated_html_comment,
1963 unterminated_obsidian_comment,
1964 lazy_cont_lines_cache: OnceLock::new(),
1965 myst_directive_ranges,
1966 myst_comment_ranges,
1967 myst_role_ranges,
1968 front_matter_end,
1969 };
1970 if let Some(mdx) = mdx_context {
1971 let (html_tags, jsx_component_tags) = mdx.html_tags(content, &ctx.lines);
1972 let _ = ctx.html_tags_cache.set(Arc::new(ctx.filter_kramdown_tags(html_tags)));
1973 let _ = ctx
1974 .jsx_component_tags_cache
1975 .set(Arc::new(ctx.filter_kramdown_tags(jsx_component_tags)));
1976 }
1977 ctx
1978 };
1979 (structure, finish)
1980 }
1981
1982 pub fn front_matter_end_line(&self) -> usize {
1987 self.front_matter_end
1988 }
1989
1990 #[inline]
1993 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1994 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1996 idx > 0 && pos < ranges[idx - 1].1
1998 }
1999
2000 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
2002 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
2003 }
2004
2005 pub fn is_in_code_span_byte_with_or_without_math(&self, pos: usize) -> bool {
2011 self.native_list_source().is_in_code_span_byte_with_or_without_math(pos)
2012 }
2013
2014 pub fn is_code_span_text_byte(&self, pos: usize) -> bool {
2020 self.is_byte_offset_in_code_span(pos) || self.is_in_code_span_byte_with_or_without_math(pos)
2021 }
2022
2023 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
2028 let line = &self.lines[line_number - 1];
2029 heading_detection::ends_with_hard_break(
2030 line.content(self.content),
2031 line.byte_offset,
2032 &self.code_span_byte_ranges,
2033 )
2034 }
2035
2036 pub fn is_in_link(&self, pos: usize) -> bool {
2038 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
2039 }
2040
2041 pub fn is_in_bare_url(&self, pos: usize) -> bool {
2043 let bare_urls = self.bare_urls();
2044 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
2046 idx > 0 && pos < bare_urls[idx - 1].byte_end
2047 }
2048
2049 pub fn inline_config(&self) -> &InlineConfig {
2051 &self.inline_config
2052 }
2053
2054 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
2059 &self.colon_fence_details
2060 }
2061
2062 pub fn raw_lines(&self) -> &[&'a str] {
2066 &self.content_lines
2067 }
2068
2069 pub(crate) fn candidate_line_indices(&self, kind: LineCandidateKind) -> &[usize] {
2072 self.line_candidates.lines(kind)
2073 }
2074
2075 pub(crate) fn lexical_emphasis(&self) -> impl Iterator<Item = EmphasisLine<'_>> {
2078 self.lexical_emphasis
2079 .get_or_init(|| emphasis::collect(self))
2080 .lines(self.content)
2081 }
2082
2083 fn document_lines(&self) -> &line_facts::DocumentLines {
2084 self.document_lines
2085 .get_or_init(|| line_facts::DocumentLines::new(&self.lines, &self.blockquote_headings))
2086 }
2087
2088 pub(crate) fn line_context_masks(&self) -> &[LineContextMask] {
2089 &self.document_lines().contexts
2090 }
2091
2092 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
2097 self.inline_config.is_rule_disabled(rule_name, line_number)
2098 }
2099
2100 pub(crate) fn inline_code_ranges(&self) -> &[(usize, usize)] {
2102 &self.inline_code_ranges
2103 }
2104
2105 pub(crate) fn source_code_spans(&self) -> &[SourceCodeSpan] {
2106 self.source_code_spans.get_or_init(|| {
2107 element_parsers::build_code_spans_from_ranges(self.content, &self.lines, &self.inline_code_ranges)
2108 })
2109 }
2110
2111 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
2113 Arc::clone(self.code_spans_cache.get_or_init(|| {
2114 Arc::new(
2115 self.source_code_spans()
2116 .iter()
2117 .map(|span| span.materialize(self.content))
2118 .collect(),
2119 )
2120 }))
2121 }
2122
2123 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
2127 self.math_byte_ranges_cache
2128 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
2129 }
2130
2131 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
2133 Arc::clone(
2134 self.math_spans_cache
2135 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
2136 )
2137 }
2138
2139 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
2141 self.bracket_math_cache
2142 .get_or_init(|| bracket_math::parse(self.content, &self.lines, self.source_code_spans(), &self.list_blocks))
2143 }
2144
2145 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
2147 let math_spans = self.math_spans();
2148 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
2150 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
2151 }
2152
2153 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2155 &self.html_comment_ranges
2156 }
2157
2158 pub fn unterminated_html_comment(&self) -> Option<usize> {
2163 self.unterminated_html_comment
2164 }
2165
2166 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
2170 self.unterminated_obsidian_comment
2171 }
2172
2173 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
2177 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
2178 }
2179
2180 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
2185 if self.obsidian_comment_ranges.is_empty() {
2186 return false;
2187 }
2188
2189 let byte_pos = self.line_index().line_col_to_byte_range(line_num, col).start;
2191 self.is_in_obsidian_comment(byte_pos)
2192 }
2193
2194 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
2196 &self.myst_directive_ranges
2197 }
2198
2199 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
2201 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
2202 }
2203
2204 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
2206 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
2207 }
2208
2209 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
2216 if !self.flavor.supports_myst_directives() {
2217 return false;
2218 }
2219 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
2220 info.in_myst_directive
2221 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
2222 })
2223 }
2224
2225 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
2227 tags.into_iter()
2228 .filter(|tag| {
2229 !self
2230 .lines
2231 .get(tag.line - 1)
2232 .is_some_and(|l| l.in_kramdown_extension_block)
2233 })
2234 .collect()
2235 }
2236
2237 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
2244 Arc::clone(self.html_tags_shared())
2245 }
2246
2247 fn html_tags_shared(&self) -> &Arc<Vec<HtmlTag>> {
2248 self.html_tags_cache.get_or_init(|| {
2249 let (html_tags, jsx_component_tags) =
2250 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
2251 let _ = self
2253 .jsx_component_tags_cache
2254 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
2255 Arc::new(self.filter_kramdown_tags(html_tags))
2256 })
2257 }
2258
2259 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
2262 Arc::clone(self.jsx_component_tags_shared())
2263 }
2264
2265 fn jsx_component_tags_shared(&self) -> &Arc<Vec<HtmlTag>> {
2266 if let Some(cached) = self.jsx_component_tags_cache.get() {
2267 return cached;
2268 }
2269 let _ = self.html_tags_shared();
2271 self.jsx_component_tags_cache
2272 .get()
2273 .expect("HTML parse populates jsx_component_tags_cache")
2274 }
2275
2276 pub(crate) fn source_emphasis_spans(&self) -> &[SourceEmphasisSpan] {
2277 &self.source_emphasis
2278 }
2279
2280 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
2282 Arc::clone(self.emphasis_spans_cache.get_or_init(|| {
2283 Arc::new(
2284 self.source_emphasis
2285 .iter()
2286 .map(|span| span.materialize(self.content))
2287 .collect(),
2288 )
2289 }))
2290 }
2291
2292 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
2294 Arc::clone(self.bare_urls_cache.get_or_init(|| {
2295 Arc::new(element_parsers::parse_bare_urls(
2296 self.content,
2297 &self.lines,
2298 &self.code_blocks,
2299 ))
2300 }))
2301 }
2302
2303 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
2305 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
2306 Arc::new(element_parsers::detect_lazy_continuation_lines(
2307 self.content,
2308 &self.lines,
2309 &self.line_offsets,
2310 ))
2311 }))
2312 }
2313
2314 pub fn has_mixed_list_nesting(&self) -> bool {
2318 *self
2319 .has_mixed_list_nesting_cache
2320 .get_or_init(|| self.compute_mixed_list_nesting())
2321 }
2322
2323 fn compute_mixed_list_nesting(&self) -> bool {
2325 let mut stack: Vec<(usize, bool)> = Vec::new();
2330 let mut last_was_blank = false;
2331
2332 for line_info in &self.lines {
2333 if line_info.in_code_block
2335 || line_info.in_front_matter
2336 || line_info.in_mkdocstrings
2337 || line_info.in_html_comment
2338 || line_info.in_mdx_comment
2339 || line_info.in_esm_block
2340 {
2341 continue;
2342 }
2343
2344 if line_info.is_blank {
2346 last_was_blank = true;
2347 continue;
2348 }
2349
2350 if let Some(list_item) = &line_info.list_item {
2351 let current_pos = if list_item.marker_column == 1 {
2353 0
2354 } else {
2355 list_item.marker_column
2356 };
2357
2358 if last_was_blank && current_pos == 0 {
2360 stack.clear();
2361 }
2362 last_was_blank = false;
2363
2364 while let Some(&(pos, _)) = stack.last() {
2366 if pos >= current_pos {
2367 stack.pop();
2368 } else {
2369 break;
2370 }
2371 }
2372
2373 if let Some(&(_, parent_is_ordered)) = stack.last()
2375 && parent_is_ordered != list_item.is_ordered
2376 {
2377 return true; }
2379
2380 stack.push((current_pos, list_item.is_ordered));
2381 } else {
2382 last_was_blank = false;
2384 }
2385 }
2386
2387 false
2388 }
2389
2390 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
2396 native_structure::offset_to_line_col(self.content, &self.line_offsets, offset)
2397 }
2398
2399 pub(crate) fn line_number_at_offset(&self, offset: usize) -> usize {
2402 self.line_offsets.partition_point(|&start| start <= offset)
2403 }
2404
2405 pub fn html_block_range_at(&self, byte_offset: usize) -> Option<(usize, usize)> {
2408 let index = self
2409 .html_block_ranges
2410 .partition_point(|&(start, _)| start <= byte_offset);
2411 self.html_block_ranges
2412 .get(index.checked_sub(1)?)
2413 .copied()
2414 .filter(|&(_, end)| byte_offset < end)
2415 }
2416
2417 fn line_index(&self) -> crate::utils::range_utils::LineIndex<'_> {
2421 crate::utils::range_utils::LineIndex::for_positions(self.content, &self.line_offsets)
2422 }
2423
2424 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
2430 line_number
2431 .checked_sub(1)
2432 .and_then(|index| self.line_offsets.get(index).copied())
2433 }
2434
2435 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
2441 self.line_index().line_col_to_byte_range(line_number, column)
2442 }
2443
2444 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
2449 self.line_index()
2450 .line_col_to_byte_range_with_length(line_number, column, length)
2451 }
2452
2453 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
2456 self.line_index().whole_line_range(line_number)
2457 }
2458
2459 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
2464 self.line_index().line_text_range(line_number, start_column, end_column)
2465 }
2466
2467 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
2470 self.line_index().line_content_range(line_number)
2471 }
2472
2473 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
2475 self.line_index().multi_line_range(start_line, end_line)
2476 }
2477
2478 pub(crate) fn parsed_table_range_at_line(&self, line: usize) -> Option<(usize, usize)> {
2482 let span = self.line_content_byte_range(line);
2483 let index = self
2484 .parser_table_ranges
2485 .partition_point(|&(start, _)| start < span.start);
2486 self.parser_table_ranges
2487 .get(index)
2488 .copied()
2489 .filter(|&(start, _)| start < span.end)
2490 }
2491
2492 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
2494 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
2496 return true;
2497 }
2498
2499 self.is_byte_offset_in_code_span(pos)
2501 }
2502
2503 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
2505 if line_num > 0 {
2506 self.lines.get(line_num - 1)
2507 } else {
2508 None
2509 }
2510 }
2511
2512 pub(crate) fn document_anchors(&self) -> &anchors::DocumentAnchors {
2515 self.document_anchors_cache
2516 .get_or_init(|| anchors::DocumentAnchors::new(self))
2517 }
2518
2519 pub fn links(&self) -> &[ParsedLink<'a>] {
2521 &self.links
2522 }
2523
2524 #[inline]
2527 pub(crate) fn is_in_parsed_link(&self, pos: usize) -> bool {
2528 Self::binary_search_ranges(&self.parsed_link_ranges, pos)
2529 }
2530
2531 pub(crate) fn has_parsed_link_start_in(&self, start: usize, end: usize) -> bool {
2534 let index = self.parsed_link_ranges.partition_point(|&(open, _)| open < start);
2535 self.parsed_link_ranges.get(index).is_some_and(|&(open, _)| open < end)
2536 }
2537
2538 pub fn images(&self) -> &[ParsedImage<'a>] {
2540 &self.images
2541 }
2542
2543 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
2545 &self.broken_links
2546 }
2547
2548 pub fn footnote_references(&self) -> &[FootnoteRef] {
2550 &self.footnote_refs
2551 }
2552
2553 pub fn reference_definitions(&self) -> &[ReferenceDef] {
2555 &self.reference_defs
2556 }
2557
2558 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
2560 let start = self.links.partition_point(|link| link.line < line_number);
2561 let end = self.links.partition_point(|link| link.line <= line_number);
2562 &self.links[start..end]
2563 }
2564
2565 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
2567 let start = self.images.partition_point(|image| image.line < line_number);
2568 let end = self.images.partition_point(|image| image.line <= line_number);
2569 &self.images[start..end]
2570 }
2571
2572 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2574 self.links
2575 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
2576 .ok()
2577 .map(|index| &self.links[index])
2578 }
2579
2580 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2582 self.images
2583 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
2584 .ok()
2585 .map(|index| &self.images[index])
2586 }
2587
2588 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2590 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2591 self.links
2592 .get(index.checked_sub(1)?)
2593 .filter(|link| byte_offset < link.byte_end)
2594 }
2595
2596 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2598 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
2599 self.images
2600 .get(index.checked_sub(1)?)
2601 .filter(|image| byte_offset < image.byte_end)
2602 }
2603
2604 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
2606 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2607 &self.links[..end]
2608 }
2609
2610 pub(crate) fn is_range_in_rendered_link(&self, range: &Range<usize>) -> bool {
2614 let count = self.links.partition_point(|link| link.byte_offset <= range.start);
2615 if count == 0 {
2616 return false;
2617 }
2618 let ends = self.rendered_link_ends_cache.get_or_init(|| {
2619 let mut end = 0;
2620 self.links
2621 .iter()
2622 .map(|link| {
2623 if !(link.is_reference && link.url.is_empty()) {
2624 end = end.max(link.byte_end);
2625 }
2626 end
2627 })
2628 .collect()
2629 });
2630 ends[count - 1] != 0 && ends[count - 1] >= range.end
2631 }
2632
2633 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
2635 let normalized_id = ref_id.to_lowercase();
2636 self.reference_defs_map
2637 .get(&normalized_id)
2638 .map(|&index| &self.reference_defs[index])
2639 }
2640
2641 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
2643 self.reference_definition(ref_id)
2644 .map(|definition| definition.url.as_str())
2645 }
2646
2647 pub fn heading_container(&self, line_num: usize) -> HeadingContainer {
2655 self.line_info(line_num)
2656 .and_then(|line| line.heading_container)
2657 .unwrap_or(HeadingContainer::Unknown)
2658 }
2659
2660 pub fn is_in_list_block(&self, line_num: usize) -> bool {
2662 if line_num == 0 || line_num > self.lines.len() {
2663 return false;
2664 }
2665 self.lines[line_num - 1].in_list_block
2666 }
2667
2668 pub fn is_in_definition_list(&self, line_num: usize) -> bool {
2676 let lists = &self.definition_lists.outer;
2677 let idx = lists.partition_point(|&(start, _)| start <= line_num);
2678 idx > 0 && line_num <= lists[idx - 1].1
2679 }
2680
2681 pub fn is_definition_term(&self, line_num: usize) -> bool {
2683 let terms = &self.definition_lists.terms;
2684 let idx = terms.partition_point(|&(_, end)| end < line_num);
2685 terms.get(idx).is_some_and(|&(start, _)| start <= line_num)
2686 }
2687
2688 pub fn in_colon_definition(&self, line_num: usize) -> bool {
2696 let ranges = &self.colon_definition_lines;
2697 let idx = ranges.partition_point(|&(_, end)| end < line_num);
2698 ranges.get(idx).is_some_and(|&(start, _)| start <= line_num)
2699 }
2700
2701 pub fn definition_text_at(&self, line_num: usize) -> Option<&DefinitionText> {
2703 let texts = &self.definition_lists.texts;
2704 let idx = texts.partition_point(|text| text.end_line < line_num);
2705 texts.get(idx).filter(|text| text.start_line <= line_num)
2706 }
2707
2708 pub fn line_holds_html_block(&self, line_idx: usize) -> bool {
2713 self.native_list_source().line_holds_html_block(line_idx)
2714 }
2715
2716 pub fn is_in_html_block(&self, line_num: usize) -> bool {
2718 if line_num == 0 || line_num > self.lines.len() {
2719 return false;
2720 }
2721 self.lines[line_num - 1].in_html_block
2722 }
2723
2724 pub fn is_in_table_block(&self, line_num: usize) -> bool {
2730 if line_num == 0 || line_num > self.lines.len() {
2731 return false;
2732 }
2733 self.lines[line_num - 1].in_table_block
2734 }
2735
2736 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
2738 if line_num == 0 || line_num > self.lines.len() {
2739 return false;
2740 }
2741
2742 let col_0indexed = if col > 0 { col - 1 } else { 0 };
2746 let code_spans = self.source_code_spans();
2747 code_spans.iter().any(|span| {
2748 if line_num < span.line || line_num > span.end_line {
2750 return false;
2751 }
2752
2753 if span.line == span.end_line {
2754 col_0indexed >= span.start_col && col_0indexed < span.end_col
2756 } else if line_num == span.line {
2757 col_0indexed >= span.start_col
2759 } else if line_num == span.end_line {
2760 col_0indexed < span.end_col
2762 } else {
2763 true
2765 }
2766 })
2767 }
2768
2769 #[inline]
2771 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
2772 Self::binary_search_ranges(&self.inline_code_ranges, byte_offset)
2773 }
2774
2775 #[inline]
2777 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
2778 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
2779 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
2780 }
2781
2782 #[inline]
2784 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
2785 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
2786 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
2787 }
2788
2789 #[inline]
2792 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
2793 let tags = self.html_tags_shared();
2794
2795 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2796 idx > 0 && byte_pos < tags[idx - 1].byte_end
2797 }
2798
2799 #[inline]
2803 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
2804 if !self.flavor.supports_jsx() {
2805 return false;
2806 }
2807
2808 let tags = self.jsx_component_tags_shared();
2809
2810 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2811 idx > 0 && byte_pos < tags[idx - 1].byte_end
2812 }
2813
2814 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
2816 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
2817 }
2818
2819 #[inline]
2822 pub(crate) fn overlaps_template_code(&self, start: usize, end: usize) -> bool {
2823 if start >= end {
2824 return false;
2825 }
2826 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2827 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2828 idx > 0 && ranges[idx - 1].1 > start
2829 })
2830 }
2831
2832 #[inline]
2835 pub(crate) fn is_inside_template_code(&self, pos: usize) -> bool {
2836 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2837 let idx = ranges.partition_point(|&(start, _)| start < pos);
2838 idx > 0 && ranges[idx - 1].1 > pos
2839 })
2840 }
2841
2842 #[inline]
2844 pub(crate) fn is_in_jinja_string(&self, byte_pos: usize) -> bool {
2845 let ranges = self
2846 .jinja_string_ranges_cache
2847 .get_or_init(|| crate::utils::jinja_utils::find_jinja_string_ranges(self.content, &self.jinja_ranges));
2848 Self::binary_search_ranges(ranges, byte_pos)
2849 }
2850
2851 #[inline]
2853 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
2854 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
2855 }
2856
2857 #[inline]
2859 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
2860 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
2861 }
2862
2863 pub(crate) fn mdx_comment_ranges(&self) -> &[(usize, usize)] {
2865 &self.mdx_comment_ranges
2866 }
2867
2868 pub(crate) fn mdx_inline_code_ranges(&self, start: usize, end: usize) -> impl Iterator<Item = (usize, usize)> + '_ {
2870 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2871 .into_iter()
2872 .flat_map(move |ranges| {
2873 let first = ranges.partition_point(|&(_, range_end)| range_end <= start);
2874 ranges[first..]
2875 .iter()
2876 .copied()
2877 .take_while(move |&(range_start, _)| start < end && range_start < end)
2878 })
2879 }
2880
2881 #[inline]
2884 pub(crate) fn overlaps_mdx_inline_code(&self, start: usize, end: usize) -> bool {
2885 if start >= end {
2886 return false;
2887 }
2888 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2889 .into_iter()
2890 .any(|ranges| {
2891 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2892 idx > 0 && ranges[idx - 1].1 > start
2893 })
2894 }
2895
2896 pub(crate) fn is_inside_mdx_code(&self, pos: usize) -> bool {
2899 if self.flavor != MarkdownFlavor::MDX {
2900 return false;
2901 }
2902 if [
2903 &self.mdx_esm_ranges,
2904 &self.jsx_expression_ranges,
2905 &self.mdx_comment_ranges,
2906 ]
2907 .into_iter()
2908 .any(|ranges| {
2909 let idx = ranges.partition_point(|&(start, _)| start < pos);
2910 idx > 0 && ranges[idx - 1].1 > pos
2911 }) {
2912 return true;
2913 }
2914
2915 let tags = [self.html_tags_shared(), self.jsx_component_tags_shared()];
2916
2917 tags.into_iter().any(|tags| {
2918 let idx = tags.partition_point(|tag| tag.byte_offset < pos);
2919 idx > 0 && tags[idx - 1].byte_end > pos
2920 })
2921 }
2922
2923 pub(crate) fn overlaps_mdx_code(&self, start: usize, end: usize) -> bool {
2925 if start >= end || self.flavor != MarkdownFlavor::MDX {
2926 return false;
2927 }
2928 if [
2929 &self.mdx_esm_ranges,
2930 &self.jsx_expression_ranges,
2931 &self.mdx_comment_ranges,
2932 ]
2933 .into_iter()
2934 .any(|ranges| {
2935 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2936 idx > 0 && ranges[idx - 1].1 > start
2937 }) {
2938 return true;
2939 }
2940
2941 let tags = [self.html_tags_shared(), self.jsx_component_tags_shared()];
2942
2943 tags.into_iter().any(|tags| {
2944 let idx = tags.partition_point(|tag| tag.byte_offset < end);
2945 idx > 0 && tags[idx - 1].byte_end > start
2946 })
2947 }
2948
2949 #[inline]
2952 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
2953 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
2954 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
2955 }
2956
2957 #[inline]
2959 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2960 &self.citation_ranges
2961 }
2962
2963 #[inline]
2966 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
2967 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
2968 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
2969 }
2970
2971 #[inline]
2974 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
2975 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
2976 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
2977 }
2978
2979 #[inline]
2982 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2983 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2984 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2985 }
2986
2987 #[inline]
2990 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2991 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2992 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2993 }
2994
2995 #[inline]
2998 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2999 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
3000 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
3001 }
3002
3003 #[inline]
3007 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
3008 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
3009 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
3010 }
3011
3012 #[inline]
3015 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
3016 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
3017 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
3018 }
3019
3020 #[inline]
3023 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
3024 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
3025 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
3026 }
3027
3028 #[inline]
3032 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
3033 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
3034 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
3035 }
3036
3037 #[inline]
3040 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
3041 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
3042 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
3043 }
3044
3045 #[inline]
3048 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
3049 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
3050 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
3051 }
3052
3053 #[inline]
3056 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
3057 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
3058 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
3059 }
3060
3061 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
3066 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
3067 self.pandoc_header_slugs.contains(&slug)
3068 }
3069
3070 #[inline]
3076 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
3077 self.pandoc_header_slugs.contains(slug)
3078 }
3079
3080 #[inline]
3082 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
3083 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
3084 }
3085
3086 #[inline]
3088 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
3089 &self.shortcode_ranges
3090 }
3091
3092 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
3094 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
3095 }
3096
3097 pub fn has_char(&self, ch: char) -> bool {
3099 match ch {
3100 '#' => self.char_frequency.hash_count > 0,
3101 '*' => self.char_frequency.asterisk_count > 0,
3102 '_' => self.char_frequency.underscore_count > 0,
3103 '-' => self.char_frequency.hyphen_count > 0,
3104 '+' => self.char_frequency.plus_count > 0,
3105 '>' => self.char_frequency.gt_count > 0,
3106 '|' => self.char_frequency.pipe_count > 0,
3107 '[' => self.char_frequency.bracket_count > 0,
3108 '`' => self.char_frequency.backtick_count > 0,
3109 '<' => self.char_frequency.lt_count > 0,
3110 '!' => self.char_frequency.exclamation_count > 0,
3111 '\n' => self.char_frequency.newline_count > 0,
3112 '$' => self.line_candidates.has_signal(candidates::DOLLAR),
3113 '=' => self.line_candidates.has_signal(candidates::EQUALS),
3114 _ => self.content.contains(ch), }
3116 }
3117
3118 pub fn char_count(&self, ch: char) -> usize {
3120 match ch {
3121 '#' => self.char_frequency.hash_count,
3122 '*' => self.char_frequency.asterisk_count,
3123 '_' => self.char_frequency.underscore_count,
3124 '-' => self.char_frequency.hyphen_count,
3125 '+' => self.char_frequency.plus_count,
3126 '>' => self.char_frequency.gt_count,
3127 '|' => self.char_frequency.pipe_count,
3128 '[' => self.char_frequency.bracket_count,
3129 '`' => self.char_frequency.backtick_count,
3130 '<' => self.char_frequency.lt_count,
3131 '!' => self.char_frequency.exclamation_count,
3132 '\n' => self.char_frequency.newline_count,
3133 _ => self.content.matches(ch).count(), }
3135 }
3136
3137 pub fn likely_has_headings(&self) -> bool {
3140 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.has_char('=')
3141 }
3142
3143 pub fn likely_has_lists(&self) -> bool {
3147 self.char_frequency.asterisk_count > 0
3148 || self.char_frequency.hyphen_count > 0
3149 || self.char_frequency.plus_count > 0
3150 }
3151
3152 pub fn likely_has_emphasis(&self) -> bool {
3154 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
3155 }
3156
3157 pub fn likely_has_tables(&self) -> bool {
3159 self.char_frequency.pipe_count > 2
3160 }
3161
3162 pub fn likely_has_blockquotes(&self) -> bool {
3164 self.char_frequency.gt_count > 0
3165 }
3166
3167 pub fn likely_has_code(&self) -> bool {
3169 self.char_frequency.backtick_count > 0
3170 }
3171
3172 pub fn likely_has_links_or_images(&self) -> bool {
3174 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
3175 }
3176
3177 pub fn likely_has_html(&self) -> bool {
3179 self.char_frequency.lt_count > 0
3180 }
3181
3182 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
3187 if let Some(line_info) = self.lines.get(line_idx)
3188 && let Some(ref bq) = line_info.blockquote
3189 {
3190 bq.prefix.trim_end().to_string()
3191 } else {
3192 String::new()
3193 }
3194 }
3195
3196 #[cfg(test)]
3207 #[inline]
3208 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
3209 let idx = source_positions::line_index(lines, byte_offset);
3210
3211 let line = &lines[idx];
3212 let line_num = idx + 1;
3213 let byte_col = byte_offset.saturating_sub(line.byte_offset);
3214 let col = byte_to_char_count(line.content(content), byte_col) - 1;
3217
3218 (idx, line_num, col)
3219 }
3220
3221 #[must_use]
3241 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
3242 ValidHeadingsIter::new(&self.lines, &self.document_lines().headings)
3243 }
3244
3245 #[must_use]
3249 pub fn has_valid_headings(&self) -> bool {
3250 !self.document_lines().headings.is_empty()
3251 }
3252
3253 #[must_use]
3255 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
3256 ParsedListItemsIter::new(&self.lines, &self.document_lines().list_items)
3257 }
3258
3259 #[must_use]
3261 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
3262 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
3263 Some(ParsedListItem::new(
3264 line_num,
3265 line_info.list_item.as_deref()?,
3266 line_info,
3267 ))
3268 }
3269
3270 #[must_use]
3272 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
3273 ParsedListBlocks::new(&self.list_blocks, &self.lines)
3274 }
3275
3276 #[must_use]
3280 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
3281 list_blocks::item_lines_by_list(self.content, &self.lines, block)
3282 }
3283
3284 #[must_use]
3286 pub fn has_list_items(&self) -> bool {
3287 !self.document_lines().list_items.is_empty()
3288 }
3289
3290 #[must_use]
3292 pub fn has_unordered_list_items(&self) -> bool {
3293 self.document_lines().has_unordered_items
3294 }
3295
3296 #[must_use]
3298 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
3299 let lists = self
3300 .commonmark_ordered_lists_cache
3301 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
3302 CommonMarkOrderedLists::new(lists, &self.lines)
3303 }
3304
3305 #[must_use]
3313 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
3314 ParsedHeadingsIter::new(
3315 &self.lines,
3316 &self.blockquote_headings,
3317 &self.document_lines().parsed_headings,
3318 )
3319 }
3320
3321 #[must_use]
3323 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
3324 let idx = line_num.checked_sub(1)?;
3325 let line_info = self.lines.get(idx)?;
3326 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
3327 Some(heading) => (heading, 0),
3328 None => (
3329 self.blockquote_headings.get(idx)?.as_deref()?,
3330 line_info.blockquote.as_ref().map_or(1, |bq| bq.nesting_level),
3331 ),
3332 };
3333 Some(ParsedHeading {
3334 line_num,
3335 heading,
3336 line_info,
3337 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
3338 blockquote_depth,
3339 })
3340 }
3341}
3342
3343fn container_comment_range(
3355 opener: usize,
3356 containers: &flavor_detection::ContainerLines,
3357 lines: &[types::LineInfo],
3358 content: &str,
3359) -> Option<crate::utils::skip_context::ByteRange> {
3360 let line_index = lines
3361 .partition_point(|line| line.byte_offset <= opener)
3362 .checked_sub(1)?;
3363 let line = lines.get(line_index)?;
3364 if line.byte_offset + line.indent != opener {
3365 return None;
3366 }
3367 if !containers.is_container_body(line_index) {
3368 return None;
3369 }
3370 let end_line = lines.get(containers.body_end_line(line_index)?)?;
3371 Some(crate::utils::skip_context::ByteRange {
3372 start: opener,
3373 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
3374 })
3375}
3376
3377#[derive(Debug)]
3379struct DefinitionListLines {
3380 outer: Vec<(usize, usize)>,
3383 terms: Vec<(usize, usize)>,
3385 texts: Vec<DefinitionText>,
3387}
3388
3389impl DefinitionListLines {
3390 fn new(
3391 line_offsets: &[usize],
3392 items: &[(usize, usize)],
3393 terms: &[(usize, usize)],
3394 texts: &[crate::utils::code_block_utils::DefinitionTextDetail],
3395 ) -> Self {
3396 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
3399 let last_line_of = |start: usize, end: usize| line_of(end.saturating_sub(1).max(start));
3400
3401 let mut outer: Vec<(usize, usize)> = Vec::new();
3404 for &(start, end) in items {
3405 let (start_line, end_line) = (line_of(start), last_line_of(start, end));
3406 match outer.last_mut() {
3407 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
3408 _ => outer.push((start_line, end_line)),
3409 }
3410 }
3411 let terms = terms
3412 .iter()
3413 .map(|&(start, end)| (line_of(start), last_line_of(start, end)))
3414 .collect();
3415
3416 let texts = texts
3417 .iter()
3418 .map(|text| {
3419 let start_line = line_of(text.start);
3420 let line_start = line_offsets[start_line - 1];
3421 let on_marker_line = line_of(text.definition_start) == start_line;
3422 DefinitionText {
3423 start_line,
3424 end_line: last_line_of(text.start, text.end),
3425 marker_prefix_len: on_marker_line.then(|| text.start - line_start),
3426 }
3427 })
3428 .collect();
3429
3430 Self { outer, terms, texts }
3431 }
3432}
3433
3434fn detect_footnote_definitions(
3447 content: &str,
3448 lines: &mut [types::LineInfo],
3449 line_offsets: &[usize],
3450) -> Vec<(usize, usize)> {
3451 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
3452
3453 if !content.contains("[^") {
3455 return Vec::new();
3456 }
3457
3458 let options = crate::utils::parser_options::rumdl_parser_options_for(content);
3459 let parser = Parser::new_ext(content, options).into_offset_iter();
3460
3461 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
3463 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
3464 let mut indented_code_ranges: Vec<(usize, usize)> = Vec::new();
3465 let mut in_footnote = false;
3466
3467 for (event, range) in parser {
3468 match event {
3469 Event::Start(Tag::FootnoteDefinition(_)) => {
3470 in_footnote = true;
3471 footnote_ranges.push((range.start, range.end));
3472 }
3473 Event::End(TagEnd::FootnoteDefinition) => {
3474 in_footnote = false;
3475 }
3476 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
3477 fenced_code_ranges.push((range.start, range.end));
3478 }
3479 Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) if in_footnote => {
3480 indented_code_ranges.push((range.start, range.end));
3481 }
3482 _ => {}
3483 }
3484 }
3485
3486 let byte_to_line = |byte_offset: usize| -> usize {
3487 line_offsets
3488 .partition_point(|&offset| offset <= byte_offset)
3489 .saturating_sub(1)
3490 };
3491
3492 for &(start, end) in &footnote_ranges {
3494 let start_line = byte_to_line(start);
3495 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
3496
3497 for line in &mut lines[start_line..end_line] {
3498 line.in_footnote_definition = true;
3499 line.in_code_block = false;
3500 }
3501 }
3502
3503 for &(start, end) in &fenced_code_ranges {
3505 let start_line = byte_to_line(start);
3506 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
3507
3508 for line in &mut lines[start_line..end_line] {
3509 line.in_code_block = true;
3510 }
3511 }
3512 indented_code_ranges
3513}