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::path::PathBuf;
20
21#[cfg(not(target_arch = "wasm32"))]
23macro_rules! profile_section {
24 ($name:expr, $profile:expr, $code:expr) => {{
25 let start = std::time::Instant::now();
26 let result = $code;
27 if $profile {
28 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
29 }
30 result
31 }};
32}
33
34#[cfg(target_arch = "wasm32")]
35macro_rules! profile_section {
36 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
37}
38
39pub(super) struct SkipByteRanges<'a> {
42 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
43 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
44 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
45 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
46}
47
48use std::sync::{Arc, OnceLock};
49
50pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
52
53pub(super) type ByteRanges = Vec<(usize, usize)>;
55
56pub struct LintContext<'a> {
57 pub content: &'a str,
58 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
60 pub code_blocks: Vec<(usize, usize)>, pub code_block_details: Vec<CodeBlockDetail>, pub strong_spans: Vec<crate::utils::code_block_utils::StrongSpanDetail>, pub line_to_list: crate::utils::code_block_utils::LineToListMap, pub list_start_values: crate::utils::code_block_utils::ListStartValues, pub lines: Vec<LineInfo>, pub links: Vec<ParsedLink<'a>>, pub images: Vec<ParsedImage<'a>>, pub broken_links: Vec<BrokenLinkInfo>, pub footnote_refs: Vec<FootnoteRef>, pub 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>, pub line_index: crate::utils::range_utils::LineIndex<'a>, jinja_ranges: Vec<(usize, usize)>, pub flavor: MarkdownFlavor, pub 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_ranges: Vec<(usize, usize)>, 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, }
118
119impl<'a> LintContext<'a> {
120 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
121 #[cfg(not(target_arch = "wasm32"))]
122 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
123
124 let line_offsets = profile_section!("Line offsets", profile, {
125 let mut offsets = vec![0];
126 for (i, c) in content.char_indices() {
127 if c == '\n' {
128 offsets.push(i + 1);
129 }
130 }
131 offsets
132 });
133
134 let content_lines: Vec<&str> = content.lines().collect();
136
137 #[allow(clippy::disallowed_methods)]
141 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
142
143 let parse_result = profile_section!(
145 "Code blocks",
146 profile,
147 CodeBlockUtils::detect_code_blocks_and_spans(content)
148 );
149 let mut code_blocks = parse_result.code_blocks;
150 let code_span_ranges = parse_result.code_spans;
151 let code_block_details = parse_result.code_block_details;
152 let strong_spans = parse_result.strong_spans;
153 let line_to_list = parse_result.line_to_list;
154 let list_start_values = parse_result.list_start_values;
155 let html_blocks = parse_result.html_blocks;
156
157 let containers = profile_section!(
160 "Container lines",
161 profile,
162 flavor_detection::detect_container_lines(&content_lines, flavor)
163 );
164
165 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
174 .iter()
175 .flat_map(|detail| {
176 if detail.is_fenced {
177 return vec![(detail.start, detail.end)];
178 }
179 let start_line = line_offsets
180 .partition_point(|&offset| offset <= detail.start)
181 .saturating_sub(1);
182 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
183 containers
184 .code_line_spans_in(start_line..end_line)
185 .into_iter()
186 .map(|span| {
187 let start = line_offsets[span.start].max(detail.start);
188 let end = line_offsets
189 .get(span.end)
190 .copied()
191 .unwrap_or(content.len())
192 .min(detail.end);
193 (start, end)
194 })
195 .collect()
196 })
197 .collect();
198 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
204 let html_comment_scan = profile_section!(
205 "HTML comment ranges",
206 profile,
207 crate::utils::skip_context::scan_html_comments(
208 content,
209 &code_span_ranges,
210 &comment_code_block_ranges,
211 body_start
212 )
213 );
214 let mut html_comment_ranges = html_comment_scan.ranges;
215 let unterminated_html_comment = html_comment_scan.unterminated;
216
217 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
221 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
222 Vec::new()
223 } else {
224 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
225 }
226 });
227
228 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
230 if flavor.is_pandoc_compatible() {
231 crate::utils::pandoc::detect_div_block_ranges(content)
232 } else {
233 Vec::new()
234 }
235 });
236
237 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
239 if flavor == MarkdownFlavor::MkDocs {
240 crate::utils::pymdown_blocks::detect_block_ranges(content)
241 } else {
242 Vec::new()
243 }
244 });
245
246 let skip_ranges = SkipByteRanges {
249 html_comment_ranges: &html_comment_ranges,
250 autodoc_ranges: &autodoc_ranges,
251 pandoc_div_ranges: &pandoc_div_ranges,
252 pymdown_block_ranges: &pymdown_block_ranges,
253 };
254 let (mut lines, emphasis_spans) = profile_section!(
255 "Basic line info",
256 profile,
257 line_computation::compute_basic_line_info(
258 content,
259 &content_lines,
260 &line_offsets,
261 &code_blocks,
262 flavor,
263 &skip_ranges,
264 front_matter_end,
265 )
266 );
267
268 profile_section!(
270 "HTML blocks",
271 profile,
272 heading_detection::detect_html_blocks(content, &mut lines)
273 );
274
275 profile_section!(
277 "ESM blocks",
278 profile,
279 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
280 );
281
282 profile_section!(
284 "JSX block detection",
285 profile,
286 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
287 );
288
289 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
291 "JSX/MDX detection",
292 profile,
293 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
294 );
295
296 profile_section!(
301 "Markdown-in-HTML blocks",
302 profile,
303 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
304 );
305
306 profile_section!(
308 "MkDocs constructs",
309 profile,
310 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
311 );
312
313 profile_section!(
318 "Footnote definitions",
319 profile,
320 detect_footnote_definitions(content, &mut lines, &line_offsets)
321 );
322
323 {
326 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
327 for &(start, end) in &code_blocks {
328 let start_line = line_offsets
329 .partition_point(|&offset| offset <= start)
330 .saturating_sub(1);
331 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
332
333 let mut sub_start: Option<usize> = None;
334 for (i, &offset) in line_offsets[start_line..end_line]
335 .iter()
336 .enumerate()
337 .map(|(j, o)| (j + start_line, o))
338 {
339 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
340 if is_real_code && sub_start.is_none() {
341 let byte_start = if i == start_line { start } else { offset };
342 sub_start = Some(byte_start);
343 } else if !is_real_code && sub_start.is_some() {
344 new_code_blocks.push((sub_start.unwrap(), offset));
345 sub_start = None;
346 }
347 }
348 if let Some(s) = sub_start {
349 new_code_blocks.push((s, end));
350 }
351 }
352 code_blocks = new_code_blocks;
353 }
354
355 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
363 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
364 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
365 for &(start, end) in &code_blocks {
366 let start_line = line_offsets
367 .partition_point(|&offset| offset <= start)
368 .saturating_sub(1);
369 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
370
371 let mut sub_start: Option<usize> = None;
373 for (i, &offset) in line_offsets[start_line..end_line]
374 .iter()
375 .enumerate()
376 .map(|(j, o)| (j + start_line, o))
377 {
378 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
379 if is_real_code && sub_start.is_none() {
380 let byte_start = if i == start_line { start } else { offset };
381 sub_start = Some(byte_start);
382 } else if !is_real_code && sub_start.is_some() {
383 new_code_blocks.push((sub_start.unwrap(), offset));
384 sub_start = None;
385 }
386 }
387 if let Some(s) = sub_start {
388 new_code_blocks.push((s, end));
389 }
390 }
391 code_blocks = new_code_blocks;
392 }
393
394 if flavor.supports_jsx() {
398 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
399 for &(start, end) in &code_blocks {
400 let start_line = line_offsets
401 .partition_point(|&offset| offset <= start)
402 .saturating_sub(1);
403 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
404
405 let mut sub_start: Option<usize> = None;
406 for (i, &offset) in line_offsets[start_line..end_line]
407 .iter()
408 .enumerate()
409 .map(|(j, o)| (j + start_line, o))
410 {
411 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
412 if is_real_code && sub_start.is_none() {
413 let byte_start = if i == start_line { start } else { offset };
414 sub_start = Some(byte_start);
415 } else if !is_real_code && sub_start.is_some() {
416 new_code_blocks.push((sub_start.unwrap(), offset));
417 sub_start = None;
418 }
419 }
420 if let Some(s) = sub_start {
421 new_code_blocks.push((s, end));
422 }
423 }
424 code_blocks = new_code_blocks;
425
426 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
433 let mut run: Option<(usize, usize)> = None;
434 for line in &lines {
435 if line.in_jsx_block && line.in_code_block {
436 let line_end = line.byte_offset + line.byte_len;
437 match &mut run {
438 Some((_, end)) => *end = line_end,
439 None => run = Some((line.byte_offset, line_end)),
440 }
441 } else if let Some(r) = run.take() {
442 jsx_fence_ranges.push(r);
443 }
444 }
445 if let Some(r) = run.take() {
446 jsx_fence_ranges.push(r);
447 }
448 if !jsx_fence_ranges.is_empty() {
449 code_blocks.extend(jsx_fence_ranges);
450 code_blocks.sort_by_key(|&(start, _)| start);
451 }
452 }
453
454 let colon_fence_ranges = profile_section!(
457 "Azure colon fence detection",
458 profile,
459 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
460 );
461 if !colon_fence_ranges.is_empty() {
462 code_blocks.extend(colon_fence_ranges.iter().copied());
463 code_blocks.sort_by_key(|&(start, _)| start);
464 }
465
466 let myst_directive_ranges = profile_section!(
469 "MyST colon directives",
470 profile,
471 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
472 );
473
474 let myst_comment_ranges = profile_section!(
476 "MyST comments",
477 profile,
478 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
479 );
480
481 profile_section!(
484 "MyST backtick directives",
485 profile,
486 flavor_detection::detect_myst_backtick_directives(
487 content,
488 &mut lines,
489 flavor,
490 &code_block_details,
491 &line_offsets
492 )
493 );
494
495 if flavor.supports_myst_directives() {
498 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
499 for &(start, end) in &code_blocks {
500 let start_line = line_offsets
501 .partition_point(|&offset| offset <= start)
502 .saturating_sub(1);
503 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
504
505 let mut sub_start: Option<usize> = None;
506 for (i, &offset) in line_offsets[start_line..end_line]
507 .iter()
508 .enumerate()
509 .map(|(j, o)| (j + start_line, o))
510 {
511 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
512 if is_real_code && sub_start.is_none() {
513 let byte_start = if i == start_line { start } else { offset };
514 sub_start = Some(byte_start);
515 } else if !is_real_code && sub_start.is_some() {
516 new_code_blocks.push((sub_start.unwrap(), offset));
517 sub_start = None;
518 }
519 }
520 if let Some(s) = sub_start {
521 new_code_blocks.push((s, end));
522 }
523 }
524 code_blocks = new_code_blocks;
525 }
526
527 profile_section!(
529 "Kramdown constructs",
530 profile,
531 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
532 );
533
534 for line in &mut lines {
539 if line.in_kramdown_extension_block {
540 line.list_item = None;
541 line.is_horizontal_rule = false;
542 line.blockquote = None;
543 line.is_kramdown_block_ial = false;
544 }
545 }
546
547 let obsidian_comment_scan = profile_section!(
549 "Obsidian comments",
550 profile,
551 flavor_detection::detect_obsidian_comments(
552 content,
553 &mut lines,
554 flavor,
555 &code_span_ranges,
556 &html_comment_ranges,
557 body_start
558 )
559 );
560 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
561 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
562
563 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
568 unterminated_html_comment,
569 &obsidian_comment_ranges,
570 content,
571 &code_span_ranges,
572 &comment_code_block_ranges,
573 body_start,
574 );
575
576 if let Some(range) = unterminated_html_comment.and_then(|opener| {
589 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
590 .or_else(|| container_comment_range(opener, &containers, &lines, content))
591 }) {
592 html_comment_ranges.push(range);
595
596 for line in &mut lines {
602 let text = line.content(content);
603 let content_start = line.byte_offset + line.indent;
604 let content_end = line.byte_offset + text.trim_end().len();
605 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
606 &html_comment_ranges,
607 content_start,
608 content_end,
609 );
610 line.in_obsidian_comment = false;
611 }
612
613 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
624 content,
625 &mut lines,
626 flavor,
627 &code_span_ranges,
628 &html_comment_ranges,
629 body_start,
630 );
631 obsidian_comment_ranges = obsidian_rescan.ranges;
632 unterminated_obsidian_comment = obsidian_rescan.unterminated;
633 }
634
635 let myst_role_ranges = profile_section!(
637 "MyST roles",
638 profile,
639 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
640 );
641
642 let pulldown_result = profile_section!(
646 "Links, images & link ranges",
647 profile,
648 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
649 );
650
651 profile_section!(
653 "Headings & blockquotes",
654 profile,
655 heading_detection::detect_headings_and_blockquotes(
656 &content_lines,
657 &mut lines,
658 flavor,
659 &html_comment_ranges,
660 &pulldown_result.link_byte_ranges,
661 front_matter_end,
662 )
663 );
664
665 for line in &mut lines {
667 if line.in_kramdown_extension_block {
668 line.heading = None;
669 }
670 }
671
672 let mut code_spans = profile_section!(
674 "Code spans",
675 profile,
676 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
677 );
678
679 if flavor == MarkdownFlavor::MkDocs {
683 let extra = profile_section!(
684 "MkDocs code spans",
685 profile,
686 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
687 );
688 if !extra.is_empty() {
689 code_spans.extend(extra);
690 code_spans.sort_by_key(|span| span.byte_offset);
691 }
692 }
693
694 if flavor == MarkdownFlavor::MDX {
699 let extra = profile_section!(
700 "MDX JSX code spans",
701 profile,
702 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
703 );
704 if !extra.is_empty() {
705 code_spans.extend(extra);
706 code_spans.sort_by_key(|span| span.byte_offset);
707 }
708 }
709
710 for span in &code_spans {
713 if span.end_line > span.line {
714 for line_num in (span.line + 1)..=span.end_line {
716 if let Some(line_info) = lines.get_mut(line_num - 1) {
717 line_info.in_code_span_continuation = true;
718 }
719 }
720 }
721 }
722
723 let (links, images, broken_links, footnote_refs) = profile_section!(
725 "Links & images finalize",
726 profile,
727 link_parser::finalize_links_and_images(
728 content,
729 &lines,
730 &code_blocks,
731 &code_spans,
732 flavor,
733 &html_comment_ranges,
734 pulldown_result
735 )
736 );
737
738 let reference_defs = profile_section!(
739 "Reference defs",
740 profile,
741 link_parser::parse_reference_defs(content, &lines)
742 );
743
744 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
745
746 let char_frequency = profile_section!(
748 "Char frequency",
749 profile,
750 line_computation::compute_char_frequency(content)
751 );
752
753 let table_blocks = profile_section!(
755 "Table blocks",
756 profile,
757 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
758 content,
759 &code_blocks,
760 &code_spans,
761 &html_comment_ranges,
762 )
763 );
764
765 let links = links
768 .into_iter()
769 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
770 .collect::<Vec<_>>();
771 let images = images
772 .into_iter()
773 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
774 .collect::<Vec<_>>();
775 let broken_links = broken_links
776 .into_iter()
777 .filter(|bl| {
778 let line_idx = line_offsets
780 .partition_point(|&offset| offset <= bl.span.start)
781 .saturating_sub(1);
782 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
783 })
784 .collect::<Vec<_>>();
785 let footnote_refs = footnote_refs
786 .into_iter()
787 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
788 .collect::<Vec<_>>();
789 let reference_defs = reference_defs
790 .into_iter()
791 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
792 .collect::<Vec<_>>();
793 let list_blocks = list_blocks
794 .into_iter()
795 .filter(|block| {
796 !lines
797 .get(block.start_line - 1)
798 .is_some_and(|l| l.in_kramdown_extension_block)
799 })
800 .collect::<Vec<_>>();
801 let table_blocks = table_blocks
802 .into_iter()
803 .filter(|block| {
804 !lines
806 .get(block.start_line)
807 .is_some_and(|l| l.in_kramdown_extension_block)
808 })
809 .collect::<Vec<_>>();
810 let emphasis_spans = emphasis_spans
811 .into_iter()
812 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
813 .collect::<Vec<_>>();
814
815 for block in &list_blocks {
819 for line_num in block.start_line..=block.end_line {
821 if let Some(li) = lines.get_mut(line_num - 1) {
822 li.in_list_block = true;
823 }
824 }
825 }
826 for block in &table_blocks {
827 for idx in block.start_line..=block.end_line {
829 if let Some(li) = lines.get_mut(idx) {
830 li.in_table_block = true;
831 }
832 }
833 }
834
835 let reference_defs_map: HashMap<String, usize> = reference_defs
837 .iter()
838 .enumerate()
839 .map(|(idx, def)| (def.id.to_lowercase(), idx))
840 .collect();
841
842 let link_title_ranges: Vec<(usize, usize)> = reference_defs
844 .iter()
845 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
846 (Some(start), Some(end)) => Some((start, end)),
847 _ => None,
848 })
849 .collect();
850
851 let line_index = profile_section!(
853 "Line index",
854 profile,
855 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
856 content,
857 line_offsets.clone(),
858 &code_blocks,
859 )
860 );
861
862 let jinja_ranges = profile_section!(
864 "Jinja ranges",
865 profile,
866 crate::utils::jinja_utils::find_jinja_ranges(content)
867 );
868
869 let citation_ranges = profile_section!("Citation ranges", profile, {
871 if flavor.is_pandoc_compatible() {
872 crate::utils::pandoc::find_citation_ranges(content)
873 } else {
874 Vec::new()
875 }
876 });
877
878 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
880 if flavor.is_pandoc_compatible() {
881 crate::utils::pandoc::detect_inline_footnote_ranges(content)
882 } else {
883 Vec::new()
884 }
885 });
886
887 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
889 if flavor.is_pandoc_compatible() {
890 crate::utils::pandoc::collect_pandoc_header_slugs(content)
891 } else {
892 std::collections::HashSet::new()
893 }
894 });
895
896 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
898 if flavor.is_pandoc_compatible() {
899 crate::utils::pandoc::detect_example_list_marker_ranges(content)
900 } else {
901 Vec::new()
902 }
903 });
904
905 let example_reference_ranges = profile_section!("Example references", profile, {
907 if flavor.is_pandoc_compatible() {
908 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
909 } else {
910 Vec::new()
911 }
912 });
913
914 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
916 if flavor.is_pandoc_compatible() {
917 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
918 } else {
919 Vec::new()
920 }
921 });
922
923 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
925 if flavor.is_pandoc_compatible() {
926 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
927 } else {
928 Vec::new()
929 }
930 });
931
932 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
934 if flavor.is_pandoc_compatible() {
935 crate::utils::pandoc::detect_bracketed_span_ranges(content)
936 } else {
937 Vec::new()
938 }
939 });
940
941 let line_block_ranges = profile_section!("Line block ranges", profile, {
943 if flavor.is_pandoc_compatible() {
944 crate::utils::pandoc::detect_line_block_ranges(content)
945 } else {
946 Vec::new()
947 }
948 });
949
950 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
952 if flavor.is_pandoc_compatible() {
953 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
954 } else {
955 Vec::new()
956 }
957 });
958
959 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
961 if flavor.is_pandoc_compatible() {
962 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
963 } else {
964 Vec::new()
965 }
966 });
967
968 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
970 if flavor.is_pandoc_compatible() {
971 crate::utils::pandoc::detect_grid_table_ranges(content)
972 } else {
973 Vec::new()
974 }
975 });
976
977 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
979 if flavor.is_pandoc_compatible() {
980 crate::utils::pandoc::detect_multi_line_table_ranges(content)
981 } else {
982 Vec::new()
983 }
984 });
985
986 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
988 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
989 let mut ranges = Vec::new();
990 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
991 ranges.push((mat.start(), mat.end()));
992 }
993 ranges
994 });
995
996 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks);
997
998 Self {
999 content,
1000 content_lines,
1001 line_offsets,
1002 code_blocks,
1003 code_block_details,
1004 strong_spans,
1005 line_to_list,
1006 list_start_values,
1007 lines,
1008 links,
1009 images,
1010 broken_links,
1011 footnote_refs,
1012 reference_defs,
1013 reference_defs_map,
1014 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1015 math_spans_cache: OnceLock::new(), math_byte_ranges_cache: OnceLock::new(), list_blocks,
1018 char_frequency,
1019 html_tags_cache: OnceLock::new(),
1020 jsx_component_tags_cache: OnceLock::new(),
1021 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1022 bare_urls_cache: OnceLock::new(),
1023 has_mixed_list_nesting_cache: OnceLock::new(),
1024 html_comment_ranges,
1025 table_blocks,
1026 line_index,
1027 jinja_ranges,
1028 flavor,
1029 source_file,
1030 jsx_expression_ranges,
1031 mdx_comment_ranges,
1032 citation_ranges,
1033 pandoc_div_ranges,
1034 colon_fence_ranges,
1035 inline_footnote_ranges,
1036 pandoc_header_slugs,
1037 example_list_marker_ranges,
1038 example_reference_ranges,
1039 sub_super_ranges,
1040 inline_code_attr_ranges,
1041 bracketed_span_ranges,
1042 line_block_ranges,
1043 pipe_table_caption_ranges,
1044 pandoc_metadata_ranges,
1045 grid_table_ranges,
1046 multi_line_table_ranges,
1047 shortcode_ranges,
1048 link_title_ranges,
1049 code_span_byte_ranges: code_span_ranges,
1050 inline_config,
1051 obsidian_comment_ranges,
1052 unterminated_html_comment,
1053 unterminated_obsidian_comment,
1054 lazy_cont_lines_cache: OnceLock::new(),
1055 myst_directive_ranges,
1056 myst_comment_ranges,
1057 myst_role_ranges,
1058 front_matter_end,
1059 }
1060 }
1061
1062 pub fn front_matter_end_line(&self) -> usize {
1067 self.front_matter_end
1068 }
1069
1070 #[inline]
1073 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1074 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1076 idx > 0 && pos < ranges[idx - 1].1
1078 }
1079
1080 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1082 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1083 }
1084
1085 pub fn is_in_link(&self, pos: usize) -> bool {
1087 let idx = self.links.partition_point(|link| link.byte_offset <= pos);
1088 if idx > 0 && pos < self.links[idx - 1].byte_end {
1089 return true;
1090 }
1091 let idx = self.images.partition_point(|img| img.byte_offset <= pos);
1092 if idx > 0 && pos < self.images[idx - 1].byte_end {
1093 return true;
1094 }
1095 self.is_in_reference_def(pos)
1096 }
1097
1098 pub fn inline_config(&self) -> &InlineConfig {
1100 &self.inline_config
1101 }
1102
1103 pub fn colon_fence_ranges(&self) -> &[(usize, usize)] {
1106 &self.colon_fence_ranges
1107 }
1108
1109 pub fn raw_lines(&self) -> &[&'a str] {
1113 &self.content_lines
1114 }
1115
1116 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1121 self.inline_config.is_rule_disabled(rule_name, line_number)
1122 }
1123
1124 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1126 Arc::clone(
1127 self.code_spans_cache
1128 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1129 )
1130 }
1131
1132 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1136 self.math_byte_ranges_cache
1137 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1138 }
1139
1140 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1142 Arc::clone(
1143 self.math_spans_cache
1144 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1145 )
1146 }
1147
1148 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1150 let math_spans = self.math_spans();
1151 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1153 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1154 }
1155
1156 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1158 &self.html_comment_ranges
1159 }
1160
1161 pub fn unterminated_html_comment(&self) -> Option<usize> {
1166 self.unterminated_html_comment
1167 }
1168
1169 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1173 self.unterminated_obsidian_comment
1174 }
1175
1176 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1180 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1181 }
1182
1183 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1188 if self.obsidian_comment_ranges.is_empty() {
1189 return false;
1190 }
1191
1192 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1194 self.is_in_obsidian_comment(byte_pos)
1195 }
1196
1197 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1199 &self.myst_directive_ranges
1200 }
1201
1202 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1204 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1205 }
1206
1207 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1209 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1210 }
1211
1212 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1219 if !self.flavor.supports_myst_directives() {
1220 return false;
1221 }
1222 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1223 info.in_myst_directive
1224 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1225 })
1226 }
1227
1228 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1230 tags.into_iter()
1231 .filter(|tag| {
1232 !self
1233 .lines
1234 .get(tag.line - 1)
1235 .is_some_and(|l| l.in_kramdown_extension_block)
1236 })
1237 .collect()
1238 }
1239
1240 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1246 Arc::clone(self.html_tags_cache.get_or_init(|| {
1247 let (html_tags, jsx_component_tags) =
1248 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1249 let _ = self
1251 .jsx_component_tags_cache
1252 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1253 Arc::new(self.filter_kramdown_tags(html_tags))
1254 }))
1255 }
1256
1257 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1260 if let Some(cached) = self.jsx_component_tags_cache.get() {
1261 return Arc::clone(cached);
1262 }
1263 let _ = self.html_tags();
1265 Arc::clone(
1266 self.jsx_component_tags_cache
1267 .get()
1268 .expect("html_tags() populates jsx_component_tags_cache"),
1269 )
1270 }
1271
1272 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1274 Arc::clone(
1275 self.emphasis_spans_cache
1276 .get()
1277 .expect("emphasis_spans_cache initialized during construction"),
1278 )
1279 }
1280
1281 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1283 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1284 Arc::new(element_parsers::parse_bare_urls(
1285 self.content,
1286 &self.lines,
1287 &self.code_blocks,
1288 ))
1289 }))
1290 }
1291
1292 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1294 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1295 Arc::new(element_parsers::detect_lazy_continuation_lines(
1296 self.content,
1297 &self.lines,
1298 &self.line_offsets,
1299 ))
1300 }))
1301 }
1302
1303 pub fn has_mixed_list_nesting(&self) -> bool {
1307 *self
1308 .has_mixed_list_nesting_cache
1309 .get_or_init(|| self.compute_mixed_list_nesting())
1310 }
1311
1312 fn compute_mixed_list_nesting(&self) -> bool {
1314 let mut stack: Vec<(usize, bool)> = Vec::new();
1319 let mut last_was_blank = false;
1320
1321 for line_info in &self.lines {
1322 if line_info.in_code_block
1324 || line_info.in_front_matter
1325 || line_info.in_mkdocstrings
1326 || line_info.in_html_comment
1327 || line_info.in_mdx_comment
1328 || line_info.in_esm_block
1329 {
1330 continue;
1331 }
1332
1333 if line_info.is_blank {
1335 last_was_blank = true;
1336 continue;
1337 }
1338
1339 if let Some(list_item) = &line_info.list_item {
1340 let current_pos = if list_item.marker_column == 1 {
1342 0
1343 } else {
1344 list_item.marker_column
1345 };
1346
1347 if last_was_blank && current_pos == 0 {
1349 stack.clear();
1350 }
1351 last_was_blank = false;
1352
1353 while let Some(&(pos, _)) = stack.last() {
1355 if pos >= current_pos {
1356 stack.pop();
1357 } else {
1358 break;
1359 }
1360 }
1361
1362 if let Some(&(_, parent_is_ordered)) = stack.last()
1364 && parent_is_ordered != list_item.is_ordered
1365 {
1366 return true; }
1368
1369 stack.push((current_pos, list_item.is_ordered));
1370 } else {
1371 last_was_blank = false;
1373 }
1374 }
1375
1376 false
1377 }
1378
1379 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1385 match self.line_offsets.binary_search(&offset) {
1386 Ok(line) => (line + 1, 1),
1387 Err(line) => {
1388 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1389 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1391 (line, col)
1392 }
1393 }
1394 }
1395
1396 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1398 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1400 return true;
1401 }
1402
1403 self.is_byte_offset_in_code_span(pos)
1405 }
1406
1407 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1409 if line_num > 0 {
1410 self.lines.get(line_num - 1)
1411 } else {
1412 None
1413 }
1414 }
1415
1416 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1418 let normalized_id = ref_id.to_lowercase();
1419 self.reference_defs_map
1420 .get(&normalized_id)
1421 .map(|&idx| self.reference_defs[idx].url.as_str())
1422 }
1423
1424 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1426 if line_num == 0 || line_num > self.lines.len() {
1427 return false;
1428 }
1429 self.lines[line_num - 1].in_list_block
1430 }
1431
1432 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1434 if line_num == 0 || line_num > self.lines.len() {
1435 return false;
1436 }
1437 self.lines[line_num - 1].in_html_block
1438 }
1439
1440 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1446 if line_num == 0 || line_num > self.lines.len() {
1447 return false;
1448 }
1449 self.lines[line_num - 1].in_table_block
1450 }
1451
1452 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1454 if line_num == 0 || line_num > self.lines.len() {
1455 return false;
1456 }
1457
1458 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1462 let code_spans = self.code_spans();
1463 code_spans.iter().any(|span| {
1464 if line_num < span.line || line_num > span.end_line {
1466 return false;
1467 }
1468
1469 if span.line == span.end_line {
1470 col_0indexed >= span.start_col && col_0indexed < span.end_col
1472 } else if line_num == span.line {
1473 col_0indexed >= span.start_col
1475 } else if line_num == span.end_line {
1476 col_0indexed < span.end_col
1478 } else {
1479 true
1481 }
1482 })
1483 }
1484
1485 #[inline]
1487 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1488 let code_spans = self.code_spans();
1489 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1490 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1491 }
1492
1493 #[inline]
1495 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1496 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1497 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1498 }
1499
1500 #[inline]
1502 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1503 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1504 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1505 }
1506
1507 #[inline]
1510 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1511 let tags = self.html_tags();
1512 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1513 idx > 0 && byte_pos < tags[idx - 1].byte_end
1514 }
1515
1516 #[inline]
1520 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1521 if !self.flavor.supports_jsx() {
1522 return false;
1523 }
1524 let tags = self.jsx_component_tags();
1525 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1526 idx > 0 && byte_pos < tags[idx - 1].byte_end
1527 }
1528
1529 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1531 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1532 }
1533
1534 #[inline]
1536 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1537 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1538 }
1539
1540 #[inline]
1542 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1543 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1544 }
1545
1546 #[inline]
1549 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1550 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1551 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1552 }
1553
1554 #[inline]
1556 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1557 &self.citation_ranges
1558 }
1559
1560 #[inline]
1563 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1564 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1565 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1566 }
1567
1568 #[inline]
1571 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1572 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1573 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1574 }
1575
1576 #[inline]
1579 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
1580 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
1581 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
1582 }
1583
1584 #[inline]
1587 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
1588 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
1589 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
1590 }
1591
1592 #[inline]
1595 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
1596 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
1597 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
1598 }
1599
1600 #[inline]
1604 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
1605 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
1606 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
1607 }
1608
1609 #[inline]
1612 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
1613 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
1614 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
1615 }
1616
1617 #[inline]
1620 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
1621 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
1622 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
1623 }
1624
1625 #[inline]
1629 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
1630 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
1631 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
1632 }
1633
1634 #[inline]
1637 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
1638 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
1639 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
1640 }
1641
1642 #[inline]
1645 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
1646 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
1647 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
1648 }
1649
1650 #[inline]
1653 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
1654 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
1655 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
1656 }
1657
1658 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
1663 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
1664 self.pandoc_header_slugs.contains(&slug)
1665 }
1666
1667 #[inline]
1673 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
1674 self.pandoc_header_slugs.contains(slug)
1675 }
1676
1677 #[inline]
1679 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
1680 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
1681 }
1682
1683 #[inline]
1685 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
1686 &self.shortcode_ranges
1687 }
1688
1689 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
1691 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
1692 }
1693
1694 pub fn has_char(&self, ch: char) -> bool {
1696 match ch {
1697 '#' => self.char_frequency.hash_count > 0,
1698 '*' => self.char_frequency.asterisk_count > 0,
1699 '_' => self.char_frequency.underscore_count > 0,
1700 '-' => self.char_frequency.hyphen_count > 0,
1701 '+' => self.char_frequency.plus_count > 0,
1702 '>' => self.char_frequency.gt_count > 0,
1703 '|' => self.char_frequency.pipe_count > 0,
1704 '[' => self.char_frequency.bracket_count > 0,
1705 '`' => self.char_frequency.backtick_count > 0,
1706 '<' => self.char_frequency.lt_count > 0,
1707 '!' => self.char_frequency.exclamation_count > 0,
1708 '\n' => self.char_frequency.newline_count > 0,
1709 _ => self.content.contains(ch), }
1711 }
1712
1713 pub fn char_count(&self, ch: char) -> usize {
1715 match ch {
1716 '#' => self.char_frequency.hash_count,
1717 '*' => self.char_frequency.asterisk_count,
1718 '_' => self.char_frequency.underscore_count,
1719 '-' => self.char_frequency.hyphen_count,
1720 '+' => self.char_frequency.plus_count,
1721 '>' => self.char_frequency.gt_count,
1722 '|' => self.char_frequency.pipe_count,
1723 '[' => self.char_frequency.bracket_count,
1724 '`' => self.char_frequency.backtick_count,
1725 '<' => self.char_frequency.lt_count,
1726 '!' => self.char_frequency.exclamation_count,
1727 '\n' => self.char_frequency.newline_count,
1728 _ => self.content.matches(ch).count(), }
1730 }
1731
1732 pub fn likely_has_headings(&self) -> bool {
1734 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 2 || self.content.contains('=') }
1736
1737 pub fn likely_has_lists(&self) -> bool {
1739 self.char_frequency.asterisk_count > 0
1740 || self.char_frequency.hyphen_count > 0
1741 || self.char_frequency.plus_count > 0
1742 }
1743
1744 pub fn likely_has_emphasis(&self) -> bool {
1746 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
1747 }
1748
1749 pub fn likely_has_tables(&self) -> bool {
1751 self.char_frequency.pipe_count > 2
1752 }
1753
1754 pub fn likely_has_blockquotes(&self) -> bool {
1756 self.char_frequency.gt_count > 0
1757 }
1758
1759 pub fn likely_has_code(&self) -> bool {
1761 self.char_frequency.backtick_count > 0
1762 }
1763
1764 pub fn likely_has_links_or_images(&self) -> bool {
1766 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
1767 }
1768
1769 pub fn likely_has_html(&self) -> bool {
1771 self.char_frequency.lt_count > 0
1772 }
1773
1774 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
1779 if let Some(line_info) = self.lines.get(line_idx)
1780 && let Some(ref bq) = line_info.blockquote
1781 {
1782 bq.prefix.trim_end().to_string()
1783 } else {
1784 String::new()
1785 }
1786 }
1787
1788 #[inline]
1799 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
1800 let idx = match lines.binary_search_by(|line| {
1802 if byte_offset < line.byte_offset {
1803 std::cmp::Ordering::Greater
1804 } else if byte_offset > line.byte_offset + line.byte_len {
1805 std::cmp::Ordering::Less
1806 } else {
1807 std::cmp::Ordering::Equal
1808 }
1809 }) {
1810 Ok(idx) => idx,
1811 Err(idx) => idx.saturating_sub(1),
1812 };
1813
1814 let line = &lines[idx];
1815 let line_num = idx + 1;
1816 let byte_col = byte_offset.saturating_sub(line.byte_offset);
1817 let col = byte_to_char_count(line.content(content), byte_col) - 1;
1820
1821 (idx, line_num, col)
1822 }
1823
1824 #[inline]
1826 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
1827 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
1829
1830 if idx > 0 {
1832 let span = &code_spans[idx - 1];
1833 if offset >= span.byte_offset && offset < span.byte_end {
1834 return true;
1835 }
1836 }
1837
1838 false
1839 }
1840
1841 #[must_use]
1861 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
1862 ValidHeadingsIter::new(&self.lines)
1863 }
1864
1865 #[must_use]
1869 pub fn has_valid_headings(&self) -> bool {
1870 self.lines
1871 .iter()
1872 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
1873 }
1874}
1875
1876fn container_comment_range(
1888 opener: usize,
1889 containers: &flavor_detection::ContainerLines,
1890 lines: &[types::LineInfo],
1891 content: &str,
1892) -> Option<crate::utils::skip_context::ByteRange> {
1893 let line_index = lines
1894 .partition_point(|line| line.byte_offset <= opener)
1895 .checked_sub(1)?;
1896 let line = lines.get(line_index)?;
1897 if line.byte_offset + line.indent != opener {
1898 return None;
1899 }
1900 if !containers.is_container_body(line_index) {
1901 return None;
1902 }
1903 let end_line = lines.get(containers.body_end_line(line_index)?)?;
1904 Some(crate::utils::skip_context::ByteRange {
1905 start: opener,
1906 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
1907 })
1908}
1909
1910fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
1919 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
1920
1921 let options = crate::utils::rumdl_parser_options();
1922 let parser = Parser::new_ext(content, options).into_offset_iter();
1923
1924 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
1926 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
1927 let mut in_footnote = false;
1928
1929 for (event, range) in parser {
1930 match event {
1931 Event::Start(Tag::FootnoteDefinition(_)) => {
1932 in_footnote = true;
1933 footnote_ranges.push((range.start, range.end));
1934 }
1935 Event::End(TagEnd::FootnoteDefinition) => {
1936 in_footnote = false;
1937 }
1938 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
1939 fenced_code_ranges.push((range.start, range.end));
1940 }
1941 _ => {}
1942 }
1943 }
1944
1945 let byte_to_line = |byte_offset: usize| -> usize {
1946 line_offsets
1947 .partition_point(|&offset| offset <= byte_offset)
1948 .saturating_sub(1)
1949 };
1950
1951 for &(start, end) in &footnote_ranges {
1953 let start_line = byte_to_line(start);
1954 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1955
1956 for line in &mut lines[start_line..end_line] {
1957 line.in_footnote_definition = true;
1958 line.in_code_block = false;
1959 }
1960 }
1961
1962 for &(start, end) in &fenced_code_ranges {
1964 let start_line = byte_to_line(start);
1965 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1966
1967 for line in &mut lines[start_line..end_line] {
1968 line.in_code_block = true;
1969 }
1970 }
1971}