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;
29pub(crate) use mdx::{leaf_block_ranges as mdx_leaf_block_ranges, structure_signature as mdx_structure_signature};
30mod table_confirm;
31#[cfg(test)]
32mod tests;
33
34use crate::config::MarkdownFlavor;
35use crate::inline_config::InlineConfig;
36use crate::rules::front_matter_utils::FrontMatterUtils;
37use crate::utils::code_block_utils::{CodeBlockDetail, CodeBlockUtils};
38use crate::utils::range_utils::byte_to_char_count;
39use std::collections::HashMap;
40use std::ops::Range;
41use std::path::{Path, PathBuf};
42
43#[derive(Debug, Clone)]
46pub struct LinkTargetPolicy {
47 supplied_paths: Arc<std::collections::HashSet<PathBuf>>,
48 supplied_dirs: Arc<std::collections::HashSet<PathBuf>>,
52 declared_paths: Arc<std::collections::HashSet<PathBuf>>,
55 declared_dirs: Arc<std::collections::HashSet<PathBuf>>,
56 allow_disk_fallback: bool,
57}
58
59impl LinkTargetPolicy {
60 pub fn open_world<I, P>(paths: I) -> Self
63 where
64 I: IntoIterator<Item = P>,
65 P: AsRef<Path>,
66 {
67 Self::from_paths(paths, true)
68 }
69
70 pub fn closed_world<I, P>(paths: I) -> Self
72 where
73 I: IntoIterator<Item = P>,
74 P: AsRef<Path>,
75 {
76 Self::from_paths(paths, false)
77 }
78
79 pub fn with_declared_targets<I, P, F, D>(paths: I, files: F, directories: D, allow_disk_fallback: bool) -> Self
84 where
85 I: IntoIterator<Item = P>,
86 P: AsRef<Path>,
87 F: IntoIterator<Item = PathBuf>,
88 D: IntoIterator<Item = PathBuf>,
89 {
90 let files: Vec<PathBuf> = files.into_iter().collect();
91 let directories: Vec<PathBuf> = directories.into_iter().collect();
92 let roots = Self::working_roots();
93 let declared = Self::build(
94 Vec::<PathBuf>::new(),
95 files.clone(),
96 directories.clone(),
97 allow_disk_fallback,
98 &roots,
99 );
100 let paths: Vec<PathBuf> = paths.into_iter().map(|path| path.as_ref().to_path_buf()).collect();
101 let documents = Self::build(&paths, Vec::new(), Vec::new(), allow_disk_fallback, &roots);
102 let policy = Self::build(paths, files, directories, allow_disk_fallback, &roots);
103 let declared_paths = declared
106 .supplied_paths
107 .iter()
108 .filter(|path| !documents.supplied_paths.contains(*path))
109 .cloned()
110 .collect();
111 Self {
112 declared_paths: Arc::new(declared_paths),
113 declared_dirs: declared.supplied_dirs,
114 ..policy
115 }
116 }
117
118 fn working_roots() -> Vec<PathBuf> {
119 let mut roots = Vec::new();
120 if let Ok(cwd) = std::env::current_dir() {
121 if let Ok(canonical_cwd) = cwd.canonicalize()
122 && canonical_cwd != cwd
123 {
124 roots.push(canonical_cwd);
125 }
126 roots.push(cwd);
127 }
128 roots
129 }
130
131 fn from_paths<I, P>(paths: I, allow_disk_fallback: bool) -> Self
132 where
133 I: IntoIterator<Item = P>,
134 P: AsRef<Path>,
135 {
136 Self::from_paths_with_roots(paths, allow_disk_fallback, Self::working_roots())
137 }
138
139 fn from_paths_with_roots<I, P, R, Q>(paths: I, allow_disk_fallback: bool, roots: R) -> Self
140 where
141 I: IntoIterator<Item = P>,
142 P: AsRef<Path>,
143 R: IntoIterator<Item = Q>,
144 Q: AsRef<Path>,
145 {
146 Self::build(paths, Vec::new(), Vec::new(), allow_disk_fallback, roots)
147 }
148
149 fn build<I, P, F, D, R, Q>(paths: I, files: F, directories: D, allow_disk_fallback: bool, roots: R) -> Self
150 where
151 I: IntoIterator<Item = P>,
152 P: AsRef<Path>,
153 F: IntoIterator<Item = PathBuf>,
154 D: IntoIterator<Item = PathBuf>,
155 R: IntoIterator<Item = Q>,
156 Q: AsRef<Path>,
157 {
158 let roots: Vec<PathBuf> = roots
159 .into_iter()
160 .map(|root| crate::workspace_index::normalize_relative_path(root.as_ref()))
161 .collect();
162 let mut supplied_paths = std::collections::HashSet::new();
163 let add_spellings = |set: &mut std::collections::HashSet<PathBuf>, path: &Path| {
165 set.insert(crate::workspace_index::normalize_relative_path(path));
166
167 if path.is_relative() {
168 for root in &roots {
169 set.insert(crate::workspace_index::normalize_relative_path(&root.join(path)));
170 }
171 } else {
172 let resolved = crate::discovery::resolve_for_matching(path);
178 let relative = [path, resolved.as_path()].into_iter().find_map(|spelling| {
179 roots
180 .iter()
181 .find_map(|source_root| spelling.strip_prefix(source_root).ok())
182 });
183 if let Some(relative) = relative {
184 set.insert(crate::workspace_index::normalize_relative_path(relative));
185 for root in &roots {
186 set.insert(crate::workspace_index::normalize_relative_path(&root.join(relative)));
187 }
188 }
189 }
190 };
191 for path in paths {
192 add_spellings(&mut supplied_paths, path.as_ref());
193 }
194 for path in files {
195 add_spellings(&mut supplied_paths, &path);
196 }
197 let mut declared_dirs = std::collections::HashSet::new();
198 for path in directories {
199 add_spellings(&mut declared_dirs, &path);
200 }
201 let escapes_roots = |path: &Path| {
210 if path.is_absolute() {
211 !roots.iter().any(|root| path.starts_with(root))
212 } else {
213 path.starts_with("..")
214 }
215 };
216 let outside_root = |absolute: bool| {
217 let file_parents = supplied_paths
219 .iter()
220 .filter(|path| path.is_absolute() == absolute && escapes_roots(path))
221 .filter_map(|path| path.parent());
222 let declared = declared_dirs
223 .iter()
224 .filter(|dir| dir.is_absolute() == absolute && escapes_roots(dir))
225 .map(PathBuf::as_path);
226 file_parents.chain(declared).fold(None::<&Path>, |common, dir| {
227 Some(common.map_or(dir, |common| {
228 common
229 .ancestors()
230 .find(|ancestor| dir.starts_with(ancestor))
231 .unwrap_or(common)
232 }))
233 })
234 };
235 let outside_roots = [outside_root(true), outside_root(false)];
236 let supplied_dirs = supplied_paths
237 .iter()
238 .flat_map(|path| path.ancestors().skip(1))
239 .chain(declared_dirs.iter().flat_map(|dir| dir.ancestors()))
240 .filter(|dir| {
241 !escapes_roots(dir)
242 || outside_roots
243 .iter()
244 .flatten()
245 .any(|outside_root| dir.starts_with(outside_root))
246 })
247 .map(Path::to_path_buf)
248 .collect();
249 Self {
250 supplied_paths: Arc::new(supplied_paths),
251 supplied_dirs: Arc::new(supplied_dirs),
252 declared_paths: Arc::default(),
253 declared_dirs: Arc::default(),
254 allow_disk_fallback,
255 }
256 }
257
258 pub fn declared_target_for(&self, path: &Path) -> Option<PathBuf> {
264 self.resolve_supplied(path)
265 .filter(|named| self.declared_paths.contains(named) || self.declared_dirs.contains(named))
266 }
267
268 pub fn contains(&self, path: &Path) -> bool {
269 self.supplied_paths
270 .contains(&crate::workspace_index::normalize_relative_path(path))
271 }
272
273 pub(crate) fn resolve_supplied(&self, path: &Path) -> Option<PathBuf> {
274 let normalized = crate::workspace_index::normalize_relative_path(path);
275 if self.supplied_paths.contains(&normalized) {
276 return Some(normalized);
277 }
278
279 if path.extension().is_none() {
280 for extension in ["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"] {
281 let candidate = crate::workspace_index::normalize_relative_path(&path.with_extension(extension));
282 if self.supplied_paths.contains(&candidate) {
283 return Some(candidate);
284 }
285 }
286 }
287
288 if self.supplied_dirs.contains(&normalized) {
291 return Some(normalized);
292 }
293
294 None
295 }
296
297 pub fn contains_with_markdown_extension(&self, path: &Path) -> bool {
298 self.resolve_supplied(path).is_some()
299 }
300
301 pub fn allow_disk_fallback(&self) -> bool {
302 self.allow_disk_fallback
303 }
304}
305
306#[cfg(not(target_arch = "wasm32"))]
308macro_rules! profile_section {
309 ($name:expr, $profile:expr, $code:expr) => {{
310 let start = $profile.then(std::time::Instant::now);
311 let result = $code;
312 if let Some(start) = start {
313 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
314 }
315 result
316 }};
317}
318
319fn build_commonmark_ordered_lists(
320 lines: &[LineInfo],
321 line_to_list: &crate::utils::code_block_utils::LineToListMap,
322 list_start_values: &crate::utils::code_block_utils::ListStartValues,
323) -> Vec<CommonMarkOrderedListInfo> {
324 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
325
326 for (&line_num, &list_id) in line_to_list {
327 let is_ordered_item = line_num
328 .checked_sub(1)
329 .and_then(|index| lines.get(index))
330 .and_then(|line| line.list_item.as_deref())
331 .is_some_and(|item| item.is_ordered);
332 if is_ordered_item {
333 grouped_lines.entry(list_id).or_default().push(line_num);
334 }
335 }
336
337 let mut lists: Vec<_> = grouped_lines
338 .into_iter()
339 .map(|(list_id, mut item_lines)| {
340 item_lines.sort_unstable();
341 CommonMarkOrderedListInfo {
342 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
343 item_lines,
344 }
345 })
346 .collect();
347 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
348 lists
349}
350
351#[cfg(target_arch = "wasm32")]
352macro_rules! profile_section {
353 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
354}
355
356pub(super) struct SkipByteRanges<'a> {
359 pub(super) front_matter_end: usize,
360 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
361 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
362 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
363 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
364}
365
366use std::sync::{Arc, OnceLock};
367
368pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
370
371pub(super) type ByteRanges = Vec<(usize, usize)>;
373
374pub(crate) struct NativeListSpacingPlan {
376 pub warnings: Vec<crate::rule::LintWarning>,
377 pub withheld: std::collections::HashSet<(usize, bool)>,
378 pub blocks: Vec<(usize, usize, String)>,
379 pub fallback: Vec<crate::rule::LintWarning>,
380}
381
382pub struct LintContext<'a> {
385 pub content: &'a str,
386
387 native_list_spacing_plan: OnceLock<NativeListSpacingPlan>,
388
389 list_guard_parse: OnceLock<crate::utils::list_fix_guard::Parsed<'static>>,
390 parser_blocks: OnceLock<document_parser::blocks::BlockFacts>,
391
392 parse_handoff: std::sync::Mutex<Option<document_parser::handoff::OwnedParsedDocument>>,
393 merge_conflicts: OnceLock<Vec<crate::merge_conflict::Marker>>,
394 content_lines: Vec<&'a str>, line_candidates: candidates::LineCandidates,
396 lexical_emphasis: OnceLock<emphasis::LexicalEmphasis>,
397 document_lines: OnceLock<line_facts::DocumentLines>,
398 pub line_offsets: Vec<usize>,
399 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>,
410 link_destinations_cache: OnceLock<link_destinations::LinkDestinations>,
411 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)>,
420 code_spans_cache: OnceLock<Arc<Vec<CodeSpan>>>,
421 source_code_spans: OnceLock<Vec<SourceCodeSpan>>, math_spans_cache: OnceLock<Arc<Vec<MathSpan>>>, bracket_math_cache: OnceLock<bracket_math::BracketDisplayMathLines>,
424 math_byte_ranges_cache: OnceLock<Vec<(usize, usize)>>, parser_has_math: bool,
427 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>,
433 emphasis_spans_cache: OnceLock<Arc<Vec<EmphasisSpan>>>, bare_urls_cache: OnceLock<Arc<Vec<BareUrl>>>, has_mixed_list_nesting_cache: OnceLock<bool>, html_comment_ranges: Vec<crate::utils::skip_context::ByteRange>, html_block_lines: Vec<std::ops::Range<usize>>, pub table_blocks: Vec<crate::utils::table_utils::TableBlock>, jinja_ranges: Vec<(usize, usize)>, jinja_string_ranges_cache: OnceLock<Vec<(usize, usize)>>,
441 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, }
476
477pub struct CodeRanges {
479 pub blocks: Vec<(usize, usize)>,
481 pub spans: Vec<(usize, usize)>,
483}
484
485pub fn code_ranges(content: &str, flavor: MarkdownFlavor) -> CodeRanges {
496 let ctx = LintContext::new(content, flavor, None);
497 CodeRanges {
498 spans: ctx.inline_code_ranges.clone(),
499 blocks: ctx.code_blocks,
500 }
501}
502
503pub fn code_span_byte_ranges(code_spans: &[CodeSpan]) -> Vec<(usize, usize)> {
505 code_spans
506 .iter()
507 .map(|span| (span.byte_offset, span.byte_end))
508 .collect()
509}
510
511impl<'a> LintContext<'a> {
512 pub(crate) fn native_list_spacing_plan(
513 &self,
514 prepare: impl FnOnce() -> NativeListSpacingPlan,
515 ) -> &NativeListSpacingPlan {
516 self.native_list_spacing_plan.get_or_init(prepare)
517 }
518
519 pub(crate) fn list_guard_parse(&self) -> &crate::utils::list_fix_guard::Parsed<'_> {
520 self.list_guard_parse
521 .get_or_init(|| crate::utils::list_fix_guard::Parsed::new(self.content).into_owned())
522 }
523
524 pub(crate) fn parser_blocks(&self) -> &document_parser::blocks::BlockFacts {
525 self.parser_blocks
526 .get_or_init(|| document_parser::blocks::BlockFacts::parse(self.content))
527 }
528
529 pub(crate) fn with_merge_conflicts(mut self, markers: &[crate::merge_conflict::Marker]) -> Self {
530 self.merge_conflicts = OnceLock::from(markers.to_vec());
531 self
532 }
533
534 pub(crate) fn merge_conflicts(&self) -> &[crate::merge_conflict::Marker] {
535 self.merge_conflicts
536 .get_or_init(|| crate::merge_conflict::scan_markers(self.content).collect())
537 }
538
539 pub fn source_file(&self) -> Option<&Path> {
544 self.source_file.as_deref()
545 }
546
547 pub fn link_target_policy(&self) -> Option<&LinkTargetPolicy> {
548 self.link_target_policy.as_ref()
549 }
550
551 pub fn with_link_target_policy(mut self, policy: LinkTargetPolicy) -> Self {
552 self.link_target_policy = Some(policy);
553 self
554 }
555
556 pub fn invalid_utf8(&self) -> Option<&'a [crate::encoding::InvalidSeq]> {
558 self.invalid_utf8
559 }
560
561 pub fn with_invalid_utf8(mut self, invalid: &'a [crate::encoding::InvalidSeq]) -> Self {
562 self.invalid_utf8 = Some(invalid);
563 self
564 }
565
566 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
567 Self::new_inner(content, flavor, source_file, None, None)
568 }
569
570 #[cfg(test)]
571 pub(crate) fn new_with_handoff(
572 content: &'a str,
573 flavor: MarkdownFlavor,
574 source_file: Option<PathBuf>,
575 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
576 ) -> Self {
577 Self::new_inner(
579 content,
580 flavor,
581 source_file,
582 handoff.filter(|parsed| parsed.matches(content)),
583 None,
584 )
585 }
586
587 pub(crate) fn store_parse_handoff(&self, parsed: document_parser::handoff::OwnedParsedDocument) {
588 if let Ok(mut slot) = self.parse_handoff.lock() {
589 *slot = Some(parsed);
590 }
591 }
592
593 pub(crate) fn certifies_list_guard(&self, content: &str, allowed: crate::utils::list_fix_guard::Allowed) -> bool {
594 self.parse_handoff
595 .lock()
596 .ok()
597 .and_then(|slot| {
598 slot.as_ref()
599 .map(|parsed| parsed.certifies_list_guard(content, allowed))
600 })
601 .unwrap_or(false)
602 }
603
604 pub(crate) fn take_parse_handoff(&self, content: &str) -> Option<document_parser::handoff::OwnedParsedDocument> {
605 self.parse_handoff
606 .lock()
607 .ok()?
608 .take()
609 .filter(|parsed| parsed.matches(content))
610 }
611
612 pub(crate) fn new_with_proof(
613 content: &'a str,
614 flavor: MarkdownFlavor,
615 source_file: Option<PathBuf>,
616 parsed: Option<document_parser::ParsedDocument<'a>>,
617 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
618 ) -> Self {
619 Self::new_inner(content, flavor, source_file, handoff, parsed)
620 }
621
622 fn new_inner(
623 content: &'a str,
624 flavor: MarkdownFlavor,
625 source_file: Option<PathBuf>,
626 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
627 proof_parsed: Option<document_parser::ParsedDocument<'a>>,
628 ) -> Self {
629 let (structure, finish) = Self::prepare_native_structure(content, flavor, source_file, handoff, proof_parsed);
630 finish(structure)
631 }
632
633 pub(crate) fn prepare_native_structure(
634 content: &'a str,
635 flavor: MarkdownFlavor,
636 source_file: Option<PathBuf>,
637 handoff: Option<document_parser::handoff::OwnedParsedDocument>,
638 proof_parsed: Option<document_parser::ParsedDocument<'a>>,
639 ) -> (
640 NativePreparedStructure<'a>,
641 impl FnOnce(NativePreparedStructure<'a>) -> Self,
642 ) {
643 #[cfg(not(target_arch = "wasm32"))]
644 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
645
646 let line_computation::SourceLayout {
647 offsets: line_offsets,
648 lines: content_lines,
649 frequency: char_frequency,
650 candidates: line_candidates,
651 } = profile_section!(
652 "Source lines & markers",
653 profile,
654 line_computation::source_layout(content)
655 );
656
657 #[allow(clippy::disallowed_methods)]
661 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
662
663 let proof_parsed = proof_parsed.filter(|parsed| parsed.matches_source(content));
666 let handoff = handoff.filter(|document| document.matches(content));
667
668 let document_parser::ParsedDocument {
671 structure: parse_result,
672
673 has_math: parser_has_math,
674
675 parser_blocks,
676 pending_links,
677 pending_lines,
678 text_start_lines,
679 } = profile_section!("Shared Markdown parse", profile, {
680 proof_parsed.unwrap_or_else(|| {
681 handoff.map_or_else(
682 || document_parser::ParsedDocument::parse(content, &line_offsets),
683 |document| document.into_document(content),
684 )
685 })
686 });
687 let mut code_blocks = parse_result.code_blocks;
688 let parser_code_blocks = code_blocks.clone();
689 let table_ranges = parse_result.table_ranges;
690 let top_level_blocks = parse_result.top_level_blocks;
691 let rejected_definition_ranges = parse_result.rejected_definition_ranges;
692 let list_items = parse_result.list_items;
693 let mut code_span_ranges = parse_result.code_spans;
694 let code_block_details = parse_result.code_block_details;
695 let strong_spans = parse_result.strong_spans;
696 let line_to_list = parse_result.line_to_list;
697 let list_start_values = parse_result.list_start_values;
698 let html_blocks = parse_result.html_blocks;
699 let definition_lists = DefinitionListLines::new(
700 &line_offsets,
701 &parse_result.definition_items,
702 &parse_result.definition_terms,
703 &parse_result.definition_texts,
704 );
705
706 let mut in_fenced_code = scratch::LineFlags::new(content_lines.len());
711 for detail in code_block_details.iter().filter(|detail| detail.is_fenced) {
712 let start_line = line_offsets
713 .partition_point(|&offset| offset <= detail.start)
714 .saturating_sub(1);
715 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
716 for flag in in_fenced_code.iter_mut().take(end_line).skip(start_line) {
717 *flag = true;
718 }
719 }
720 let tag_hiding = if crate::utils::mkdocs_html_markdown::mentions_markdown(content) {
725 let comment_code_ranges: Vec<(usize, usize)> = code_block_details
726 .iter()
727 .map(|detail| (detail.start, detail.end))
728 .collect();
729 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
730 let comments = crate::utils::skip_context::scan_html_comments(
731 content,
732 &code_span_ranges,
733 &comment_code_ranges,
734 body_start,
735 )
736 .ranges;
737 let comments: Vec<(usize, usize)> = comments.iter().map(|range| (range.start, range.end)).collect();
738 let indented: Vec<(usize, usize)> = code_block_details
739 .iter()
740 .filter(|detail| !detail.is_fenced)
741 .map(|detail| (detail.start, detail.end))
742 .collect();
743 let containing = |ranges: &[(usize, usize)], at: usize| {
745 let idx = ranges.partition_point(|&(start, _)| start <= at);
746 idx.checked_sub(1).map(|i| ranges[i]).filter(|&(_, end)| at < end)
747 };
748 let mut text_resume: Vec<Option<usize>> = vec![None; content_lines.len()];
749 let mut outside_elements = vec![false; content_lines.len()];
750 for (i, line) in content_lines.iter().enumerate() {
751 if i < front_matter_end {
753 text_resume[i] = Some(line.len());
754 continue;
755 }
756 let at = line_offsets[i] + (line.len() - line.trim_start().len());
757 if let Some((_, text_end)) = containing(&comments, at) {
758 text_resume[i] = Some(text_end.min(line_offsets[i] + line.len()) - line_offsets[i]);
759 } else if let Some((_, text_end)) = containing(&code_span_ranges, at) {
760 let column = text_end.min(line_offsets[i] + line.len()) - line_offsets[i];
764 let closes = line
765 .get(column..)
766 .is_some_and(|rest| rest.trim_start().starts_with("</"));
767 text_resume[i] = Some(if closes { column } else { line.len() });
768 }
769 outside_elements[i] = containing(&indented, at).is_some();
770 }
771 flavor_detection::TagHiding {
772 text_resume,
773 outside_elements,
774 }
775 } else {
776 flavor_detection::TagHiding {
777 text_resume: Vec::new(),
778 outside_elements: Vec::new(),
779 }
780 };
781 let containers = profile_section!(
782 "Container lines",
783 profile,
784 flavor_detection::detect_container_lines(&content_lines, &in_fenced_code, &tag_hiding, flavor)
785 );
786
787 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
796 .iter()
797 .flat_map(|detail| {
798 if detail.is_fenced {
799 return vec![(detail.start, detail.end)];
800 }
801 let start_line = line_offsets
802 .partition_point(|&offset| offset <= detail.start)
803 .saturating_sub(1);
804 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
805 containers
806 .code_line_spans_in(start_line..end_line)
807 .into_iter()
808 .map(|span| {
809 let start = line_offsets[span.start].max(detail.start);
810 let end = line_offsets
811 .get(span.end)
812 .copied()
813 .unwrap_or(content.len())
814 .min(detail.end);
815 (start, end)
816 })
817 .collect()
818 })
819 .collect();
820 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
826 let html_comment_scan = profile_section!(
827 "HTML comment ranges",
828 profile,
829 crate::utils::skip_context::scan_html_comments(
830 content,
831 &code_span_ranges,
832 &comment_code_block_ranges,
833 body_start
834 )
835 );
836 let mut html_comment_ranges = html_comment_scan.ranges;
837 let unterminated_html_comment = html_comment_scan.unterminated;
838
839 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
843 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() || !content.contains(":::") {
844 Vec::new()
845 } else {
846 crate::utils::mkdocstrings_refs::autodoc_block_ranges_in_lines(&content_lines, flavor)
847 }
848 });
849
850 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
852 if flavor.is_pandoc_compatible() {
853 crate::utils::pandoc::detect_div_block_ranges(content)
854 } else {
855 Vec::new()
856 }
857 });
858
859 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
861 if flavor == MarkdownFlavor::MkDocs {
862 crate::utils::pymdown_blocks::detect_block_ranges(content)
863 } else {
864 Vec::new()
865 }
866 });
867
868 let skip_ranges = SkipByteRanges {
871 front_matter_end,
872 html_comment_ranges: &html_comment_ranges,
873 autodoc_ranges: &autodoc_ranges,
874 pandoc_div_ranges: &pandoc_div_ranges,
875 pymdown_block_ranges: &pymdown_block_ranges,
876 };
877 let (mut lines, emphasis_spans) = profile_section!(
878 "Basic line info",
879 profile,
880 line_computation::compute_basic_line_info(
881 content,
882 &content_lines,
883 &line_offsets,
884 &code_blocks,
885 flavor,
886 &skip_ranges,
887 pending_lines,
888 )
889 );
890
891 profile_section!(
893 "HTML blocks",
894 profile,
895 heading_detection::detect_html_blocks(content, &mut lines)
896 );
897
898 profile_section!(
900 "ESM blocks",
901 profile,
902 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
903 );
904
905 profile_section!(
907 "JSX block detection",
908 profile,
909 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
910 );
911
912 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
914 "JSX/MDX detection",
915 profile,
916 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
917 );
918
919 profile_section!(
924 "Markdown-in-HTML blocks",
925 profile,
926 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
927 );
928
929 let mdx_context = if flavor == MarkdownFlavor::MDX {
930 mdx::MdxContext::parse(content, &lines)
931 } else {
932 None
933 };
934 let mdx_esm_ranges = mdx_context.as_ref().map_or_else(Vec::new, |mdx| mdx.esm.clone());
935 let (jsx_expression_ranges, mdx_comment_ranges) = if let Some(mdx) = &mdx_context {
936 mdx.apply_lines(&mut lines);
937 code_blocks.clone_from(&mdx.code_blocks);
938 code_span_ranges.clone_from(&mdx.code_spans);
939 (mdx.expressions.clone(), mdx.comments.clone())
940 } else {
941 (jsx_expression_ranges, mdx_comment_ranges)
942 };
943
944 if !flavor.is_pandoc_compatible() {
952 let mut spans = code_span_ranges.iter().peekable();
955 for (line, &offset) in lines.iter_mut().zip(&line_offsets) {
956 if line.is_div_marker {
957 let end = offset + content[offset..].find('\n').unwrap_or(content.len() - offset);
958 while spans.peek().is_some_and(|&&(_, stop)| stop <= end) {
959 spans.next();
960 }
961 line.is_div_marker = spans.peek().is_none_or(|&&(start, _)| start >= end);
962 }
963 }
964 }
965
966 let mut colon_definition_lines: Vec<(usize, usize)> = Vec::new();
971 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
972 for detail in &parse_result.colon_definitions {
973 let definition_line = line_of(detail.definition_start);
974 if flavor.is_pandoc_compatible() && lines[definition_line - 1].is_div_marker {
975 continue;
976 }
977 let (start_line, end_line) = (
978 line_of(detail.start),
979 line_of(detail.end.saturating_sub(1).max(detail.start)),
980 );
981 match colon_definition_lines.last_mut() {
982 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
983 _ => colon_definition_lines.push((start_line, end_line)),
984 }
985 }
986
987 profile_section!(
989 "MkDocs constructs",
990 profile,
991 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
992 );
993
994 let footnote_code_blocks = profile_section!(
1000 "Footnote definitions",
1001 profile,
1002 detect_footnote_definitions(content, &mut lines, &line_offsets)
1003 );
1004
1005 {
1008 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1009 for &(start, end) in &code_blocks {
1010 let start_line = line_offsets
1011 .partition_point(|&offset| offset <= start)
1012 .saturating_sub(1);
1013 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1014
1015 let mut sub_start: Option<usize> = None;
1016 for (i, &offset) in line_offsets[start_line..end_line]
1017 .iter()
1018 .enumerate()
1019 .map(|(j, o)| (j + start_line, o))
1020 {
1021 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1022 if is_real_code && sub_start.is_none() {
1023 let byte_start = if i == start_line { start } else { offset };
1024 sub_start = Some(byte_start);
1025 } else if !is_real_code && sub_start.is_some() {
1026 new_code_blocks.push((sub_start.unwrap(), offset));
1027 sub_start = None;
1028 }
1029 }
1030 if let Some(s) = sub_start {
1031 new_code_blocks.push((s, end));
1032 }
1033 }
1034 code_blocks = new_code_blocks;
1035 }
1036
1037 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
1045 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
1046 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1047 for &(start, end) in &code_blocks {
1048 let start_line = line_offsets
1049 .partition_point(|&offset| offset <= start)
1050 .saturating_sub(1);
1051 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1052
1053 let mut sub_start: Option<usize> = None;
1055 for (i, &offset) in line_offsets[start_line..end_line]
1056 .iter()
1057 .enumerate()
1058 .map(|(j, o)| (j + start_line, o))
1059 {
1060 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1061 if is_real_code && sub_start.is_none() {
1062 let byte_start = if i == start_line { start } else { offset };
1063 sub_start = Some(byte_start);
1064 } else if !is_real_code && sub_start.is_some() {
1065 new_code_blocks.push((sub_start.unwrap(), offset));
1066 sub_start = None;
1067 }
1068 }
1069 if let Some(s) = sub_start {
1070 new_code_blocks.push((s, end));
1071 }
1072 }
1073 code_blocks = new_code_blocks;
1074 }
1075
1076 if flavor.supports_jsx() {
1080 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1081 for &(start, end) in &code_blocks {
1082 let start_line = line_offsets
1083 .partition_point(|&offset| offset <= start)
1084 .saturating_sub(1);
1085 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1086
1087 let mut sub_start: Option<usize> = None;
1088 for (i, &offset) in line_offsets[start_line..end_line]
1089 .iter()
1090 .enumerate()
1091 .map(|(j, o)| (j + start_line, o))
1092 {
1093 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1094 if is_real_code && sub_start.is_none() {
1095 let byte_start = if i == start_line { start } else { offset };
1096 sub_start = Some(byte_start);
1097 } else if !is_real_code && sub_start.is_some() {
1098 new_code_blocks.push((sub_start.unwrap(), offset));
1099 sub_start = None;
1100 }
1101 }
1102 if let Some(s) = sub_start {
1103 new_code_blocks.push((s, end));
1104 }
1105 }
1106 code_blocks = new_code_blocks;
1107
1108 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
1115 let mut run: Option<(usize, usize)> = None;
1116 for line in &lines {
1117 if line.in_jsx_block && line.in_code_block {
1118 let line_end = line.byte_offset + line.byte_len;
1119 match &mut run {
1120 Some((_, end)) => *end = line_end,
1121 None => run = Some((line.byte_offset, line_end)),
1122 }
1123 } else if let Some(r) = run.take() {
1124 jsx_fence_ranges.push(r);
1125 }
1126 }
1127 if let Some(r) = run.take() {
1128 jsx_fence_ranges.push(r);
1129 }
1130 if !jsx_fence_ranges.is_empty() {
1131 code_blocks.extend(jsx_fence_ranges);
1132 code_blocks.sort_by_key(|&(start, _)| start);
1133 }
1134 }
1135
1136 let colon_fence_details = profile_section!(
1139 "Azure colon fence detection",
1140 profile,
1141 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
1142 );
1143 if !colon_fence_details.is_empty() {
1144 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
1145 code_blocks.sort_by_key(|&(start, _)| start);
1146 }
1147
1148 let myst_directive_ranges = profile_section!(
1151 "MyST colon directives",
1152 profile,
1153 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
1154 );
1155
1156 let myst_comment_ranges = profile_section!(
1158 "MyST comments",
1159 profile,
1160 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
1161 );
1162
1163 profile_section!(
1166 "MyST backtick directives",
1167 profile,
1168 flavor_detection::detect_myst_backtick_directives(
1169 content,
1170 &mut lines,
1171 flavor,
1172 &code_block_details,
1173 &line_offsets
1174 )
1175 );
1176
1177 if flavor.supports_myst_directives() {
1180 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
1181 for &(start, end) in &code_blocks {
1182 let start_line = line_offsets
1183 .partition_point(|&offset| offset <= start)
1184 .saturating_sub(1);
1185 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1186
1187 let mut sub_start: Option<usize> = None;
1188 for (i, &offset) in line_offsets[start_line..end_line]
1189 .iter()
1190 .enumerate()
1191 .map(|(j, o)| (j + start_line, o))
1192 {
1193 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
1194 if is_real_code && sub_start.is_none() {
1195 let byte_start = if i == start_line { start } else { offset };
1196 sub_start = Some(byte_start);
1197 } else if !is_real_code && sub_start.is_some() {
1198 new_code_blocks.push((sub_start.unwrap(), offset));
1199 sub_start = None;
1200 }
1201 }
1202 if let Some(s) = sub_start {
1203 new_code_blocks.push((s, end));
1204 }
1205 }
1206 code_blocks = new_code_blocks;
1207 }
1208
1209 profile_section!(
1211 "Kramdown constructs",
1212 profile,
1213 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
1214 );
1215
1216 for line in &mut lines {
1221 if line.in_kramdown_extension_block {
1222 line.list_item = None;
1223 line.is_horizontal_rule = false;
1224 line.blockquote = None;
1225 line.is_kramdown_block_ial = false;
1226 }
1227 }
1228
1229 let obsidian_comment_scan = profile_section!(
1231 "Obsidian comments",
1232 profile,
1233 flavor_detection::detect_obsidian_comments(
1234 content,
1235 &mut lines,
1236 flavor,
1237 &code_span_ranges,
1238 &html_comment_ranges,
1239 body_start
1240 )
1241 );
1242 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
1243 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
1244
1245 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
1250 unterminated_html_comment,
1251 &obsidian_comment_ranges,
1252 content,
1253 &code_span_ranges,
1254 &comment_code_block_ranges,
1255 body_start,
1256 );
1257
1258 if let Some(range) = unterminated_html_comment.and_then(|opener| {
1271 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
1272 .or_else(|| container_comment_range(opener, &containers, &lines, content))
1273 }) {
1274 html_comment_ranges.push(range);
1277
1278 for line in &mut lines {
1284 let text = line.content(content);
1285 let content_start = line.byte_offset + line.indent;
1286 let content_end = line.byte_offset + text.trim_end().len();
1287 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
1288 &html_comment_ranges,
1289 content_start,
1290 content_end,
1291 );
1292 line.in_obsidian_comment = false;
1293 }
1294
1295 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
1306 content,
1307 &mut lines,
1308 flavor,
1309 &code_span_ranges,
1310 &html_comment_ranges,
1311 body_start,
1312 );
1313 obsidian_comment_ranges = obsidian_rescan.ranges;
1314 unterminated_obsidian_comment = obsidian_rescan.unterminated;
1315 }
1316
1317 let myst_role_ranges = profile_section!(
1319 "MyST roles",
1320 profile,
1321 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
1322 );
1323
1324 let mut pulldown_result = profile_section!(
1328 "Links, images & link ranges",
1329 profile,
1330 pending_links.finish(&lines, &code_blocks, flavor, &html_comment_ranges)
1331 );
1332
1333 if let Some(mdx) = &mdx_context {
1334 let (links, images) = mdx.links_and_images(content, &lines);
1335 pulldown_result.link_byte_ranges = links.iter().map(|link| (link.byte_offset, link.byte_end)).collect();
1336 pulldown_result.link_found_positions = links.iter().map(|link| link.byte_offset).collect();
1337 pulldown_result.image_found_positions = images.iter().map(|image| image.byte_offset).collect();
1338 pulldown_result.links = links;
1339 pulldown_result.images = images;
1340 pulldown_result.footnote_refs = mdx.footnote_refs();
1341 pulldown_result
1342 .broken_links
1343 .retain(|link| mdx.contains_text(link.span.start, link.span.end));
1344 }
1345
1346 heading_detection::mark_parser_html_blocks(content, &mut lines, &html_blocks, flavor);
1347 let html_block_lines = heading_detection::parser_html_block_lines(&lines, &html_blocks, flavor);
1348
1349 let mdx_flow_lines = mdx_context.as_ref().map(|mdx| mdx.flow_lines(&lines));
1351 let heading_detection::HeadingAnalysis {
1352 mut blockquote_headings,
1353 lazy_table_text,
1354 } = profile_section!(
1355 "Headings & blockquotes",
1356 profile,
1357 heading_detection::detect_headings_and_blockquotes(
1358 &content_lines,
1359 &mut lines,
1360 flavor,
1361 &html_comment_ranges,
1362 &html_blocks,
1363 &code_blocks,
1364 &code_span_ranges,
1365 &pulldown_result.link_byte_ranges,
1366 front_matter_end,
1367 mdx_flow_lines.as_deref(),
1368 mdx_context
1369 .as_ref()
1370 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1371 )
1372 );
1373
1374 for line in &mut lines {
1376 if line.in_kramdown_extension_block {
1377 line.heading = None;
1378 }
1379 }
1380 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
1381 if line.in_kramdown_extension_block {
1382 *heading = None;
1383 }
1384 }
1385
1386 for range in &html_block_lines {
1403 for line in &mut lines[range.clone()] {
1404 if !line.in_mkdocs_html_markdown {
1405 line.is_horizontal_rule = false;
1406 }
1407 }
1408 }
1409 for &(start, end) in &footnote_code_blocks {
1410 let spanned = line_computation::spanned_lines(&lines, start, end);
1411 for line in &mut lines[spanned] {
1412 line.is_horizontal_rule = false;
1413 }
1414 }
1415 for line in &mut lines {
1416 if line.is_horizontal_rule
1417 && (line.in_code_block
1418 || line.in_html_block
1419 || line.in_html_comment
1420 || line.in_math_block
1421 || line.in_mdx_comment
1422 || line.in_obsidian_comment)
1423 {
1424 line.is_horizontal_rule = false;
1425 }
1426 }
1427
1428 let mut inline_code_ranges = code_span_ranges.clone();
1431
1432 if flavor == MarkdownFlavor::MkDocs {
1436 let extra = profile_section!(
1437 "MkDocs code spans",
1438 profile,
1439 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
1440 );
1441 if !extra.is_empty() {
1442 inline_code_ranges.extend(extra);
1443 inline_code_ranges.sort_by_key(|span| span.0);
1444 }
1445 }
1446
1447 if flavor == MarkdownFlavor::MDX && mdx_context.is_none() {
1452 let extra = profile_section!(
1453 "MDX JSX code spans",
1454 profile,
1455 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
1456 );
1457 if !extra.is_empty() {
1458 inline_code_ranges.extend(extra);
1459 inline_code_ranges.sort_by_key(|span| span.0);
1460 }
1461 }
1462
1463 for &(start, end) in &inline_code_ranges {
1466 let first = lines.partition_point(|line| line.byte_offset <= start);
1467 let last = lines.partition_point(|line| line.byte_offset <= end);
1468 for line in &mut lines[first..last] {
1469 line.in_code_span_continuation = true;
1470 }
1471 }
1472
1473 let mut parsed_link_ranges = std::mem::take(&mut pulldown_result.link_byte_ranges);
1476 parsed_link_ranges.sort_unstable();
1477
1478 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
1479 let list_blocks: Vec<_> = list_blocks
1480 .into_iter()
1481 .filter(|block| {
1482 !lines
1483 .get(block.start_line - 1)
1484 .is_some_and(|line| line.in_kramdown_extension_block)
1485 })
1486 .collect();
1487 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks, &inline_code_ranges);
1488 let code_spans_without_math_cache = OnceLock::new();
1489 let structure = NativePreparedStructure {
1490 content,
1491 flavor,
1492 line_offsets,
1493 code_blocks,
1494 code_block_details,
1495 lines,
1496 list_blocks,
1497 html_block_lines,
1498 inline_code_ranges,
1499 code_span_ranges,
1500 inline_config,
1501 front_matter_end,
1502 code_spans_without_math_cache,
1503
1504 parser_has_math,
1505 };
1506 let finish = move |structure: NativePreparedStructure<'a>| {
1510 let NativePreparedStructure {
1511 content,
1512 flavor,
1513 line_offsets,
1514 code_blocks,
1515 code_block_details,
1516 mut lines,
1517 list_blocks,
1518 html_block_lines,
1519 inline_code_ranges,
1520 code_span_ranges,
1521 inline_config,
1522 front_matter_end,
1523 code_spans_without_math_cache,
1524
1525 parser_has_math,
1526 } = structure;
1527 let (links, images, broken_links, footnote_refs) = profile_section!(
1529 "Links & images finalize",
1530 profile,
1531 link_parser::finalize_links_and_images(
1532 content,
1533 &lines,
1534 flavor,
1535 &link_parser::LinkExclusions {
1536 code_blocks: &code_blocks,
1537 code_spans: &inline_code_ranges,
1538 html_comment_ranges: &html_comment_ranges,
1539 mdx: mdx_context.as_ref(),
1540 },
1541 pulldown_result,
1542 )
1543 );
1544
1545 let reference_defs = profile_section!("Reference defs", profile, {
1546 if let Some(mdx) = &mdx_context {
1547 mdx.reference_defs(content)
1548 } else {
1549 link_parser::parse_reference_defs(content, &lines)
1550 }
1551 });
1552
1553 let mut lazy_text = if lines.iter().zip(&content_lines).any(|(line, text)| {
1557 text.contains('|')
1558 && (line.visual_indent >= 4
1559 || line
1560 .blockquote
1561 .as_deref()
1562 .is_some_and(|quote| heading_detection::prefix_may_hide_block("e.prefix)))
1563 }) {
1564 let mut definition_lines = vec![false; if mdx_context.is_some() { lines.len() } else { 0 }];
1567 let definitions = if mdx_context.is_some() {
1568 for definition in &reference_defs {
1569 let first = line_offsets.partition_point(|&offset| offset <= definition.byte_offset);
1570 let end = line_offsets
1571 .partition_point(|&offset| offset < definition.byte_end.max(definition.byte_offset + 1));
1572 for flag in
1573 &mut definition_lines[first.saturating_sub(1).min(lines.len())..end.min(lines.len())]
1574 {
1575 *flag = true;
1576 }
1577 }
1578 Some(heading_detection::Definitions::Lines(&definition_lines))
1579 } else {
1580 text_start_lines
1581 .as_deref()
1582 .map(heading_detection::Definitions::TextStarts)
1583 };
1584 if reference_defs.is_empty()
1589 && text_start_lines.is_none()
1590 && let Some(lazy_text) = lazy_table_text
1591 {
1592 lazy_text
1593 } else {
1594 heading_detection::indented_text_lines(
1595 &content_lines,
1596 &lines,
1597 flavor,
1598 &html_blocks,
1599 &code_blocks,
1600 &code_span_ranges,
1601 mdx_flow_lines.as_deref(),
1602 mdx_context
1603 .as_ref()
1604 .map(|_| [jsx_expression_ranges.as_slice(), mdx_comment_ranges.as_slice()]),
1605 definitions,
1606 )
1607 }
1608 } else {
1609 Vec::new()
1610 };
1611 if flavor == MarkdownFlavor::MDG {
1612 for (flag, line) in lazy_text.iter_mut().zip(&lines) {
1615 *flag &= line.blockquote.is_some();
1616 }
1617 }
1618
1619 let table_blocks = profile_section!(
1621 "Table blocks",
1622 profile,
1623 if char_frequency.pipe_count == 0 {
1624 Vec::new()
1625 } else {
1626 crate::utils::table_utils::TableUtils::find_table_blocks_with_layout(
1627 &content_lines,
1628 &line_offsets,
1629 &code_blocks,
1630 &inline_code_ranges,
1631 &html_comment_ranges,
1632 flavor,
1633 &lazy_text,
1634 &list_items,
1635 )
1636 }
1637 );
1638 let list_item_parent: Vec<Option<usize>> = {
1639 let mut open: Vec<usize> = Vec::new();
1640 list_items
1641 .iter()
1642 .enumerate()
1643 .map(|(index, &(start, _, _))| {
1644 while open.last().is_some_and(|&top| list_items[top].1 <= start) {
1645 open.pop();
1646 }
1647 let parent = open.last().copied();
1648 open.push(index);
1649 parent
1650 })
1651 .collect()
1652 };
1653 let table_blocks: Vec<_> = table_blocks
1657 .into_iter()
1658 .filter(|block| flavor == MarkdownFlavor::MDX || !lines[block.header_line].in_markdown_span)
1659 .collect();
1660 let table_blocks = table_confirm::confirm_table_blocks(
1661 table_blocks,
1662 &table_confirm::Evidence {
1663 content,
1664 lines: &lines,
1665 line_offsets: &line_offsets,
1666 flavor,
1667 table_ranges: &table_ranges,
1668 top_level_blocks: &top_level_blocks,
1669 rejected_definitions: &rejected_definition_ranges,
1670 list_items: &list_items,
1671 list_item_parent: &list_item_parent,
1672 parser_code_blocks: &parser_code_blocks,
1673 native_tables: mdx_context.as_ref().map_or(&[][..], |mdx| &mdx.tables[..]),
1674 html_above: std::cell::OnceCell::new(),
1675 opener_body: std::cell::OnceCell::new(),
1676 rejected_definition_reach: std::cell::OnceCell::new(),
1677 },
1678 );
1679
1680 let links = links
1683 .into_iter()
1684 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1685 .collect::<Vec<_>>();
1686 let images = images
1687 .into_iter()
1688 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1689 .collect::<Vec<_>>();
1690 let broken_links = broken_links
1691 .into_iter()
1692 .filter(|bl| {
1693 let line_idx = line_offsets
1695 .partition_point(|&offset| offset <= bl.span.start)
1696 .saturating_sub(1);
1697 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
1698 })
1699 .collect::<Vec<_>>();
1700 let footnote_refs = footnote_refs
1701 .into_iter()
1702 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1703 .collect::<Vec<_>>();
1704 let reference_defs = reference_defs
1705 .into_iter()
1706 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1707 .collect::<Vec<_>>();
1708 let table_blocks = table_blocks
1709 .into_iter()
1710 .filter(|block| {
1711 !lines
1713 .get(block.start_line)
1714 .is_some_and(|l| l.in_kramdown_extension_block)
1715 })
1716 .collect::<Vec<_>>();
1717 let emphasis_spans = emphasis_spans
1718 .into_iter()
1719 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
1720 .collect::<Vec<_>>();
1721
1722 for block in &list_blocks {
1726 for line_num in block.start_line..=block.end_line {
1728 if let Some(li) = lines.get_mut(line_num - 1) {
1729 li.in_list_block = true;
1730 }
1731 }
1732 }
1733 for block in &table_blocks {
1734 for idx in block.start_line..=block.end_line {
1736 if let Some(li) = lines.get_mut(idx) {
1737 li.in_table_block = true;
1738 }
1739 }
1740 }
1741
1742 let reference_defs_map: HashMap<String, usize> = reference_defs
1744 .iter()
1745 .enumerate()
1746 .map(|(idx, def)| (def.id.to_lowercase(), idx))
1747 .collect();
1748
1749 let link_title_ranges: Vec<(usize, usize)> = reference_defs
1751 .iter()
1752 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
1753 (Some(start), Some(end)) => Some((start, end)),
1754 _ => None,
1755 })
1756 .collect();
1757
1758 let jinja_ranges = profile_section!(
1760 "Jinja ranges",
1761 profile,
1762 crate::utils::jinja_utils::find_jinja_ranges(content)
1763 );
1764
1765 let citation_ranges = profile_section!("Citation ranges", profile, {
1767 if flavor.is_pandoc_compatible() {
1768 crate::utils::pandoc::find_citation_ranges(content)
1769 } else {
1770 Vec::new()
1771 }
1772 });
1773
1774 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
1776 if flavor.is_pandoc_compatible() {
1777 crate::utils::pandoc::detect_inline_footnote_ranges(content)
1778 } else {
1779 Vec::new()
1780 }
1781 });
1782
1783 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
1785 if flavor.is_pandoc_compatible() {
1786 crate::utils::pandoc::collect_pandoc_header_slugs(content)
1787 } else {
1788 std::collections::HashSet::new()
1789 }
1790 });
1791
1792 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
1794 if flavor.is_pandoc_compatible() {
1795 crate::utils::pandoc::detect_example_list_marker_ranges(content)
1796 } else {
1797 Vec::new()
1798 }
1799 });
1800
1801 let example_reference_ranges = profile_section!("Example references", profile, {
1803 if flavor.is_pandoc_compatible() {
1804 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
1805 } else {
1806 Vec::new()
1807 }
1808 });
1809
1810 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1812 if flavor.is_pandoc_compatible() {
1813 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1814 } else {
1815 Vec::new()
1816 }
1817 });
1818
1819 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1821 if flavor.is_pandoc_compatible() {
1822 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1823 } else {
1824 Vec::new()
1825 }
1826 });
1827
1828 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1830 if flavor.is_pandoc_compatible() {
1831 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1832 } else {
1833 Vec::new()
1834 }
1835 });
1836
1837 let line_block_ranges = profile_section!("Line block ranges", profile, {
1839 if flavor.is_pandoc_compatible() {
1840 crate::utils::pandoc::detect_line_block_ranges(content)
1841 } else {
1842 Vec::new()
1843 }
1844 });
1845
1846 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1848 if flavor.is_pandoc_compatible() {
1849 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1850 } else {
1851 Vec::new()
1852 }
1853 });
1854
1855 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1857 if flavor.is_pandoc_compatible() {
1858 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1859 } else {
1860 Vec::new()
1861 }
1862 });
1863
1864 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1866 if flavor.is_pandoc_compatible() {
1867 crate::utils::pandoc::detect_grid_table_ranges(content)
1868 } else {
1869 Vec::new()
1870 }
1871 });
1872
1873 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1875 if flavor.is_pandoc_compatible() {
1876 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1877 } else {
1878 Vec::new()
1879 }
1880 });
1881
1882 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1884 crate::utils::shortcode_utils::shortcode_ranges(content).collect()
1885 });
1886
1887 let ctx = Self {
1888 content,
1889
1890 native_list_spacing_plan: OnceLock::new(),
1891
1892 list_guard_parse: OnceLock::new(),
1893
1894 parser_blocks: parser_blocks.map_or_else(OnceLock::new, OnceLock::from),
1895
1896 parse_handoff: std::sync::Mutex::new(None),
1897 merge_conflicts: OnceLock::new(),
1898 content_lines,
1899 line_candidates,
1900 lexical_emphasis: OnceLock::new(),
1901 document_lines: OnceLock::new(),
1902 line_offsets,
1903 code_blocks,
1904 code_block_details,
1905 strong_spans,
1906 line_to_list,
1907 list_start_values,
1908 definition_lists,
1909 colon_definition_lines,
1910 commonmark_ordered_lists_cache: OnceLock::new(),
1911 lines,
1912 blockquote_headings,
1913 document_anchors_cache: OnceLock::new(),
1914 link_destinations_cache: OnceLock::new(),
1915 links,
1916 rendered_link_ends_cache: OnceLock::new(),
1917 parsed_link_ranges,
1918 images,
1919 broken_links,
1920 footnote_refs,
1921 reference_defs,
1922 reference_defs_map,
1923 inline_code_ranges,
1924 code_spans_cache: OnceLock::new(),
1925 source_code_spans: OnceLock::new(),
1926 math_spans_cache: OnceLock::new(), bracket_math_cache: OnceLock::new(),
1928 math_byte_ranges_cache: OnceLock::new(), parser_has_math,
1931 code_spans_without_math_cache,
1932 list_blocks,
1933 char_frequency,
1934 html_tags_cache: OnceLock::new(),
1935 jsx_component_tags_cache: OnceLock::new(),
1936 source_emphasis: emphasis_spans,
1937 emphasis_spans_cache: OnceLock::new(),
1938 bare_urls_cache: OnceLock::new(),
1939 has_mixed_list_nesting_cache: OnceLock::new(),
1940 html_comment_ranges,
1941 html_block_lines,
1942 table_blocks,
1943 jinja_ranges,
1944 jinja_string_ranges_cache: OnceLock::new(),
1945 flavor,
1946 source_file,
1947 link_target_policy: None,
1948 invalid_utf8: None,
1949 jsx_expression_ranges,
1950 mdx_esm_ranges,
1951 mdx_comment_ranges,
1952 citation_ranges,
1953 pandoc_div_ranges,
1954 colon_fence_details,
1955 inline_footnote_ranges,
1956 pandoc_header_slugs,
1957 example_list_marker_ranges,
1958 example_reference_ranges,
1959 sub_super_ranges,
1960 inline_code_attr_ranges,
1961 bracketed_span_ranges,
1962 line_block_ranges,
1963 pipe_table_caption_ranges,
1964 pandoc_metadata_ranges,
1965 grid_table_ranges,
1966 multi_line_table_ranges,
1967 shortcode_ranges,
1968 link_title_ranges,
1969 code_span_byte_ranges: code_span_ranges,
1970 inline_config,
1971 obsidian_comment_ranges,
1972 unterminated_html_comment,
1973 unterminated_obsidian_comment,
1974 lazy_cont_lines_cache: OnceLock::new(),
1975 myst_directive_ranges,
1976 myst_comment_ranges,
1977 myst_role_ranges,
1978 front_matter_end,
1979 };
1980 if let Some(mdx) = mdx_context {
1981 let (html_tags, jsx_component_tags) = mdx.html_tags(content, &ctx.lines);
1982 let _ = ctx.html_tags_cache.set(Arc::new(ctx.filter_kramdown_tags(html_tags)));
1983 let _ = ctx
1984 .jsx_component_tags_cache
1985 .set(Arc::new(ctx.filter_kramdown_tags(jsx_component_tags)));
1986 }
1987 ctx
1988 };
1989 (structure, finish)
1990 }
1991
1992 pub fn front_matter_end_line(&self) -> usize {
1997 self.front_matter_end
1998 }
1999
2000 #[inline]
2003 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
2004 let idx = ranges.partition_point(|&(start, _)| start <= pos);
2006 idx > 0 && pos < ranges[idx - 1].1
2008 }
2009
2010 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
2012 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
2013 }
2014
2015 pub fn is_in_code_span_byte_with_or_without_math(&self, pos: usize) -> bool {
2021 self.native_list_source().is_in_code_span_byte_with_or_without_math(pos)
2022 }
2023
2024 pub fn is_code_span_text_byte(&self, pos: usize) -> bool {
2030 self.is_byte_offset_in_code_span(pos) || self.is_in_code_span_byte_with_or_without_math(pos)
2031 }
2032
2033 pub fn line_ends_with_hard_break(&self, line_number: usize) -> bool {
2038 let line = &self.lines[line_number - 1];
2039 heading_detection::ends_with_hard_break(
2040 line.content(self.content),
2041 line.byte_offset,
2042 &self.code_span_byte_ranges,
2043 )
2044 }
2045
2046 pub fn is_in_link(&self, pos: usize) -> bool {
2048 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
2049 }
2050
2051 pub fn is_in_bare_url(&self, pos: usize) -> bool {
2053 let bare_urls = self.bare_urls();
2054 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
2056 idx > 0 && pos < bare_urls[idx - 1].byte_end
2057 }
2058
2059 pub fn inline_config(&self) -> &InlineConfig {
2061 &self.inline_config
2062 }
2063
2064 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
2069 &self.colon_fence_details
2070 }
2071
2072 pub fn raw_lines(&self) -> &[&'a str] {
2076 &self.content_lines
2077 }
2078
2079 pub(crate) fn candidate_line_indices(&self, kind: LineCandidateKind) -> &[usize] {
2082 self.line_candidates.lines(kind)
2083 }
2084
2085 pub(crate) fn lexical_emphasis(&self) -> impl Iterator<Item = EmphasisLine<'_>> {
2088 self.lexical_emphasis
2089 .get_or_init(|| emphasis::collect(self))
2090 .lines(self.content)
2091 }
2092
2093 fn document_lines(&self) -> &line_facts::DocumentLines {
2094 self.document_lines
2095 .get_or_init(|| line_facts::DocumentLines::new(&self.lines, &self.blockquote_headings))
2096 }
2097
2098 pub(crate) fn line_context_masks(&self) -> &[LineContextMask] {
2099 &self.document_lines().contexts
2100 }
2101
2102 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
2107 self.inline_config.is_rule_disabled(rule_name, line_number)
2108 }
2109
2110 pub(crate) fn inline_code_ranges(&self) -> &[(usize, usize)] {
2112 &self.inline_code_ranges
2113 }
2114
2115 pub(crate) fn source_code_spans(&self) -> &[SourceCodeSpan] {
2116 self.source_code_spans.get_or_init(|| {
2117 element_parsers::build_code_spans_from_ranges(self.content, &self.lines, &self.inline_code_ranges)
2118 })
2119 }
2120
2121 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
2123 Arc::clone(self.code_spans_cache.get_or_init(|| {
2124 Arc::new(
2125 self.source_code_spans()
2126 .iter()
2127 .map(|span| span.materialize(self.content))
2128 .collect(),
2129 )
2130 }))
2131 }
2132
2133 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
2137 self.math_byte_ranges_cache
2138 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
2139 }
2140
2141 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
2143 Arc::clone(
2144 self.math_spans_cache
2145 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
2146 )
2147 }
2148
2149 pub(crate) fn bracket_display_math_lines(&self) -> &bracket_math::BracketDisplayMathLines {
2151 self.bracket_math_cache
2152 .get_or_init(|| bracket_math::parse(self.content, &self.lines, self.source_code_spans(), &self.list_blocks))
2153 }
2154
2155 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
2157 let math_spans = self.math_spans();
2158 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
2160 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
2161 }
2162
2163 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2165 &self.html_comment_ranges
2166 }
2167
2168 pub fn unterminated_html_comment(&self) -> Option<usize> {
2173 self.unterminated_html_comment
2174 }
2175
2176 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
2180 self.unterminated_obsidian_comment
2181 }
2182
2183 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
2187 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
2188 }
2189
2190 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
2195 if self.obsidian_comment_ranges.is_empty() {
2196 return false;
2197 }
2198
2199 let byte_pos = self.line_index().line_col_to_byte_range(line_num, col).start;
2201 self.is_in_obsidian_comment(byte_pos)
2202 }
2203
2204 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
2206 &self.myst_directive_ranges
2207 }
2208
2209 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
2211 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
2212 }
2213
2214 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
2216 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
2217 }
2218
2219 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
2226 if !self.flavor.supports_myst_directives() {
2227 return false;
2228 }
2229 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
2230 info.in_myst_directive
2231 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
2232 })
2233 }
2234
2235 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
2237 tags.into_iter()
2238 .filter(|tag| {
2239 !self
2240 .lines
2241 .get(tag.line - 1)
2242 .is_some_and(|l| l.in_kramdown_extension_block)
2243 })
2244 .collect()
2245 }
2246
2247 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
2254 Arc::clone(self.html_tags_shared())
2255 }
2256
2257 fn html_tags_shared(&self) -> &Arc<Vec<HtmlTag>> {
2258 self.html_tags_cache.get_or_init(|| {
2259 let (html_tags, jsx_component_tags) =
2260 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
2261 let _ = self
2263 .jsx_component_tags_cache
2264 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
2265 Arc::new(self.filter_kramdown_tags(html_tags))
2266 })
2267 }
2268
2269 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
2272 Arc::clone(self.jsx_component_tags_shared())
2273 }
2274
2275 fn jsx_component_tags_shared(&self) -> &Arc<Vec<HtmlTag>> {
2276 if let Some(cached) = self.jsx_component_tags_cache.get() {
2277 return cached;
2278 }
2279 let _ = self.html_tags_shared();
2281 self.jsx_component_tags_cache
2282 .get()
2283 .expect("HTML parse populates jsx_component_tags_cache")
2284 }
2285
2286 pub(crate) fn source_emphasis_spans(&self) -> &[SourceEmphasisSpan] {
2287 &self.source_emphasis
2288 }
2289
2290 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
2292 Arc::clone(self.emphasis_spans_cache.get_or_init(|| {
2293 Arc::new(
2294 self.source_emphasis
2295 .iter()
2296 .map(|span| span.materialize(self.content))
2297 .collect(),
2298 )
2299 }))
2300 }
2301
2302 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
2304 Arc::clone(self.bare_urls_cache.get_or_init(|| {
2305 Arc::new(element_parsers::parse_bare_urls(
2306 self.content,
2307 &self.lines,
2308 &self.code_blocks,
2309 ))
2310 }))
2311 }
2312
2313 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
2315 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
2316 Arc::new(element_parsers::detect_lazy_continuation_lines(
2317 self.content,
2318 &self.lines,
2319 &self.line_offsets,
2320 ))
2321 }))
2322 }
2323
2324 pub fn has_mixed_list_nesting(&self) -> bool {
2328 *self
2329 .has_mixed_list_nesting_cache
2330 .get_or_init(|| self.compute_mixed_list_nesting())
2331 }
2332
2333 fn compute_mixed_list_nesting(&self) -> bool {
2335 let mut stack: Vec<(usize, bool)> = Vec::new();
2340 let mut last_was_blank = false;
2341
2342 for line_info in &self.lines {
2343 if line_info.in_code_block
2345 || line_info.in_front_matter
2346 || line_info.in_mkdocstrings
2347 || line_info.in_html_comment
2348 || line_info.in_mdx_comment
2349 || line_info.in_esm_block
2350 {
2351 continue;
2352 }
2353
2354 if line_info.is_blank {
2356 last_was_blank = true;
2357 continue;
2358 }
2359
2360 if let Some(list_item) = &line_info.list_item {
2361 let current_pos = if list_item.marker_column == 1 {
2363 0
2364 } else {
2365 list_item.marker_column
2366 };
2367
2368 if last_was_blank && current_pos == 0 {
2370 stack.clear();
2371 }
2372 last_was_blank = false;
2373
2374 while let Some(&(pos, _)) = stack.last() {
2376 if pos >= current_pos {
2377 stack.pop();
2378 } else {
2379 break;
2380 }
2381 }
2382
2383 if let Some(&(_, parent_is_ordered)) = stack.last()
2385 && parent_is_ordered != list_item.is_ordered
2386 {
2387 return true; }
2389
2390 stack.push((current_pos, list_item.is_ordered));
2391 } else {
2392 last_was_blank = false;
2394 }
2395 }
2396
2397 false
2398 }
2399
2400 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
2406 native_structure::offset_to_line_col(self.content, &self.line_offsets, offset)
2407 }
2408
2409 fn line_index(&self) -> crate::utils::range_utils::LineIndex<'_> {
2413 crate::utils::range_utils::LineIndex::for_positions(self.content, &self.line_offsets)
2414 }
2415
2416 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
2422 line_number
2423 .checked_sub(1)
2424 .and_then(|index| self.line_offsets.get(index).copied())
2425 }
2426
2427 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
2433 self.line_index().line_col_to_byte_range(line_number, column)
2434 }
2435
2436 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
2441 self.line_index()
2442 .line_col_to_byte_range_with_length(line_number, column, length)
2443 }
2444
2445 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
2448 self.line_index().whole_line_range(line_number)
2449 }
2450
2451 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
2456 self.line_index().line_text_range(line_number, start_column, end_column)
2457 }
2458
2459 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
2462 self.line_index().line_content_range(line_number)
2463 }
2464
2465 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
2467 self.line_index().multi_line_range(start_line, end_line)
2468 }
2469
2470 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
2472 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
2474 return true;
2475 }
2476
2477 self.is_byte_offset_in_code_span(pos)
2479 }
2480
2481 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
2483 if line_num > 0 {
2484 self.lines.get(line_num - 1)
2485 } else {
2486 None
2487 }
2488 }
2489
2490 pub(crate) fn document_anchors(&self) -> &anchors::DocumentAnchors {
2493 self.document_anchors_cache
2494 .get_or_init(|| anchors::DocumentAnchors::new(self))
2495 }
2496
2497 pub fn links(&self) -> &[ParsedLink<'a>] {
2499 &self.links
2500 }
2501
2502 #[inline]
2505 pub(crate) fn is_in_parsed_link(&self, pos: usize) -> bool {
2506 Self::binary_search_ranges(&self.parsed_link_ranges, pos)
2507 }
2508
2509 pub(crate) fn has_parsed_link_start_in(&self, start: usize, end: usize) -> bool {
2512 let index = self.parsed_link_ranges.partition_point(|&(open, _)| open < start);
2513 self.parsed_link_ranges.get(index).is_some_and(|&(open, _)| open < end)
2514 }
2515
2516 pub fn images(&self) -> &[ParsedImage<'a>] {
2518 &self.images
2519 }
2520
2521 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
2523 &self.broken_links
2524 }
2525
2526 pub fn footnote_references(&self) -> &[FootnoteRef] {
2528 &self.footnote_refs
2529 }
2530
2531 pub fn reference_definitions(&self) -> &[ReferenceDef] {
2533 &self.reference_defs
2534 }
2535
2536 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
2538 let start = self.links.partition_point(|link| link.line < line_number);
2539 let end = self.links.partition_point(|link| link.line <= line_number);
2540 &self.links[start..end]
2541 }
2542
2543 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
2545 let start = self.images.partition_point(|image| image.line < line_number);
2546 let end = self.images.partition_point(|image| image.line <= line_number);
2547 &self.images[start..end]
2548 }
2549
2550 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2552 self.links
2553 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
2554 .ok()
2555 .map(|index| &self.links[index])
2556 }
2557
2558 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2560 self.images
2561 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
2562 .ok()
2563 .map(|index| &self.images[index])
2564 }
2565
2566 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
2568 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2569 self.links
2570 .get(index.checked_sub(1)?)
2571 .filter(|link| byte_offset < link.byte_end)
2572 }
2573
2574 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
2576 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
2577 self.images
2578 .get(index.checked_sub(1)?)
2579 .filter(|image| byte_offset < image.byte_end)
2580 }
2581
2582 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
2584 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
2585 &self.links[..end]
2586 }
2587
2588 pub(crate) fn is_range_in_rendered_link(&self, range: &Range<usize>) -> bool {
2592 let count = self.links.partition_point(|link| link.byte_offset <= range.start);
2593 if count == 0 {
2594 return false;
2595 }
2596 let ends = self.rendered_link_ends_cache.get_or_init(|| {
2597 let mut end = 0;
2598 self.links
2599 .iter()
2600 .map(|link| {
2601 if !(link.is_reference && link.url.is_empty()) {
2602 end = end.max(link.byte_end);
2603 }
2604 end
2605 })
2606 .collect()
2607 });
2608 ends[count - 1] != 0 && ends[count - 1] >= range.end
2609 }
2610
2611 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
2613 let normalized_id = ref_id.to_lowercase();
2614 self.reference_defs_map
2615 .get(&normalized_id)
2616 .map(|&index| &self.reference_defs[index])
2617 }
2618
2619 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
2621 self.reference_definition(ref_id)
2622 .map(|definition| definition.url.as_str())
2623 }
2624
2625 pub fn heading_container(&self, line_num: usize) -> HeadingContainer {
2633 self.line_info(line_num)
2634 .and_then(|line| line.heading_container)
2635 .unwrap_or(HeadingContainer::Unknown)
2636 }
2637
2638 pub fn is_in_list_block(&self, line_num: usize) -> bool {
2640 if line_num == 0 || line_num > self.lines.len() {
2641 return false;
2642 }
2643 self.lines[line_num - 1].in_list_block
2644 }
2645
2646 pub fn is_in_definition_list(&self, line_num: usize) -> bool {
2654 let lists = &self.definition_lists.outer;
2655 let idx = lists.partition_point(|&(start, _)| start <= line_num);
2656 idx > 0 && line_num <= lists[idx - 1].1
2657 }
2658
2659 pub fn is_definition_term(&self, line_num: usize) -> bool {
2661 let terms = &self.definition_lists.terms;
2662 let idx = terms.partition_point(|&(_, end)| end < line_num);
2663 terms.get(idx).is_some_and(|&(start, _)| start <= line_num)
2664 }
2665
2666 pub fn in_colon_definition(&self, line_num: usize) -> bool {
2674 let ranges = &self.colon_definition_lines;
2675 let idx = ranges.partition_point(|&(_, end)| end < line_num);
2676 ranges.get(idx).is_some_and(|&(start, _)| start <= line_num)
2677 }
2678
2679 pub fn definition_text_at(&self, line_num: usize) -> Option<&DefinitionText> {
2681 let texts = &self.definition_lists.texts;
2682 let idx = texts.partition_point(|text| text.end_line < line_num);
2683 texts.get(idx).filter(|text| text.start_line <= line_num)
2684 }
2685
2686 pub fn line_holds_html_block(&self, line_idx: usize) -> bool {
2691 self.native_list_source().line_holds_html_block(line_idx)
2692 }
2693
2694 pub fn is_in_html_block(&self, line_num: usize) -> bool {
2696 if line_num == 0 || line_num > self.lines.len() {
2697 return false;
2698 }
2699 self.lines[line_num - 1].in_html_block
2700 }
2701
2702 pub fn is_in_table_block(&self, line_num: usize) -> bool {
2708 if line_num == 0 || line_num > self.lines.len() {
2709 return false;
2710 }
2711 self.lines[line_num - 1].in_table_block
2712 }
2713
2714 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
2716 if line_num == 0 || line_num > self.lines.len() {
2717 return false;
2718 }
2719
2720 let col_0indexed = if col > 0 { col - 1 } else { 0 };
2724 let code_spans = self.source_code_spans();
2725 code_spans.iter().any(|span| {
2726 if line_num < span.line || line_num > span.end_line {
2728 return false;
2729 }
2730
2731 if span.line == span.end_line {
2732 col_0indexed >= span.start_col && col_0indexed < span.end_col
2734 } else if line_num == span.line {
2735 col_0indexed >= span.start_col
2737 } else if line_num == span.end_line {
2738 col_0indexed < span.end_col
2740 } else {
2741 true
2743 }
2744 })
2745 }
2746
2747 #[inline]
2749 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
2750 Self::binary_search_ranges(&self.inline_code_ranges, byte_offset)
2751 }
2752
2753 #[inline]
2755 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
2756 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
2757 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
2758 }
2759
2760 #[inline]
2762 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
2763 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
2764 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
2765 }
2766
2767 #[inline]
2770 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
2771 let tags = self.html_tags_shared();
2772
2773 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2774 idx > 0 && byte_pos < tags[idx - 1].byte_end
2775 }
2776
2777 #[inline]
2781 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
2782 if !self.flavor.supports_jsx() {
2783 return false;
2784 }
2785
2786 let tags = self.jsx_component_tags_shared();
2787
2788 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
2789 idx > 0 && byte_pos < tags[idx - 1].byte_end
2790 }
2791
2792 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
2794 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
2795 }
2796
2797 #[inline]
2800 pub(crate) fn overlaps_template_code(&self, start: usize, end: usize) -> bool {
2801 if start >= end {
2802 return false;
2803 }
2804 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2805 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2806 idx > 0 && ranges[idx - 1].1 > start
2807 })
2808 }
2809
2810 #[inline]
2813 pub(crate) fn is_inside_template_code(&self, pos: usize) -> bool {
2814 [&self.jinja_ranges, &self.shortcode_ranges].into_iter().any(|ranges| {
2815 let idx = ranges.partition_point(|&(start, _)| start < pos);
2816 idx > 0 && ranges[idx - 1].1 > pos
2817 })
2818 }
2819
2820 #[inline]
2822 pub(crate) fn is_in_jinja_string(&self, byte_pos: usize) -> bool {
2823 let ranges = self
2824 .jinja_string_ranges_cache
2825 .get_or_init(|| crate::utils::jinja_utils::find_jinja_string_ranges(self.content, &self.jinja_ranges));
2826 Self::binary_search_ranges(ranges, byte_pos)
2827 }
2828
2829 #[inline]
2831 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
2832 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
2833 }
2834
2835 #[inline]
2837 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
2838 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
2839 }
2840
2841 pub(crate) fn mdx_comment_ranges(&self) -> &[(usize, usize)] {
2843 &self.mdx_comment_ranges
2844 }
2845
2846 pub(crate) fn mdx_inline_code_ranges(&self, start: usize, end: usize) -> impl Iterator<Item = (usize, usize)> + '_ {
2848 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2849 .into_iter()
2850 .flat_map(move |ranges| {
2851 let first = ranges.partition_point(|&(_, range_end)| range_end <= start);
2852 ranges[first..]
2853 .iter()
2854 .copied()
2855 .take_while(move |&(range_start, _)| start < end && range_start < end)
2856 })
2857 }
2858
2859 #[inline]
2862 pub(crate) fn overlaps_mdx_inline_code(&self, start: usize, end: usize) -> bool {
2863 if start >= end {
2864 return false;
2865 }
2866 [&self.jsx_expression_ranges, &self.mdx_comment_ranges]
2867 .into_iter()
2868 .any(|ranges| {
2869 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2870 idx > 0 && ranges[idx - 1].1 > start
2871 })
2872 }
2873
2874 pub(crate) fn is_inside_mdx_code(&self, pos: usize) -> bool {
2877 if self.flavor != MarkdownFlavor::MDX {
2878 return false;
2879 }
2880 if [
2881 &self.mdx_esm_ranges,
2882 &self.jsx_expression_ranges,
2883 &self.mdx_comment_ranges,
2884 ]
2885 .into_iter()
2886 .any(|ranges| {
2887 let idx = ranges.partition_point(|&(start, _)| start < pos);
2888 idx > 0 && ranges[idx - 1].1 > pos
2889 }) {
2890 return true;
2891 }
2892
2893 let tags = [self.html_tags_shared(), self.jsx_component_tags_shared()];
2894
2895 tags.into_iter().any(|tags| {
2896 let idx = tags.partition_point(|tag| tag.byte_offset < pos);
2897 idx > 0 && tags[idx - 1].byte_end > pos
2898 })
2899 }
2900
2901 pub(crate) fn overlaps_mdx_code(&self, start: usize, end: usize) -> bool {
2903 if start >= end || self.flavor != MarkdownFlavor::MDX {
2904 return false;
2905 }
2906 if [
2907 &self.mdx_esm_ranges,
2908 &self.jsx_expression_ranges,
2909 &self.mdx_comment_ranges,
2910 ]
2911 .into_iter()
2912 .any(|ranges| {
2913 let idx = ranges.partition_point(|&(range_start, _)| range_start < end);
2914 idx > 0 && ranges[idx - 1].1 > start
2915 }) {
2916 return true;
2917 }
2918
2919 let tags = [self.html_tags_shared(), self.jsx_component_tags_shared()];
2920
2921 tags.into_iter().any(|tags| {
2922 let idx = tags.partition_point(|tag| tag.byte_offset < end);
2923 idx > 0 && tags[idx - 1].byte_end > start
2924 })
2925 }
2926
2927 #[inline]
2930 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
2931 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
2932 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
2933 }
2934
2935 #[inline]
2937 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
2938 &self.citation_ranges
2939 }
2940
2941 #[inline]
2944 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
2945 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
2946 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
2947 }
2948
2949 #[inline]
2952 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
2953 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
2954 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
2955 }
2956
2957 #[inline]
2960 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
2961 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
2962 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
2963 }
2964
2965 #[inline]
2968 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
2969 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
2970 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
2971 }
2972
2973 #[inline]
2976 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
2977 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
2978 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
2979 }
2980
2981 #[inline]
2985 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
2986 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
2987 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
2988 }
2989
2990 #[inline]
2993 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
2994 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
2995 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
2996 }
2997
2998 #[inline]
3001 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
3002 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
3003 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
3004 }
3005
3006 #[inline]
3010 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
3011 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
3012 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
3013 }
3014
3015 #[inline]
3018 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
3019 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
3020 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
3021 }
3022
3023 #[inline]
3026 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
3027 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
3028 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
3029 }
3030
3031 #[inline]
3034 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
3035 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
3036 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
3037 }
3038
3039 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
3044 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
3045 self.pandoc_header_slugs.contains(&slug)
3046 }
3047
3048 #[inline]
3054 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
3055 self.pandoc_header_slugs.contains(slug)
3056 }
3057
3058 #[inline]
3060 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
3061 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
3062 }
3063
3064 #[inline]
3066 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
3067 &self.shortcode_ranges
3068 }
3069
3070 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
3072 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
3073 }
3074
3075 pub fn has_char(&self, ch: char) -> bool {
3077 match ch {
3078 '#' => self.char_frequency.hash_count > 0,
3079 '*' => self.char_frequency.asterisk_count > 0,
3080 '_' => self.char_frequency.underscore_count > 0,
3081 '-' => self.char_frequency.hyphen_count > 0,
3082 '+' => self.char_frequency.plus_count > 0,
3083 '>' => self.char_frequency.gt_count > 0,
3084 '|' => self.char_frequency.pipe_count > 0,
3085 '[' => self.char_frequency.bracket_count > 0,
3086 '`' => self.char_frequency.backtick_count > 0,
3087 '<' => self.char_frequency.lt_count > 0,
3088 '!' => self.char_frequency.exclamation_count > 0,
3089 '\n' => self.char_frequency.newline_count > 0,
3090 '$' => self.line_candidates.has_signal(candidates::DOLLAR),
3091 '=' => self.line_candidates.has_signal(candidates::EQUALS),
3092 _ => self.content.contains(ch), }
3094 }
3095
3096 pub fn char_count(&self, ch: char) -> usize {
3098 match ch {
3099 '#' => self.char_frequency.hash_count,
3100 '*' => self.char_frequency.asterisk_count,
3101 '_' => self.char_frequency.underscore_count,
3102 '-' => self.char_frequency.hyphen_count,
3103 '+' => self.char_frequency.plus_count,
3104 '>' => self.char_frequency.gt_count,
3105 '|' => self.char_frequency.pipe_count,
3106 '[' => self.char_frequency.bracket_count,
3107 '`' => self.char_frequency.backtick_count,
3108 '<' => self.char_frequency.lt_count,
3109 '!' => self.char_frequency.exclamation_count,
3110 '\n' => self.char_frequency.newline_count,
3111 _ => self.content.matches(ch).count(), }
3113 }
3114
3115 pub fn likely_has_headings(&self) -> bool {
3118 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 0 || self.has_char('=')
3119 }
3120
3121 pub fn likely_has_lists(&self) -> bool {
3125 self.char_frequency.asterisk_count > 0
3126 || self.char_frequency.hyphen_count > 0
3127 || self.char_frequency.plus_count > 0
3128 }
3129
3130 pub fn likely_has_emphasis(&self) -> bool {
3132 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
3133 }
3134
3135 pub fn likely_has_tables(&self) -> bool {
3137 self.char_frequency.pipe_count > 2
3138 }
3139
3140 pub fn likely_has_blockquotes(&self) -> bool {
3142 self.char_frequency.gt_count > 0
3143 }
3144
3145 pub fn likely_has_code(&self) -> bool {
3147 self.char_frequency.backtick_count > 0
3148 }
3149
3150 pub fn likely_has_links_or_images(&self) -> bool {
3152 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
3153 }
3154
3155 pub fn likely_has_html(&self) -> bool {
3157 self.char_frequency.lt_count > 0
3158 }
3159
3160 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
3165 if let Some(line_info) = self.lines.get(line_idx)
3166 && let Some(ref bq) = line_info.blockquote
3167 {
3168 bq.prefix.trim_end().to_string()
3169 } else {
3170 String::new()
3171 }
3172 }
3173
3174 #[inline]
3185 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
3186 let idx = match lines.binary_search_by(|line| {
3188 if byte_offset < line.byte_offset {
3189 std::cmp::Ordering::Greater
3190 } else if byte_offset > line.byte_offset + line.byte_len {
3191 std::cmp::Ordering::Less
3192 } else {
3193 std::cmp::Ordering::Equal
3194 }
3195 }) {
3196 Ok(idx) => idx,
3197 Err(idx) => idx.saturating_sub(1),
3198 };
3199
3200 let line = &lines[idx];
3201 let line_num = idx + 1;
3202 let byte_col = byte_offset.saturating_sub(line.byte_offset);
3203 let col = byte_to_char_count(line.content(content), byte_col) - 1;
3206
3207 (idx, line_num, col)
3208 }
3209
3210 #[must_use]
3230 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
3231 ValidHeadingsIter::new(&self.lines, &self.document_lines().headings)
3232 }
3233
3234 #[must_use]
3238 pub fn has_valid_headings(&self) -> bool {
3239 !self.document_lines().headings.is_empty()
3240 }
3241
3242 #[must_use]
3244 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
3245 ParsedListItemsIter::new(&self.lines, &self.document_lines().list_items)
3246 }
3247
3248 #[must_use]
3250 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
3251 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
3252 Some(ParsedListItem::new(
3253 line_num,
3254 line_info.list_item.as_deref()?,
3255 line_info,
3256 ))
3257 }
3258
3259 #[must_use]
3261 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
3262 ParsedListBlocks::new(&self.list_blocks, &self.lines)
3263 }
3264
3265 #[must_use]
3269 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
3270 list_blocks::item_lines_by_list(self.content, &self.lines, block)
3271 }
3272
3273 #[must_use]
3275 pub fn has_list_items(&self) -> bool {
3276 !self.document_lines().list_items.is_empty()
3277 }
3278
3279 #[must_use]
3281 pub fn has_unordered_list_items(&self) -> bool {
3282 self.document_lines().has_unordered_items
3283 }
3284
3285 #[must_use]
3287 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
3288 let lists = self
3289 .commonmark_ordered_lists_cache
3290 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
3291 CommonMarkOrderedLists::new(lists, &self.lines)
3292 }
3293
3294 #[must_use]
3302 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
3303 ParsedHeadingsIter::new(
3304 &self.lines,
3305 &self.blockquote_headings,
3306 &self.document_lines().parsed_headings,
3307 )
3308 }
3309
3310 #[must_use]
3312 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
3313 let idx = line_num.checked_sub(1)?;
3314 let line_info = self.lines.get(idx)?;
3315 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
3316 Some(heading) => (heading, 0),
3317 None => (
3318 self.blockquote_headings.get(idx)?.as_deref()?,
3319 line_info.blockquote.as_ref().map_or(1, |bq| bq.nesting_level),
3320 ),
3321 };
3322 Some(ParsedHeading {
3323 line_num,
3324 heading,
3325 line_info,
3326 text_line_infos: &self.lines[idx + 1 - heading.text_lines..=idx],
3327 blockquote_depth,
3328 })
3329 }
3330}
3331
3332fn container_comment_range(
3344 opener: usize,
3345 containers: &flavor_detection::ContainerLines,
3346 lines: &[types::LineInfo],
3347 content: &str,
3348) -> Option<crate::utils::skip_context::ByteRange> {
3349 let line_index = lines
3350 .partition_point(|line| line.byte_offset <= opener)
3351 .checked_sub(1)?;
3352 let line = lines.get(line_index)?;
3353 if line.byte_offset + line.indent != opener {
3354 return None;
3355 }
3356 if !containers.is_container_body(line_index) {
3357 return None;
3358 }
3359 let end_line = lines.get(containers.body_end_line(line_index)?)?;
3360 Some(crate::utils::skip_context::ByteRange {
3361 start: opener,
3362 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
3363 })
3364}
3365
3366#[derive(Debug)]
3368struct DefinitionListLines {
3369 outer: Vec<(usize, usize)>,
3372 terms: Vec<(usize, usize)>,
3374 texts: Vec<DefinitionText>,
3376}
3377
3378impl DefinitionListLines {
3379 fn new(
3380 line_offsets: &[usize],
3381 items: &[(usize, usize)],
3382 terms: &[(usize, usize)],
3383 texts: &[crate::utils::code_block_utils::DefinitionTextDetail],
3384 ) -> Self {
3385 let line_of = |byte: usize| line_offsets.partition_point(|&offset| offset <= byte).max(1);
3388 let last_line_of = |start: usize, end: usize| line_of(end.saturating_sub(1).max(start));
3389
3390 let mut outer: Vec<(usize, usize)> = Vec::new();
3393 for &(start, end) in items {
3394 let (start_line, end_line) = (line_of(start), last_line_of(start, end));
3395 match outer.last_mut() {
3396 Some(last) if start_line <= last.1 => last.1 = last.1.max(end_line),
3397 _ => outer.push((start_line, end_line)),
3398 }
3399 }
3400 let terms = terms
3401 .iter()
3402 .map(|&(start, end)| (line_of(start), last_line_of(start, end)))
3403 .collect();
3404
3405 let texts = texts
3406 .iter()
3407 .map(|text| {
3408 let start_line = line_of(text.start);
3409 let line_start = line_offsets[start_line - 1];
3410 let on_marker_line = line_of(text.definition_start) == start_line;
3411 DefinitionText {
3412 start_line,
3413 end_line: last_line_of(text.start, text.end),
3414 marker_prefix_len: on_marker_line.then(|| text.start - line_start),
3415 }
3416 })
3417 .collect();
3418
3419 Self { outer, terms, texts }
3420 }
3421}
3422
3423fn detect_footnote_definitions(
3436 content: &str,
3437 lines: &mut [types::LineInfo],
3438 line_offsets: &[usize],
3439) -> Vec<(usize, usize)> {
3440 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
3441
3442 if !content.contains("[^") {
3444 return Vec::new();
3445 }
3446
3447 let options = crate::utils::parser_options::rumdl_parser_options_for(content);
3448 let parser = Parser::new_ext(content, options).into_offset_iter();
3449
3450 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
3452 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
3453 let mut indented_code_ranges: Vec<(usize, usize)> = Vec::new();
3454 let mut in_footnote = false;
3455
3456 for (event, range) in parser {
3457 match event {
3458 Event::Start(Tag::FootnoteDefinition(_)) => {
3459 in_footnote = true;
3460 footnote_ranges.push((range.start, range.end));
3461 }
3462 Event::End(TagEnd::FootnoteDefinition) => {
3463 in_footnote = false;
3464 }
3465 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
3466 fenced_code_ranges.push((range.start, range.end));
3467 }
3468 Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) if in_footnote => {
3469 indented_code_ranges.push((range.start, range.end));
3470 }
3471 _ => {}
3472 }
3473 }
3474
3475 let byte_to_line = |byte_offset: usize| -> usize {
3476 line_offsets
3477 .partition_point(|&offset| offset <= byte_offset)
3478 .saturating_sub(1)
3479 };
3480
3481 for &(start, end) in &footnote_ranges {
3483 let start_line = byte_to_line(start);
3484 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
3485
3486 for line in &mut lines[start_line..end_line] {
3487 line.in_footnote_definition = true;
3488 line.in_code_block = false;
3489 }
3490 }
3491
3492 for &(start, end) in &fenced_code_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_code_block = true;
3499 }
3500 }
3501 indented_code_ranges
3502}