1pub mod types;
2pub use types::*;
3
4mod element_parsers;
5mod flavor_detection;
6mod heading_detection;
7mod line_computation;
8mod link_parser;
9mod list_blocks;
10#[cfg(test)]
11mod tests;
12
13use crate::config::MarkdownFlavor;
14use crate::inline_config::InlineConfig;
15use crate::rules::front_matter_utils::FrontMatterUtils;
16use crate::utils::code_block_utils::{CodeBlockDetail, CodeBlockUtils};
17use crate::utils::range_utils::byte_to_char_count;
18use std::collections::HashMap;
19use std::ops::Range;
20use std::path::{Path, PathBuf};
21
22#[cfg(not(target_arch = "wasm32"))]
24macro_rules! profile_section {
25 ($name:expr, $profile:expr, $code:expr) => {{
26 let start = std::time::Instant::now();
27 let result = $code;
28 if $profile {
29 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
30 }
31 result
32 }};
33}
34
35fn build_commonmark_ordered_lists(
36 lines: &[LineInfo],
37 line_to_list: &crate::utils::code_block_utils::LineToListMap,
38 list_start_values: &crate::utils::code_block_utils::ListStartValues,
39) -> Vec<CommonMarkOrderedListInfo> {
40 let mut grouped_lines: HashMap<usize, Vec<usize>> = HashMap::new();
41
42 for (&line_num, &list_id) in line_to_list {
43 let is_ordered_item = line_num
44 .checked_sub(1)
45 .and_then(|index| lines.get(index))
46 .and_then(|line| line.list_item.as_deref())
47 .is_some_and(|item| item.is_ordered);
48 if is_ordered_item {
49 grouped_lines.entry(list_id).or_default().push(line_num);
50 }
51 }
52
53 let mut lists: Vec<_> = grouped_lines
54 .into_iter()
55 .map(|(list_id, mut item_lines)| {
56 item_lines.sort_unstable();
57 CommonMarkOrderedListInfo {
58 start_value: list_start_values.get(&list_id).copied().unwrap_or(1),
59 item_lines,
60 }
61 })
62 .collect();
63 lists.sort_by_key(|list| list.item_lines.first().copied().unwrap_or(0));
64 lists
65}
66
67#[cfg(target_arch = "wasm32")]
68macro_rules! profile_section {
69 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
70}
71
72pub(super) struct SkipByteRanges<'a> {
75 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
76 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
77 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
78 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
79}
80
81use std::sync::{Arc, OnceLock};
82
83pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
85
86pub(super) type ByteRanges = Vec<(usize, usize)>;
88
89pub struct LintContext<'a> {
90 pub content: &'a str,
91 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
93 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, commonmark_ordered_lists_cache: OnceLock<Vec<CommonMarkOrderedListInfo>>, pub lines: Vec<LineInfo>, blockquote_headings: Vec<Option<Box<HeadingInfo>>>, links: Vec<ParsedLink<'a>>, images: Vec<ParsedImage<'a>>, broken_links: Vec<BrokenLinkInfo>, footnote_refs: Vec<FootnoteRef>, reference_defs: Vec<ReferenceDef>, reference_defs_map: HashMap<String, usize>, code_spans_cache: OnceLock<Arc<Vec<CodeSpan>>>, math_spans_cache: OnceLock<Arc<Vec<MathSpan>>>, math_byte_ranges_cache: OnceLock<Vec<(usize, usize)>>, pub list_blocks: Vec<ListBlock>, pub char_frequency: CharFrequency, html_tags_cache: OnceLock<Arc<Vec<HtmlTag>>>, jsx_component_tags_cache: OnceLock<Arc<Vec<HtmlTag>>>, emphasis_spans_cache: OnceLock<Arc<Vec<EmphasisSpan>>>, bare_urls_cache: OnceLock<Arc<Vec<BareUrl>>>, has_mixed_list_nesting_cache: OnceLock<bool>, html_comment_ranges: Vec<crate::utils::skip_context::ByteRange>, pub table_blocks: Vec<crate::utils::table_utils::TableBlock>, line_index: crate::utils::range_utils::LineIndex<'a>, jinja_ranges: Vec<(usize, usize)>, pub flavor: MarkdownFlavor, source_file: Option<PathBuf>, jsx_expression_ranges: Vec<(usize, usize)>, mdx_comment_ranges: Vec<(usize, usize)>, citation_ranges: Vec<crate::utils::skip_context::ByteRange>, pandoc_div_ranges: Vec<crate::utils::skip_context::ByteRange>, colon_fence_details: Vec<CodeBlockDetail>, inline_footnote_ranges: Vec<crate::utils::skip_context::ByteRange>, pandoc_header_slugs: std::collections::HashSet<String>, example_list_marker_ranges: Vec<crate::utils::skip_context::ByteRange>, example_reference_ranges: Vec<crate::utils::skip_context::ByteRange>, sub_super_ranges: Vec<crate::utils::skip_context::ByteRange>, inline_code_attr_ranges: Vec<crate::utils::skip_context::ByteRange>, bracketed_span_ranges: Vec<crate::utils::skip_context::ByteRange>, line_block_ranges: Vec<crate::utils::skip_context::ByteRange>, pipe_table_caption_ranges: Vec<crate::utils::skip_context::ByteRange>, pandoc_metadata_ranges: Vec<crate::utils::skip_context::ByteRange>, grid_table_ranges: Vec<crate::utils::skip_context::ByteRange>, multi_line_table_ranges: Vec<crate::utils::skip_context::ByteRange>, shortcode_ranges: Vec<(usize, usize)>, link_title_ranges: Vec<(usize, usize)>, code_span_byte_ranges: Vec<(usize, usize)>, inline_config: InlineConfig, obsidian_comment_ranges: Vec<(usize, usize)>, unterminated_html_comment: Option<usize>, unterminated_obsidian_comment: Option<usize>, lazy_cont_lines_cache: OnceLock<Arc<Vec<LazyContLine>>>, myst_directive_ranges: Vec<(usize, usize)>, myst_comment_ranges: Vec<(usize, usize)>, myst_role_ranges: Vec<(usize, usize)>, front_matter_end: usize, }
153
154pub fn code_block_ranges(content: &str, flavor: MarkdownFlavor) -> Vec<(usize, usize)> {
165 LintContext::new(content, flavor, None).code_blocks
166}
167
168impl<'a> LintContext<'a> {
169 pub fn source_file(&self) -> Option<&Path> {
174 self.source_file.as_deref()
175 }
176
177 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
178 #[cfg(not(target_arch = "wasm32"))]
179 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
180
181 let line_offsets = profile_section!("Line offsets", profile, {
182 let mut offsets = vec![0];
183 for (i, c) in content.char_indices() {
184 if c == '\n' {
185 offsets.push(i + 1);
186 }
187 }
188 offsets
189 });
190
191 let content_lines: Vec<&str> = content.lines().collect();
193
194 #[allow(clippy::disallowed_methods)]
198 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
199
200 let parse_result = profile_section!(
202 "Code blocks",
203 profile,
204 CodeBlockUtils::detect_code_blocks_and_spans(content)
205 );
206 let mut code_blocks = parse_result.code_blocks;
207 let code_span_ranges = parse_result.code_spans;
208 let code_block_details = parse_result.code_block_details;
209 let strong_spans = parse_result.strong_spans;
210 let line_to_list = parse_result.line_to_list;
211 let list_start_values = parse_result.list_start_values;
212 let html_blocks = parse_result.html_blocks;
213
214 let containers = profile_section!(
217 "Container lines",
218 profile,
219 flavor_detection::detect_container_lines(&content_lines, flavor)
220 );
221
222 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
231 .iter()
232 .flat_map(|detail| {
233 if detail.is_fenced {
234 return vec![(detail.start, detail.end)];
235 }
236 let start_line = line_offsets
237 .partition_point(|&offset| offset <= detail.start)
238 .saturating_sub(1);
239 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
240 containers
241 .code_line_spans_in(start_line..end_line)
242 .into_iter()
243 .map(|span| {
244 let start = line_offsets[span.start].max(detail.start);
245 let end = line_offsets
246 .get(span.end)
247 .copied()
248 .unwrap_or(content.len())
249 .min(detail.end);
250 (start, end)
251 })
252 .collect()
253 })
254 .collect();
255 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
261 let html_comment_scan = profile_section!(
262 "HTML comment ranges",
263 profile,
264 crate::utils::skip_context::scan_html_comments(
265 content,
266 &code_span_ranges,
267 &comment_code_block_ranges,
268 body_start
269 )
270 );
271 let mut html_comment_ranges = html_comment_scan.ranges;
272 let unterminated_html_comment = html_comment_scan.unterminated;
273
274 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
278 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
279 Vec::new()
280 } else {
281 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
282 }
283 });
284
285 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
287 if flavor.is_pandoc_compatible() {
288 crate::utils::pandoc::detect_div_block_ranges(content)
289 } else {
290 Vec::new()
291 }
292 });
293
294 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
296 if flavor == MarkdownFlavor::MkDocs {
297 crate::utils::pymdown_blocks::detect_block_ranges(content)
298 } else {
299 Vec::new()
300 }
301 });
302
303 let skip_ranges = SkipByteRanges {
306 html_comment_ranges: &html_comment_ranges,
307 autodoc_ranges: &autodoc_ranges,
308 pandoc_div_ranges: &pandoc_div_ranges,
309 pymdown_block_ranges: &pymdown_block_ranges,
310 };
311 let (mut lines, emphasis_spans) = profile_section!(
312 "Basic line info",
313 profile,
314 line_computation::compute_basic_line_info(
315 content,
316 &content_lines,
317 &line_offsets,
318 &code_blocks,
319 flavor,
320 &skip_ranges,
321 front_matter_end,
322 )
323 );
324
325 profile_section!(
327 "HTML blocks",
328 profile,
329 heading_detection::detect_html_blocks(content, &mut lines)
330 );
331
332 profile_section!(
334 "ESM blocks",
335 profile,
336 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
337 );
338
339 profile_section!(
341 "JSX block detection",
342 profile,
343 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
344 );
345
346 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
348 "JSX/MDX detection",
349 profile,
350 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
351 );
352
353 profile_section!(
358 "Markdown-in-HTML blocks",
359 profile,
360 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
361 );
362
363 profile_section!(
365 "MkDocs constructs",
366 profile,
367 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
368 );
369
370 profile_section!(
375 "Footnote definitions",
376 profile,
377 detect_footnote_definitions(content, &mut lines, &line_offsets)
378 );
379
380 {
383 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
384 for &(start, end) in &code_blocks {
385 let start_line = line_offsets
386 .partition_point(|&offset| offset <= start)
387 .saturating_sub(1);
388 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
389
390 let mut sub_start: Option<usize> = None;
391 for (i, &offset) in line_offsets[start_line..end_line]
392 .iter()
393 .enumerate()
394 .map(|(j, o)| (j + start_line, o))
395 {
396 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
397 if is_real_code && sub_start.is_none() {
398 let byte_start = if i == start_line { start } else { offset };
399 sub_start = Some(byte_start);
400 } else if !is_real_code && sub_start.is_some() {
401 new_code_blocks.push((sub_start.unwrap(), offset));
402 sub_start = None;
403 }
404 }
405 if let Some(s) = sub_start {
406 new_code_blocks.push((s, end));
407 }
408 }
409 code_blocks = new_code_blocks;
410 }
411
412 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
420 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
421 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
422 for &(start, end) in &code_blocks {
423 let start_line = line_offsets
424 .partition_point(|&offset| offset <= start)
425 .saturating_sub(1);
426 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
427
428 let mut sub_start: Option<usize> = None;
430 for (i, &offset) in line_offsets[start_line..end_line]
431 .iter()
432 .enumerate()
433 .map(|(j, o)| (j + start_line, o))
434 {
435 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
436 if is_real_code && sub_start.is_none() {
437 let byte_start = if i == start_line { start } else { offset };
438 sub_start = Some(byte_start);
439 } else if !is_real_code && sub_start.is_some() {
440 new_code_blocks.push((sub_start.unwrap(), offset));
441 sub_start = None;
442 }
443 }
444 if let Some(s) = sub_start {
445 new_code_blocks.push((s, end));
446 }
447 }
448 code_blocks = new_code_blocks;
449 }
450
451 if flavor.supports_jsx() {
455 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
456 for &(start, end) in &code_blocks {
457 let start_line = line_offsets
458 .partition_point(|&offset| offset <= start)
459 .saturating_sub(1);
460 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
461
462 let mut sub_start: Option<usize> = None;
463 for (i, &offset) in line_offsets[start_line..end_line]
464 .iter()
465 .enumerate()
466 .map(|(j, o)| (j + start_line, o))
467 {
468 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
469 if is_real_code && sub_start.is_none() {
470 let byte_start = if i == start_line { start } else { offset };
471 sub_start = Some(byte_start);
472 } else if !is_real_code && sub_start.is_some() {
473 new_code_blocks.push((sub_start.unwrap(), offset));
474 sub_start = None;
475 }
476 }
477 if let Some(s) = sub_start {
478 new_code_blocks.push((s, end));
479 }
480 }
481 code_blocks = new_code_blocks;
482
483 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
490 let mut run: Option<(usize, usize)> = None;
491 for line in &lines {
492 if line.in_jsx_block && line.in_code_block {
493 let line_end = line.byte_offset + line.byte_len;
494 match &mut run {
495 Some((_, end)) => *end = line_end,
496 None => run = Some((line.byte_offset, line_end)),
497 }
498 } else if let Some(r) = run.take() {
499 jsx_fence_ranges.push(r);
500 }
501 }
502 if let Some(r) = run.take() {
503 jsx_fence_ranges.push(r);
504 }
505 if !jsx_fence_ranges.is_empty() {
506 code_blocks.extend(jsx_fence_ranges);
507 code_blocks.sort_by_key(|&(start, _)| start);
508 }
509 }
510
511 let colon_fence_details = profile_section!(
514 "Azure colon fence detection",
515 profile,
516 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
517 );
518 if !colon_fence_details.is_empty() {
519 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
520 code_blocks.sort_by_key(|&(start, _)| start);
521 }
522
523 let myst_directive_ranges = profile_section!(
526 "MyST colon directives",
527 profile,
528 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
529 );
530
531 let myst_comment_ranges = profile_section!(
533 "MyST comments",
534 profile,
535 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
536 );
537
538 profile_section!(
541 "MyST backtick directives",
542 profile,
543 flavor_detection::detect_myst_backtick_directives(
544 content,
545 &mut lines,
546 flavor,
547 &code_block_details,
548 &line_offsets
549 )
550 );
551
552 if flavor.supports_myst_directives() {
555 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
556 for &(start, end) in &code_blocks {
557 let start_line = line_offsets
558 .partition_point(|&offset| offset <= start)
559 .saturating_sub(1);
560 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
561
562 let mut sub_start: Option<usize> = None;
563 for (i, &offset) in line_offsets[start_line..end_line]
564 .iter()
565 .enumerate()
566 .map(|(j, o)| (j + start_line, o))
567 {
568 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
569 if is_real_code && sub_start.is_none() {
570 let byte_start = if i == start_line { start } else { offset };
571 sub_start = Some(byte_start);
572 } else if !is_real_code && sub_start.is_some() {
573 new_code_blocks.push((sub_start.unwrap(), offset));
574 sub_start = None;
575 }
576 }
577 if let Some(s) = sub_start {
578 new_code_blocks.push((s, end));
579 }
580 }
581 code_blocks = new_code_blocks;
582 }
583
584 profile_section!(
586 "Kramdown constructs",
587 profile,
588 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
589 );
590
591 for line in &mut lines {
596 if line.in_kramdown_extension_block {
597 line.list_item = None;
598 line.is_horizontal_rule = false;
599 line.blockquote = None;
600 line.is_kramdown_block_ial = false;
601 }
602 }
603
604 let obsidian_comment_scan = profile_section!(
606 "Obsidian comments",
607 profile,
608 flavor_detection::detect_obsidian_comments(
609 content,
610 &mut lines,
611 flavor,
612 &code_span_ranges,
613 &html_comment_ranges,
614 body_start
615 )
616 );
617 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
618 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
619
620 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
625 unterminated_html_comment,
626 &obsidian_comment_ranges,
627 content,
628 &code_span_ranges,
629 &comment_code_block_ranges,
630 body_start,
631 );
632
633 if let Some(range) = unterminated_html_comment.and_then(|opener| {
646 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
647 .or_else(|| container_comment_range(opener, &containers, &lines, content))
648 }) {
649 html_comment_ranges.push(range);
652
653 for line in &mut lines {
659 let text = line.content(content);
660 let content_start = line.byte_offset + line.indent;
661 let content_end = line.byte_offset + text.trim_end().len();
662 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
663 &html_comment_ranges,
664 content_start,
665 content_end,
666 );
667 line.in_obsidian_comment = false;
668 }
669
670 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
681 content,
682 &mut lines,
683 flavor,
684 &code_span_ranges,
685 &html_comment_ranges,
686 body_start,
687 );
688 obsidian_comment_ranges = obsidian_rescan.ranges;
689 unterminated_obsidian_comment = obsidian_rescan.unterminated;
690 }
691
692 let myst_role_ranges = profile_section!(
694 "MyST roles",
695 profile,
696 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
697 );
698
699 let pulldown_result = profile_section!(
703 "Links, images & link ranges",
704 profile,
705 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
706 );
707
708 let mut blockquote_headings = profile_section!(
710 "Headings & blockquotes",
711 profile,
712 heading_detection::detect_headings_and_blockquotes(
713 &content_lines,
714 &mut lines,
715 flavor,
716 &html_comment_ranges,
717 &pulldown_result.link_byte_ranges,
718 front_matter_end,
719 )
720 );
721
722 for line in &mut lines {
724 if line.in_kramdown_extension_block {
725 line.heading = None;
726 }
727 }
728 for (line, heading) in lines.iter().zip(&mut blockquote_headings) {
729 if line.in_kramdown_extension_block {
730 *heading = None;
731 }
732 }
733
734 for line in &mut lines {
745 if line.is_horizontal_rule
746 && (line.in_code_block
747 || line.in_html_block
748 || line.in_html_comment
749 || line.in_math_block
750 || line.in_mdx_comment
751 || line.in_obsidian_comment)
752 {
753 line.is_horizontal_rule = false;
754 }
755 }
756
757 let mut code_spans = profile_section!(
759 "Code spans",
760 profile,
761 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
762 );
763
764 if flavor == MarkdownFlavor::MkDocs {
768 let extra = profile_section!(
769 "MkDocs code spans",
770 profile,
771 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
772 );
773 if !extra.is_empty() {
774 code_spans.extend(extra);
775 code_spans.sort_by_key(|span| span.byte_offset);
776 }
777 }
778
779 if flavor == MarkdownFlavor::MDX {
784 let extra = profile_section!(
785 "MDX JSX code spans",
786 profile,
787 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
788 );
789 if !extra.is_empty() {
790 code_spans.extend(extra);
791 code_spans.sort_by_key(|span| span.byte_offset);
792 }
793 }
794
795 for span in &code_spans {
798 if span.end_line > span.line {
799 for line_num in (span.line + 1)..=span.end_line {
801 if let Some(line_info) = lines.get_mut(line_num - 1) {
802 line_info.in_code_span_continuation = true;
803 }
804 }
805 }
806 }
807
808 let (links, images, broken_links, footnote_refs) = profile_section!(
810 "Links & images finalize",
811 profile,
812 link_parser::finalize_links_and_images(
813 content,
814 &lines,
815 &code_blocks,
816 &code_spans,
817 flavor,
818 &html_comment_ranges,
819 pulldown_result
820 )
821 );
822
823 let reference_defs = profile_section!(
824 "Reference defs",
825 profile,
826 link_parser::parse_reference_defs(content, &lines)
827 );
828
829 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
830
831 let char_frequency = profile_section!(
833 "Char frequency",
834 profile,
835 line_computation::compute_char_frequency(content)
836 );
837
838 let table_blocks = profile_section!(
840 "Table blocks",
841 profile,
842 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
843 content,
844 &code_blocks,
845 &code_spans,
846 &html_comment_ranges,
847 flavor,
848 )
849 );
850
851 let links = links
854 .into_iter()
855 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
856 .collect::<Vec<_>>();
857 let images = images
858 .into_iter()
859 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
860 .collect::<Vec<_>>();
861 let broken_links = broken_links
862 .into_iter()
863 .filter(|bl| {
864 let line_idx = line_offsets
866 .partition_point(|&offset| offset <= bl.span.start)
867 .saturating_sub(1);
868 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
869 })
870 .collect::<Vec<_>>();
871 let footnote_refs = footnote_refs
872 .into_iter()
873 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
874 .collect::<Vec<_>>();
875 let reference_defs = reference_defs
876 .into_iter()
877 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
878 .collect::<Vec<_>>();
879 let list_blocks = list_blocks
880 .into_iter()
881 .filter(|block| {
882 !lines
883 .get(block.start_line - 1)
884 .is_some_and(|l| l.in_kramdown_extension_block)
885 })
886 .collect::<Vec<_>>();
887 let table_blocks = table_blocks
888 .into_iter()
889 .filter(|block| {
890 !lines
892 .get(block.start_line)
893 .is_some_and(|l| l.in_kramdown_extension_block)
894 })
895 .collect::<Vec<_>>();
896 let emphasis_spans = emphasis_spans
897 .into_iter()
898 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
899 .collect::<Vec<_>>();
900
901 for block in &list_blocks {
905 for line_num in block.start_line..=block.end_line {
907 if let Some(li) = lines.get_mut(line_num - 1) {
908 li.in_list_block = true;
909 }
910 }
911 }
912 for block in &table_blocks {
913 for idx in block.start_line..=block.end_line {
915 if let Some(li) = lines.get_mut(idx) {
916 li.in_table_block = true;
917 }
918 }
919 }
920
921 let reference_defs_map: HashMap<String, usize> = reference_defs
923 .iter()
924 .enumerate()
925 .map(|(idx, def)| (def.id.to_lowercase(), idx))
926 .collect();
927
928 let link_title_ranges: Vec<(usize, usize)> = reference_defs
930 .iter()
931 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
932 (Some(start), Some(end)) => Some((start, end)),
933 _ => None,
934 })
935 .collect();
936
937 let line_index = profile_section!(
939 "Line index",
940 profile,
941 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
942 content,
943 line_offsets.clone(),
944 &code_blocks,
945 )
946 );
947
948 let jinja_ranges = profile_section!(
950 "Jinja ranges",
951 profile,
952 crate::utils::jinja_utils::find_jinja_ranges(content)
953 );
954
955 let citation_ranges = profile_section!("Citation ranges", profile, {
957 if flavor.is_pandoc_compatible() {
958 crate::utils::pandoc::find_citation_ranges(content)
959 } else {
960 Vec::new()
961 }
962 });
963
964 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
966 if flavor.is_pandoc_compatible() {
967 crate::utils::pandoc::detect_inline_footnote_ranges(content)
968 } else {
969 Vec::new()
970 }
971 });
972
973 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
975 if flavor.is_pandoc_compatible() {
976 crate::utils::pandoc::collect_pandoc_header_slugs(content)
977 } else {
978 std::collections::HashSet::new()
979 }
980 });
981
982 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
984 if flavor.is_pandoc_compatible() {
985 crate::utils::pandoc::detect_example_list_marker_ranges(content)
986 } else {
987 Vec::new()
988 }
989 });
990
991 let example_reference_ranges = profile_section!("Example references", profile, {
993 if flavor.is_pandoc_compatible() {
994 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
995 } else {
996 Vec::new()
997 }
998 });
999
1000 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
1002 if flavor.is_pandoc_compatible() {
1003 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
1004 } else {
1005 Vec::new()
1006 }
1007 });
1008
1009 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
1011 if flavor.is_pandoc_compatible() {
1012 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
1013 } else {
1014 Vec::new()
1015 }
1016 });
1017
1018 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
1020 if flavor.is_pandoc_compatible() {
1021 crate::utils::pandoc::detect_bracketed_span_ranges(content)
1022 } else {
1023 Vec::new()
1024 }
1025 });
1026
1027 let line_block_ranges = profile_section!("Line block ranges", profile, {
1029 if flavor.is_pandoc_compatible() {
1030 crate::utils::pandoc::detect_line_block_ranges(content)
1031 } else {
1032 Vec::new()
1033 }
1034 });
1035
1036 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
1038 if flavor.is_pandoc_compatible() {
1039 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
1040 } else {
1041 Vec::new()
1042 }
1043 });
1044
1045 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
1047 if flavor.is_pandoc_compatible() {
1048 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1049 } else {
1050 Vec::new()
1051 }
1052 });
1053
1054 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1056 if flavor.is_pandoc_compatible() {
1057 crate::utils::pandoc::detect_grid_table_ranges(content)
1058 } else {
1059 Vec::new()
1060 }
1061 });
1062
1063 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1065 if flavor.is_pandoc_compatible() {
1066 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1067 } else {
1068 Vec::new()
1069 }
1070 });
1071
1072 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1074 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
1075 let mut ranges = Vec::new();
1076 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
1077 ranges.push((mat.start(), mat.end()));
1078 }
1079 ranges
1080 });
1081
1082 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks);
1083 Self {
1084 content,
1085 content_lines,
1086 line_offsets,
1087 code_blocks,
1088 code_block_details,
1089 strong_spans,
1090 line_to_list,
1091 list_start_values,
1092 commonmark_ordered_lists_cache: OnceLock::new(),
1093 lines,
1094 blockquote_headings,
1095 links,
1096 images,
1097 broken_links,
1098 footnote_refs,
1099 reference_defs,
1100 reference_defs_map,
1101 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1102 math_spans_cache: OnceLock::new(), math_byte_ranges_cache: OnceLock::new(), list_blocks,
1105 char_frequency,
1106 html_tags_cache: OnceLock::new(),
1107 jsx_component_tags_cache: OnceLock::new(),
1108 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1109 bare_urls_cache: OnceLock::new(),
1110 has_mixed_list_nesting_cache: OnceLock::new(),
1111 html_comment_ranges,
1112 table_blocks,
1113 line_index,
1114 jinja_ranges,
1115 flavor,
1116 source_file,
1117 jsx_expression_ranges,
1118 mdx_comment_ranges,
1119 citation_ranges,
1120 pandoc_div_ranges,
1121 colon_fence_details,
1122 inline_footnote_ranges,
1123 pandoc_header_slugs,
1124 example_list_marker_ranges,
1125 example_reference_ranges,
1126 sub_super_ranges,
1127 inline_code_attr_ranges,
1128 bracketed_span_ranges,
1129 line_block_ranges,
1130 pipe_table_caption_ranges,
1131 pandoc_metadata_ranges,
1132 grid_table_ranges,
1133 multi_line_table_ranges,
1134 shortcode_ranges,
1135 link_title_ranges,
1136 code_span_byte_ranges: code_span_ranges,
1137 inline_config,
1138 obsidian_comment_ranges,
1139 unterminated_html_comment,
1140 unterminated_obsidian_comment,
1141 lazy_cont_lines_cache: OnceLock::new(),
1142 myst_directive_ranges,
1143 myst_comment_ranges,
1144 myst_role_ranges,
1145 front_matter_end,
1146 }
1147 }
1148
1149 pub fn front_matter_end_line(&self) -> usize {
1154 self.front_matter_end
1155 }
1156
1157 #[inline]
1160 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1161 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1163 idx > 0 && pos < ranges[idx - 1].1
1165 }
1166
1167 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1169 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1170 }
1171
1172 pub fn is_in_link(&self, pos: usize) -> bool {
1174 self.link_containing(pos).is_some() || self.image_containing(pos).is_some() || self.is_in_reference_def(pos)
1175 }
1176
1177 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1179 let bare_urls = self.bare_urls();
1180 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1182 idx > 0 && pos < bare_urls[idx - 1].byte_end
1183 }
1184
1185 pub fn inline_config(&self) -> &InlineConfig {
1187 &self.inline_config
1188 }
1189
1190 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1195 &self.colon_fence_details
1196 }
1197
1198 pub fn raw_lines(&self) -> &[&'a str] {
1202 &self.content_lines
1203 }
1204
1205 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1210 self.inline_config.is_rule_disabled(rule_name, line_number)
1211 }
1212
1213 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1215 Arc::clone(
1216 self.code_spans_cache
1217 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1218 )
1219 }
1220
1221 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1225 self.math_byte_ranges_cache
1226 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1227 }
1228
1229 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1231 Arc::clone(
1232 self.math_spans_cache
1233 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1234 )
1235 }
1236
1237 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1239 let math_spans = self.math_spans();
1240 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1242 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1243 }
1244
1245 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1247 &self.html_comment_ranges
1248 }
1249
1250 pub fn unterminated_html_comment(&self) -> Option<usize> {
1255 self.unterminated_html_comment
1256 }
1257
1258 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1262 self.unterminated_obsidian_comment
1263 }
1264
1265 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1269 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1270 }
1271
1272 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1277 if self.obsidian_comment_ranges.is_empty() {
1278 return false;
1279 }
1280
1281 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1283 self.is_in_obsidian_comment(byte_pos)
1284 }
1285
1286 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1288 &self.myst_directive_ranges
1289 }
1290
1291 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1293 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1294 }
1295
1296 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1298 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1299 }
1300
1301 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1308 if !self.flavor.supports_myst_directives() {
1309 return false;
1310 }
1311 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1312 info.in_myst_directive
1313 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1314 })
1315 }
1316
1317 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1319 tags.into_iter()
1320 .filter(|tag| {
1321 !self
1322 .lines
1323 .get(tag.line - 1)
1324 .is_some_and(|l| l.in_kramdown_extension_block)
1325 })
1326 .collect()
1327 }
1328
1329 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1335 Arc::clone(self.html_tags_cache.get_or_init(|| {
1336 let (html_tags, jsx_component_tags) =
1337 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1338 let _ = self
1340 .jsx_component_tags_cache
1341 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1342 Arc::new(self.filter_kramdown_tags(html_tags))
1343 }))
1344 }
1345
1346 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1349 if let Some(cached) = self.jsx_component_tags_cache.get() {
1350 return Arc::clone(cached);
1351 }
1352 let _ = self.html_tags();
1354 Arc::clone(
1355 self.jsx_component_tags_cache
1356 .get()
1357 .expect("html_tags() populates jsx_component_tags_cache"),
1358 )
1359 }
1360
1361 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1363 Arc::clone(
1364 self.emphasis_spans_cache
1365 .get()
1366 .expect("emphasis_spans_cache initialized during construction"),
1367 )
1368 }
1369
1370 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1372 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1373 Arc::new(element_parsers::parse_bare_urls(
1374 self.content,
1375 &self.lines,
1376 &self.code_blocks,
1377 ))
1378 }))
1379 }
1380
1381 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1383 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1384 Arc::new(element_parsers::detect_lazy_continuation_lines(
1385 self.content,
1386 &self.lines,
1387 &self.line_offsets,
1388 ))
1389 }))
1390 }
1391
1392 pub fn has_mixed_list_nesting(&self) -> bool {
1396 *self
1397 .has_mixed_list_nesting_cache
1398 .get_or_init(|| self.compute_mixed_list_nesting())
1399 }
1400
1401 fn compute_mixed_list_nesting(&self) -> bool {
1403 let mut stack: Vec<(usize, bool)> = Vec::new();
1408 let mut last_was_blank = false;
1409
1410 for line_info in &self.lines {
1411 if line_info.in_code_block
1413 || line_info.in_front_matter
1414 || line_info.in_mkdocstrings
1415 || line_info.in_html_comment
1416 || line_info.in_mdx_comment
1417 || line_info.in_esm_block
1418 {
1419 continue;
1420 }
1421
1422 if line_info.is_blank {
1424 last_was_blank = true;
1425 continue;
1426 }
1427
1428 if let Some(list_item) = &line_info.list_item {
1429 let current_pos = if list_item.marker_column == 1 {
1431 0
1432 } else {
1433 list_item.marker_column
1434 };
1435
1436 if last_was_blank && current_pos == 0 {
1438 stack.clear();
1439 }
1440 last_was_blank = false;
1441
1442 while let Some(&(pos, _)) = stack.last() {
1444 if pos >= current_pos {
1445 stack.pop();
1446 } else {
1447 break;
1448 }
1449 }
1450
1451 if let Some(&(_, parent_is_ordered)) = stack.last()
1453 && parent_is_ordered != list_item.is_ordered
1454 {
1455 return true; }
1457
1458 stack.push((current_pos, list_item.is_ordered));
1459 } else {
1460 last_was_blank = false;
1462 }
1463 }
1464
1465 false
1466 }
1467
1468 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1474 match self.line_offsets.binary_search(&offset) {
1475 Ok(line) => (line + 1, 1),
1476 Err(line) => {
1477 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1478 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1480 (line, col)
1481 }
1482 }
1483 }
1484
1485 pub fn line_start_byte(&self, line_number: usize) -> Option<usize> {
1491 self.line_index.get_line_start_byte(line_number)
1492 }
1493
1494 pub fn line_column_byte_range(&self, line_number: usize, column: usize) -> Range<usize> {
1500 self.line_index.line_col_to_byte_range(line_number, column)
1501 }
1502
1503 pub fn line_column_byte_range_with_length(&self, line_number: usize, column: usize, length: usize) -> Range<usize> {
1508 self.line_index
1509 .line_col_to_byte_range_with_length(line_number, column, length)
1510 }
1511
1512 pub fn whole_line_byte_range(&self, line_number: usize) -> Range<usize> {
1515 self.line_index.whole_line_range(line_number)
1516 }
1517
1518 pub fn line_text_byte_range(&self, line_number: usize, start_column: usize, end_column: usize) -> Range<usize> {
1523 self.line_index.line_text_range(line_number, start_column, end_column)
1524 }
1525
1526 pub fn line_content_byte_range(&self, line_number: usize) -> Range<usize> {
1529 self.line_index.line_content_range(line_number)
1530 }
1531
1532 pub fn line_span_byte_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
1534 self.line_index.multi_line_range(start_line, end_line)
1535 }
1536
1537 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1539 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1541 return true;
1542 }
1543
1544 self.is_byte_offset_in_code_span(pos)
1546 }
1547
1548 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1550 if line_num > 0 {
1551 self.lines.get(line_num - 1)
1552 } else {
1553 None
1554 }
1555 }
1556
1557 pub fn links(&self) -> &[ParsedLink<'a>] {
1559 &self.links
1560 }
1561
1562 pub fn images(&self) -> &[ParsedImage<'a>] {
1564 &self.images
1565 }
1566
1567 pub fn broken_links(&self) -> &[BrokenLinkInfo] {
1569 &self.broken_links
1570 }
1571
1572 pub fn footnote_references(&self) -> &[FootnoteRef] {
1574 &self.footnote_refs
1575 }
1576
1577 pub fn reference_definitions(&self) -> &[ReferenceDef] {
1579 &self.reference_defs
1580 }
1581
1582 pub fn links_on_line(&self, line_number: usize) -> &[ParsedLink<'a>] {
1584 let start = self.links.partition_point(|link| link.line < line_number);
1585 let end = self.links.partition_point(|link| link.line <= line_number);
1586 &self.links[start..end]
1587 }
1588
1589 pub fn images_on_line(&self, line_number: usize) -> &[ParsedImage<'a>] {
1591 let start = self.images.partition_point(|image| image.line < line_number);
1592 let end = self.images.partition_point(|image| image.line <= line_number);
1593 &self.images[start..end]
1594 }
1595
1596 pub fn link_starting_at(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1598 self.links
1599 .binary_search_by_key(&byte_offset, |link| link.byte_offset)
1600 .ok()
1601 .map(|index| &self.links[index])
1602 }
1603
1604 pub fn image_starting_at(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1606 self.images
1607 .binary_search_by_key(&byte_offset, |image| image.byte_offset)
1608 .ok()
1609 .map(|index| &self.images[index])
1610 }
1611
1612 pub fn link_containing(&self, byte_offset: usize) -> Option<&ParsedLink<'a>> {
1614 let index = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1615 self.links
1616 .get(index.checked_sub(1)?)
1617 .filter(|link| byte_offset < link.byte_end)
1618 }
1619
1620 pub fn image_containing(&self, byte_offset: usize) -> Option<&ParsedImage<'a>> {
1622 let index = self.images.partition_point(|image| image.byte_offset <= byte_offset);
1623 self.images
1624 .get(index.checked_sub(1)?)
1625 .filter(|image| byte_offset < image.byte_end)
1626 }
1627
1628 pub fn links_starting_before_or_at(&self, byte_offset: usize) -> &[ParsedLink<'a>] {
1630 let end = self.links.partition_point(|link| link.byte_offset <= byte_offset);
1631 &self.links[..end]
1632 }
1633
1634 pub fn reference_definition(&self, ref_id: &str) -> Option<&ReferenceDef> {
1636 let normalized_id = ref_id.to_lowercase();
1637 self.reference_defs_map
1638 .get(&normalized_id)
1639 .map(|&index| &self.reference_defs[index])
1640 }
1641
1642 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1644 self.reference_definition(ref_id)
1645 .map(|definition| definition.url.as_str())
1646 }
1647
1648 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1650 if line_num == 0 || line_num > self.lines.len() {
1651 return false;
1652 }
1653 self.lines[line_num - 1].in_list_block
1654 }
1655
1656 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1658 if line_num == 0 || line_num > self.lines.len() {
1659 return false;
1660 }
1661 self.lines[line_num - 1].in_html_block
1662 }
1663
1664 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1670 if line_num == 0 || line_num > self.lines.len() {
1671 return false;
1672 }
1673 self.lines[line_num - 1].in_table_block
1674 }
1675
1676 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1678 if line_num == 0 || line_num > self.lines.len() {
1679 return false;
1680 }
1681
1682 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1686 let code_spans = self.code_spans();
1687 code_spans.iter().any(|span| {
1688 if line_num < span.line || line_num > span.end_line {
1690 return false;
1691 }
1692
1693 if span.line == span.end_line {
1694 col_0indexed >= span.start_col && col_0indexed < span.end_col
1696 } else if line_num == span.line {
1697 col_0indexed >= span.start_col
1699 } else if line_num == span.end_line {
1700 col_0indexed < span.end_col
1702 } else {
1703 true
1705 }
1706 })
1707 }
1708
1709 #[inline]
1711 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1712 let code_spans = self.code_spans();
1713 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1714 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1715 }
1716
1717 #[inline]
1719 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1720 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1721 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1722 }
1723
1724 #[inline]
1726 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1727 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1728 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1729 }
1730
1731 #[inline]
1734 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1735 let tags = self.html_tags();
1736 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1737 idx > 0 && byte_pos < tags[idx - 1].byte_end
1738 }
1739
1740 #[inline]
1744 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1745 if !self.flavor.supports_jsx() {
1746 return false;
1747 }
1748 let tags = self.jsx_component_tags();
1749 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1750 idx > 0 && byte_pos < tags[idx - 1].byte_end
1751 }
1752
1753 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1755 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1756 }
1757
1758 #[inline]
1760 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1761 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1762 }
1763
1764 #[inline]
1766 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1767 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1768 }
1769
1770 #[inline]
1773 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1774 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1775 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1776 }
1777
1778 #[inline]
1780 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1781 &self.citation_ranges
1782 }
1783
1784 #[inline]
1787 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1788 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1789 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1790 }
1791
1792 #[inline]
1795 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1796 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1797 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1798 }
1799
1800 #[inline]
1803 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
1804 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
1805 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
1806 }
1807
1808 #[inline]
1811 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
1812 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
1813 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
1814 }
1815
1816 #[inline]
1819 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
1820 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
1821 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
1822 }
1823
1824 #[inline]
1828 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
1829 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
1830 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
1831 }
1832
1833 #[inline]
1836 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
1837 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
1838 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
1839 }
1840
1841 #[inline]
1844 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
1845 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
1846 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
1847 }
1848
1849 #[inline]
1853 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
1854 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
1855 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
1856 }
1857
1858 #[inline]
1861 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
1862 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
1863 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
1864 }
1865
1866 #[inline]
1869 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
1870 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
1871 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
1872 }
1873
1874 #[inline]
1877 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
1878 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
1879 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
1880 }
1881
1882 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
1887 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
1888 self.pandoc_header_slugs.contains(&slug)
1889 }
1890
1891 #[inline]
1897 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
1898 self.pandoc_header_slugs.contains(slug)
1899 }
1900
1901 #[inline]
1903 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
1904 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
1905 }
1906
1907 #[inline]
1909 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
1910 &self.shortcode_ranges
1911 }
1912
1913 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
1915 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
1916 }
1917
1918 pub fn has_char(&self, ch: char) -> bool {
1920 match ch {
1921 '#' => self.char_frequency.hash_count > 0,
1922 '*' => self.char_frequency.asterisk_count > 0,
1923 '_' => self.char_frequency.underscore_count > 0,
1924 '-' => self.char_frequency.hyphen_count > 0,
1925 '+' => self.char_frequency.plus_count > 0,
1926 '>' => self.char_frequency.gt_count > 0,
1927 '|' => self.char_frequency.pipe_count > 0,
1928 '[' => self.char_frequency.bracket_count > 0,
1929 '`' => self.char_frequency.backtick_count > 0,
1930 '<' => self.char_frequency.lt_count > 0,
1931 '!' => self.char_frequency.exclamation_count > 0,
1932 '\n' => self.char_frequency.newline_count > 0,
1933 _ => self.content.contains(ch), }
1935 }
1936
1937 pub fn char_count(&self, ch: char) -> usize {
1939 match ch {
1940 '#' => self.char_frequency.hash_count,
1941 '*' => self.char_frequency.asterisk_count,
1942 '_' => self.char_frequency.underscore_count,
1943 '-' => self.char_frequency.hyphen_count,
1944 '+' => self.char_frequency.plus_count,
1945 '>' => self.char_frequency.gt_count,
1946 '|' => self.char_frequency.pipe_count,
1947 '[' => self.char_frequency.bracket_count,
1948 '`' => self.char_frequency.backtick_count,
1949 '<' => self.char_frequency.lt_count,
1950 '!' => self.char_frequency.exclamation_count,
1951 '\n' => self.char_frequency.newline_count,
1952 _ => self.content.matches(ch).count(), }
1954 }
1955
1956 pub fn likely_has_headings(&self) -> bool {
1958 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 2 || self.content.contains('=') }
1960
1961 pub fn likely_has_lists(&self) -> bool {
1963 self.char_frequency.asterisk_count > 0
1964 || self.char_frequency.hyphen_count > 0
1965 || self.char_frequency.plus_count > 0
1966 }
1967
1968 pub fn likely_has_emphasis(&self) -> bool {
1970 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
1971 }
1972
1973 pub fn likely_has_tables(&self) -> bool {
1975 self.char_frequency.pipe_count > 2
1976 }
1977
1978 pub fn likely_has_blockquotes(&self) -> bool {
1980 self.char_frequency.gt_count > 0
1981 }
1982
1983 pub fn likely_has_code(&self) -> bool {
1985 self.char_frequency.backtick_count > 0
1986 }
1987
1988 pub fn likely_has_links_or_images(&self) -> bool {
1990 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
1991 }
1992
1993 pub fn likely_has_html(&self) -> bool {
1995 self.char_frequency.lt_count > 0
1996 }
1997
1998 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
2003 if let Some(line_info) = self.lines.get(line_idx)
2004 && let Some(ref bq) = line_info.blockquote
2005 {
2006 bq.prefix.trim_end().to_string()
2007 } else {
2008 String::new()
2009 }
2010 }
2011
2012 #[inline]
2023 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
2024 let idx = match lines.binary_search_by(|line| {
2026 if byte_offset < line.byte_offset {
2027 std::cmp::Ordering::Greater
2028 } else if byte_offset > line.byte_offset + line.byte_len {
2029 std::cmp::Ordering::Less
2030 } else {
2031 std::cmp::Ordering::Equal
2032 }
2033 }) {
2034 Ok(idx) => idx,
2035 Err(idx) => idx.saturating_sub(1),
2036 };
2037
2038 let line = &lines[idx];
2039 let line_num = idx + 1;
2040 let byte_col = byte_offset.saturating_sub(line.byte_offset);
2041 let col = byte_to_char_count(line.content(content), byte_col) - 1;
2044
2045 (idx, line_num, col)
2046 }
2047
2048 #[inline]
2050 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
2051 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
2053
2054 if idx > 0 {
2056 let span = &code_spans[idx - 1];
2057 if offset >= span.byte_offset && offset < span.byte_end {
2058 return true;
2059 }
2060 }
2061
2062 false
2063 }
2064
2065 #[must_use]
2085 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
2086 ValidHeadingsIter::new(&self.lines)
2087 }
2088
2089 #[must_use]
2093 pub fn has_valid_headings(&self) -> bool {
2094 self.lines
2095 .iter()
2096 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
2097 }
2098
2099 #[must_use]
2101 pub fn list_items(&self) -> ParsedListItemsIter<'_> {
2102 ParsedListItemsIter::new(&self.lines)
2103 }
2104
2105 #[must_use]
2107 pub fn list_item_on_line(&self, line_num: usize) -> Option<ParsedListItem<'_>> {
2108 let line_info = self.lines.get(line_num.checked_sub(1)?)?;
2109 Some(ParsedListItem::new(
2110 line_num,
2111 line_info.list_item.as_deref()?,
2112 line_info,
2113 ))
2114 }
2115
2116 #[must_use]
2118 pub fn parsed_list_blocks(&self) -> ParsedListBlocks<'_> {
2119 ParsedListBlocks::new(&self.list_blocks, &self.lines)
2120 }
2121
2122 #[must_use]
2126 pub fn list_block_item_groups(&self, block: &ListBlock) -> Vec<Vec<usize>> {
2127 list_blocks::item_lines_by_list(self.content, &self.lines, block)
2128 }
2129
2130 #[must_use]
2132 pub fn has_list_items(&self) -> bool {
2133 self.lines.iter().any(|line| line.list_item.is_some())
2134 }
2135
2136 #[must_use]
2138 pub fn has_unordered_list_items(&self) -> bool {
2139 self.lines
2140 .iter()
2141 .any(|line| line.list_item.as_ref().is_some_and(|item| !item.is_ordered))
2142 }
2143
2144 #[must_use]
2146 pub fn commonmark_ordered_lists(&self) -> CommonMarkOrderedLists<'_> {
2147 let lists = self
2148 .commonmark_ordered_lists_cache
2149 .get_or_init(|| build_commonmark_ordered_lists(&self.lines, &self.line_to_list, &self.list_start_values));
2150 CommonMarkOrderedLists::new(lists, &self.lines)
2151 }
2152
2153 #[must_use]
2161 pub fn headings(&self) -> ParsedHeadingsIter<'_> {
2162 ParsedHeadingsIter::new(&self.lines, &self.blockquote_headings)
2163 }
2164
2165 #[must_use]
2167 pub fn heading_on_line(&self, line_num: usize) -> Option<ParsedHeading<'_>> {
2168 let idx = line_num.checked_sub(1)?;
2169 let line_info = self.lines.get(idx)?;
2170 let (heading, blockquote_depth) = match line_info.heading.as_deref() {
2171 Some(heading) => (heading, 0),
2172 None => (
2173 self.blockquote_headings.get(idx)?.as_deref()?,
2174 line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level),
2175 ),
2176 };
2177 Some(ParsedHeading {
2178 line_num,
2179 heading,
2180 line_info,
2181 blockquote_depth,
2182 })
2183 }
2184}
2185
2186fn container_comment_range(
2198 opener: usize,
2199 containers: &flavor_detection::ContainerLines,
2200 lines: &[types::LineInfo],
2201 content: &str,
2202) -> Option<crate::utils::skip_context::ByteRange> {
2203 let line_index = lines
2204 .partition_point(|line| line.byte_offset <= opener)
2205 .checked_sub(1)?;
2206 let line = lines.get(line_index)?;
2207 if line.byte_offset + line.indent != opener {
2208 return None;
2209 }
2210 if !containers.is_container_body(line_index) {
2211 return None;
2212 }
2213 let end_line = lines.get(containers.body_end_line(line_index)?)?;
2214 Some(crate::utils::skip_context::ByteRange {
2215 start: opener,
2216 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
2217 })
2218}
2219
2220fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
2229 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
2230
2231 let options = crate::utils::rumdl_parser_options();
2232 let parser = Parser::new_ext(content, options).into_offset_iter();
2233
2234 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
2236 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
2237 let mut in_footnote = false;
2238
2239 for (event, range) in parser {
2240 match event {
2241 Event::Start(Tag::FootnoteDefinition(_)) => {
2242 in_footnote = true;
2243 footnote_ranges.push((range.start, range.end));
2244 }
2245 Event::End(TagEnd::FootnoteDefinition) => {
2246 in_footnote = false;
2247 }
2248 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
2249 fenced_code_ranges.push((range.start, range.end));
2250 }
2251 _ => {}
2252 }
2253 }
2254
2255 let byte_to_line = |byte_offset: usize| -> usize {
2256 line_offsets
2257 .partition_point(|&offset| offset <= byte_offset)
2258 .saturating_sub(1)
2259 };
2260
2261 for &(start, end) in &footnote_ranges {
2263 let start_line = byte_to_line(start);
2264 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2265
2266 for line in &mut lines[start_line..end_line] {
2267 line.in_footnote_definition = true;
2268 line.in_code_block = false;
2269 }
2270 }
2271
2272 for &(start, end) in &fenced_code_ranges {
2274 let start_line = byte_to_line(start);
2275 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2276
2277 for line in &mut lines[start_line..end_line] {
2278 line.in_code_block = true;
2279 }
2280 }
2281}