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_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, }
118
119pub fn code_block_ranges(content: &str, flavor: MarkdownFlavor) -> Vec<(usize, usize)> {
130 LintContext::new(content, flavor, None).code_blocks
131}
132
133impl<'a> LintContext<'a> {
134 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
135 #[cfg(not(target_arch = "wasm32"))]
136 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
137
138 let line_offsets = profile_section!("Line offsets", profile, {
139 let mut offsets = vec![0];
140 for (i, c) in content.char_indices() {
141 if c == '\n' {
142 offsets.push(i + 1);
143 }
144 }
145 offsets
146 });
147
148 let content_lines: Vec<&str> = content.lines().collect();
150
151 #[allow(clippy::disallowed_methods)]
155 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
156
157 let parse_result = profile_section!(
159 "Code blocks",
160 profile,
161 CodeBlockUtils::detect_code_blocks_and_spans(content)
162 );
163 let mut code_blocks = parse_result.code_blocks;
164 let code_span_ranges = parse_result.code_spans;
165 let code_block_details = parse_result.code_block_details;
166 let strong_spans = parse_result.strong_spans;
167 let line_to_list = parse_result.line_to_list;
168 let list_start_values = parse_result.list_start_values;
169 let html_blocks = parse_result.html_blocks;
170
171 let containers = profile_section!(
174 "Container lines",
175 profile,
176 flavor_detection::detect_container_lines(&content_lines, flavor)
177 );
178
179 let comment_code_block_ranges: Vec<(usize, usize)> = code_block_details
188 .iter()
189 .flat_map(|detail| {
190 if detail.is_fenced {
191 return vec![(detail.start, detail.end)];
192 }
193 let start_line = line_offsets
194 .partition_point(|&offset| offset <= detail.start)
195 .saturating_sub(1);
196 let end_line = line_offsets.partition_point(|&offset| offset < detail.end);
197 containers
198 .code_line_spans_in(start_line..end_line)
199 .into_iter()
200 .map(|span| {
201 let start = line_offsets[span.start].max(detail.start);
202 let end = line_offsets
203 .get(span.end)
204 .copied()
205 .unwrap_or(content.len())
206 .min(detail.end);
207 (start, end)
208 })
209 .collect()
210 })
211 .collect();
212 let body_start = line_offsets.get(front_matter_end).copied().unwrap_or(content.len());
218 let html_comment_scan = profile_section!(
219 "HTML comment ranges",
220 profile,
221 crate::utils::skip_context::scan_html_comments(
222 content,
223 &code_span_ranges,
224 &comment_code_block_ranges,
225 body_start
226 )
227 );
228 let mut html_comment_ranges = html_comment_scan.ranges;
229 let unterminated_html_comment = html_comment_scan.unterminated;
230
231 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
235 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
236 Vec::new()
237 } else {
238 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
239 }
240 });
241
242 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
244 if flavor.is_pandoc_compatible() {
245 crate::utils::pandoc::detect_div_block_ranges(content)
246 } else {
247 Vec::new()
248 }
249 });
250
251 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
253 if flavor == MarkdownFlavor::MkDocs {
254 crate::utils::pymdown_blocks::detect_block_ranges(content)
255 } else {
256 Vec::new()
257 }
258 });
259
260 let skip_ranges = SkipByteRanges {
263 html_comment_ranges: &html_comment_ranges,
264 autodoc_ranges: &autodoc_ranges,
265 pandoc_div_ranges: &pandoc_div_ranges,
266 pymdown_block_ranges: &pymdown_block_ranges,
267 };
268 let (mut lines, emphasis_spans) = profile_section!(
269 "Basic line info",
270 profile,
271 line_computation::compute_basic_line_info(
272 content,
273 &content_lines,
274 &line_offsets,
275 &code_blocks,
276 flavor,
277 &skip_ranges,
278 front_matter_end,
279 )
280 );
281
282 profile_section!(
284 "HTML blocks",
285 profile,
286 heading_detection::detect_html_blocks(content, &mut lines)
287 );
288
289 profile_section!(
291 "ESM blocks",
292 profile,
293 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
294 );
295
296 profile_section!(
298 "JSX block detection",
299 profile,
300 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
301 );
302
303 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
305 "JSX/MDX detection",
306 profile,
307 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
308 );
309
310 profile_section!(
315 "Markdown-in-HTML blocks",
316 profile,
317 flavor_detection::detect_markdown_html_blocks(&mut lines, &containers)
318 );
319
320 profile_section!(
322 "MkDocs constructs",
323 profile,
324 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor, &containers)
325 );
326
327 profile_section!(
332 "Footnote definitions",
333 profile,
334 detect_footnote_definitions(content, &mut lines, &line_offsets)
335 );
336
337 {
340 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
341 for &(start, end) in &code_blocks {
342 let start_line = line_offsets
343 .partition_point(|&offset| offset <= start)
344 .saturating_sub(1);
345 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
346
347 let mut sub_start: Option<usize> = None;
348 for (i, &offset) in line_offsets[start_line..end_line]
349 .iter()
350 .enumerate()
351 .map(|(j, o)| (j + start_line, o))
352 {
353 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
354 if is_real_code && sub_start.is_none() {
355 let byte_start = if i == start_line { start } else { offset };
356 sub_start = Some(byte_start);
357 } else if !is_real_code && sub_start.is_some() {
358 new_code_blocks.push((sub_start.unwrap(), offset));
359 sub_start = None;
360 }
361 }
362 if let Some(s) = sub_start {
363 new_code_blocks.push((s, end));
364 }
365 }
366 code_blocks = new_code_blocks;
367 }
368
369 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
377 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
378 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
379 for &(start, end) in &code_blocks {
380 let start_line = line_offsets
381 .partition_point(|&offset| offset <= start)
382 .saturating_sub(1);
383 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
384
385 let mut sub_start: Option<usize> = None;
387 for (i, &offset) in line_offsets[start_line..end_line]
388 .iter()
389 .enumerate()
390 .map(|(j, o)| (j + start_line, o))
391 {
392 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
393 if is_real_code && sub_start.is_none() {
394 let byte_start = if i == start_line { start } else { offset };
395 sub_start = Some(byte_start);
396 } else if !is_real_code && sub_start.is_some() {
397 new_code_blocks.push((sub_start.unwrap(), offset));
398 sub_start = None;
399 }
400 }
401 if let Some(s) = sub_start {
402 new_code_blocks.push((s, end));
403 }
404 }
405 code_blocks = new_code_blocks;
406 }
407
408 if flavor.supports_jsx() {
412 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
413 for &(start, end) in &code_blocks {
414 let start_line = line_offsets
415 .partition_point(|&offset| offset <= start)
416 .saturating_sub(1);
417 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
418
419 let mut sub_start: Option<usize> = None;
420 for (i, &offset) in line_offsets[start_line..end_line]
421 .iter()
422 .enumerate()
423 .map(|(j, o)| (j + start_line, o))
424 {
425 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
426 if is_real_code && sub_start.is_none() {
427 let byte_start = if i == start_line { start } else { offset };
428 sub_start = Some(byte_start);
429 } else if !is_real_code && sub_start.is_some() {
430 new_code_blocks.push((sub_start.unwrap(), offset));
431 sub_start = None;
432 }
433 }
434 if let Some(s) = sub_start {
435 new_code_blocks.push((s, end));
436 }
437 }
438 code_blocks = new_code_blocks;
439
440 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
447 let mut run: Option<(usize, usize)> = None;
448 for line in &lines {
449 if line.in_jsx_block && line.in_code_block {
450 let line_end = line.byte_offset + line.byte_len;
451 match &mut run {
452 Some((_, end)) => *end = line_end,
453 None => run = Some((line.byte_offset, line_end)),
454 }
455 } else if let Some(r) = run.take() {
456 jsx_fence_ranges.push(r);
457 }
458 }
459 if let Some(r) = run.take() {
460 jsx_fence_ranges.push(r);
461 }
462 if !jsx_fence_ranges.is_empty() {
463 code_blocks.extend(jsx_fence_ranges);
464 code_blocks.sort_by_key(|&(start, _)| start);
465 }
466 }
467
468 let colon_fence_details = profile_section!(
471 "Azure colon fence detection",
472 profile,
473 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
474 );
475 if !colon_fence_details.is_empty() {
476 code_blocks.extend(colon_fence_details.iter().map(|fence| (fence.start, fence.end)));
477 code_blocks.sort_by_key(|&(start, _)| start);
478 }
479
480 let myst_directive_ranges = profile_section!(
483 "MyST colon directives",
484 profile,
485 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
486 );
487
488 let myst_comment_ranges = profile_section!(
490 "MyST comments",
491 profile,
492 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
493 );
494
495 profile_section!(
498 "MyST backtick directives",
499 profile,
500 flavor_detection::detect_myst_backtick_directives(
501 content,
502 &mut lines,
503 flavor,
504 &code_block_details,
505 &line_offsets
506 )
507 );
508
509 if flavor.supports_myst_directives() {
512 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
513 for &(start, end) in &code_blocks {
514 let start_line = line_offsets
515 .partition_point(|&offset| offset <= start)
516 .saturating_sub(1);
517 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
518
519 let mut sub_start: Option<usize> = None;
520 for (i, &offset) in line_offsets[start_line..end_line]
521 .iter()
522 .enumerate()
523 .map(|(j, o)| (j + start_line, o))
524 {
525 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
526 if is_real_code && sub_start.is_none() {
527 let byte_start = if i == start_line { start } else { offset };
528 sub_start = Some(byte_start);
529 } else if !is_real_code && sub_start.is_some() {
530 new_code_blocks.push((sub_start.unwrap(), offset));
531 sub_start = None;
532 }
533 }
534 if let Some(s) = sub_start {
535 new_code_blocks.push((s, end));
536 }
537 }
538 code_blocks = new_code_blocks;
539 }
540
541 profile_section!(
543 "Kramdown constructs",
544 profile,
545 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
546 );
547
548 for line in &mut lines {
553 if line.in_kramdown_extension_block {
554 line.list_item = None;
555 line.is_horizontal_rule = false;
556 line.blockquote = None;
557 line.is_kramdown_block_ial = false;
558 }
559 }
560
561 let obsidian_comment_scan = profile_section!(
563 "Obsidian comments",
564 profile,
565 flavor_detection::detect_obsidian_comments(
566 content,
567 &mut lines,
568 flavor,
569 &code_span_ranges,
570 &html_comment_ranges,
571 body_start
572 )
573 );
574 let mut obsidian_comment_ranges = obsidian_comment_scan.ranges;
575 let mut unterminated_obsidian_comment = obsidian_comment_scan.unterminated;
576
577 let unterminated_html_comment = crate::utils::skip_context::unterminated_html_comment_outside(
582 unterminated_html_comment,
583 &obsidian_comment_ranges,
584 content,
585 &code_span_ranges,
586 &comment_code_block_ranges,
587 body_start,
588 );
589
590 if let Some(range) = unterminated_html_comment.and_then(|opener| {
603 crate::utils::skip_context::unterminated_comment_range(opener, &html_blocks)
604 .or_else(|| container_comment_range(opener, &containers, &lines, content))
605 }) {
606 html_comment_ranges.push(range);
609
610 for line in &mut lines {
616 let text = line.content(content);
617 let content_start = line.byte_offset + line.indent;
618 let content_end = line.byte_offset + text.trim_end().len();
619 line.in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
620 &html_comment_ranges,
621 content_start,
622 content_end,
623 );
624 line.in_obsidian_comment = false;
625 }
626
627 let obsidian_rescan = flavor_detection::detect_obsidian_comments(
638 content,
639 &mut lines,
640 flavor,
641 &code_span_ranges,
642 &html_comment_ranges,
643 body_start,
644 );
645 obsidian_comment_ranges = obsidian_rescan.ranges;
646 unterminated_obsidian_comment = obsidian_rescan.unterminated;
647 }
648
649 let myst_role_ranges = profile_section!(
651 "MyST roles",
652 profile,
653 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
654 );
655
656 let pulldown_result = profile_section!(
660 "Links, images & link ranges",
661 profile,
662 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
663 );
664
665 profile_section!(
667 "Headings & blockquotes",
668 profile,
669 heading_detection::detect_headings_and_blockquotes(
670 &content_lines,
671 &mut lines,
672 flavor,
673 &html_comment_ranges,
674 &pulldown_result.link_byte_ranges,
675 front_matter_end,
676 )
677 );
678
679 for line in &mut lines {
681 if line.in_kramdown_extension_block {
682 line.heading = None;
683 }
684 }
685
686 for line in &mut lines {
697 if line.is_horizontal_rule
698 && (line.in_code_block
699 || line.in_html_block
700 || line.in_html_comment
701 || line.in_math_block
702 || line.in_mdx_comment
703 || line.in_obsidian_comment)
704 {
705 line.is_horizontal_rule = false;
706 }
707 }
708
709 let mut code_spans = profile_section!(
711 "Code spans",
712 profile,
713 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
714 );
715
716 if flavor == MarkdownFlavor::MkDocs {
720 let extra = profile_section!(
721 "MkDocs code spans",
722 profile,
723 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
724 );
725 if !extra.is_empty() {
726 code_spans.extend(extra);
727 code_spans.sort_by_key(|span| span.byte_offset);
728 }
729 }
730
731 if flavor == MarkdownFlavor::MDX {
736 let extra = profile_section!(
737 "MDX JSX code spans",
738 profile,
739 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
740 );
741 if !extra.is_empty() {
742 code_spans.extend(extra);
743 code_spans.sort_by_key(|span| span.byte_offset);
744 }
745 }
746
747 for span in &code_spans {
750 if span.end_line > span.line {
751 for line_num in (span.line + 1)..=span.end_line {
753 if let Some(line_info) = lines.get_mut(line_num - 1) {
754 line_info.in_code_span_continuation = true;
755 }
756 }
757 }
758 }
759
760 let (links, images, broken_links, footnote_refs) = profile_section!(
762 "Links & images finalize",
763 profile,
764 link_parser::finalize_links_and_images(
765 content,
766 &lines,
767 &code_blocks,
768 &code_spans,
769 flavor,
770 &html_comment_ranges,
771 pulldown_result
772 )
773 );
774
775 let reference_defs = profile_section!(
776 "Reference defs",
777 profile,
778 link_parser::parse_reference_defs(content, &lines)
779 );
780
781 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
782
783 let char_frequency = profile_section!(
785 "Char frequency",
786 profile,
787 line_computation::compute_char_frequency(content)
788 );
789
790 let table_blocks = profile_section!(
792 "Table blocks",
793 profile,
794 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
795 content,
796 &code_blocks,
797 &code_spans,
798 &html_comment_ranges,
799 flavor,
800 )
801 );
802
803 let links = links
806 .into_iter()
807 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
808 .collect::<Vec<_>>();
809 let images = images
810 .into_iter()
811 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
812 .collect::<Vec<_>>();
813 let broken_links = broken_links
814 .into_iter()
815 .filter(|bl| {
816 let line_idx = line_offsets
818 .partition_point(|&offset| offset <= bl.span.start)
819 .saturating_sub(1);
820 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
821 })
822 .collect::<Vec<_>>();
823 let footnote_refs = footnote_refs
824 .into_iter()
825 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
826 .collect::<Vec<_>>();
827 let reference_defs = reference_defs
828 .into_iter()
829 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
830 .collect::<Vec<_>>();
831 let list_blocks = list_blocks
832 .into_iter()
833 .filter(|block| {
834 !lines
835 .get(block.start_line - 1)
836 .is_some_and(|l| l.in_kramdown_extension_block)
837 })
838 .collect::<Vec<_>>();
839 let table_blocks = table_blocks
840 .into_iter()
841 .filter(|block| {
842 !lines
844 .get(block.start_line)
845 .is_some_and(|l| l.in_kramdown_extension_block)
846 })
847 .collect::<Vec<_>>();
848 let emphasis_spans = emphasis_spans
849 .into_iter()
850 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
851 .collect::<Vec<_>>();
852
853 for block in &list_blocks {
857 for line_num in block.start_line..=block.end_line {
859 if let Some(li) = lines.get_mut(line_num - 1) {
860 li.in_list_block = true;
861 }
862 }
863 }
864 for block in &table_blocks {
865 for idx in block.start_line..=block.end_line {
867 if let Some(li) = lines.get_mut(idx) {
868 li.in_table_block = true;
869 }
870 }
871 }
872
873 let reference_defs_map: HashMap<String, usize> = reference_defs
875 .iter()
876 .enumerate()
877 .map(|(idx, def)| (def.id.to_lowercase(), idx))
878 .collect();
879
880 let link_title_ranges: Vec<(usize, usize)> = reference_defs
882 .iter()
883 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
884 (Some(start), Some(end)) => Some((start, end)),
885 _ => None,
886 })
887 .collect();
888
889 let line_index = profile_section!(
891 "Line index",
892 profile,
893 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
894 content,
895 line_offsets.clone(),
896 &code_blocks,
897 )
898 );
899
900 let jinja_ranges = profile_section!(
902 "Jinja ranges",
903 profile,
904 crate::utils::jinja_utils::find_jinja_ranges(content)
905 );
906
907 let citation_ranges = profile_section!("Citation ranges", profile, {
909 if flavor.is_pandoc_compatible() {
910 crate::utils::pandoc::find_citation_ranges(content)
911 } else {
912 Vec::new()
913 }
914 });
915
916 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
918 if flavor.is_pandoc_compatible() {
919 crate::utils::pandoc::detect_inline_footnote_ranges(content)
920 } else {
921 Vec::new()
922 }
923 });
924
925 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
927 if flavor.is_pandoc_compatible() {
928 crate::utils::pandoc::collect_pandoc_header_slugs(content)
929 } else {
930 std::collections::HashSet::new()
931 }
932 });
933
934 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
936 if flavor.is_pandoc_compatible() {
937 crate::utils::pandoc::detect_example_list_marker_ranges(content)
938 } else {
939 Vec::new()
940 }
941 });
942
943 let example_reference_ranges = profile_section!("Example references", profile, {
945 if flavor.is_pandoc_compatible() {
946 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
947 } else {
948 Vec::new()
949 }
950 });
951
952 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
954 if flavor.is_pandoc_compatible() {
955 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
956 } else {
957 Vec::new()
958 }
959 });
960
961 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
963 if flavor.is_pandoc_compatible() {
964 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
965 } else {
966 Vec::new()
967 }
968 });
969
970 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
972 if flavor.is_pandoc_compatible() {
973 crate::utils::pandoc::detect_bracketed_span_ranges(content)
974 } else {
975 Vec::new()
976 }
977 });
978
979 let line_block_ranges = profile_section!("Line block ranges", profile, {
981 if flavor.is_pandoc_compatible() {
982 crate::utils::pandoc::detect_line_block_ranges(content)
983 } else {
984 Vec::new()
985 }
986 });
987
988 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
990 if flavor.is_pandoc_compatible() {
991 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
992 } else {
993 Vec::new()
994 }
995 });
996
997 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
999 if flavor.is_pandoc_compatible() {
1000 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
1001 } else {
1002 Vec::new()
1003 }
1004 });
1005
1006 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
1008 if flavor.is_pandoc_compatible() {
1009 crate::utils::pandoc::detect_grid_table_ranges(content)
1010 } else {
1011 Vec::new()
1012 }
1013 });
1014
1015 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
1017 if flavor.is_pandoc_compatible() {
1018 crate::utils::pandoc::detect_multi_line_table_ranges(content)
1019 } else {
1020 Vec::new()
1021 }
1022 });
1023
1024 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
1026 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
1027 let mut ranges = Vec::new();
1028 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
1029 ranges.push((mat.start(), mat.end()));
1030 }
1031 ranges
1032 });
1033
1034 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks);
1035
1036 Self {
1037 content,
1038 content_lines,
1039 line_offsets,
1040 code_blocks,
1041 code_block_details,
1042 strong_spans,
1043 line_to_list,
1044 list_start_values,
1045 lines,
1046 links,
1047 images,
1048 broken_links,
1049 footnote_refs,
1050 reference_defs,
1051 reference_defs_map,
1052 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
1053 math_spans_cache: OnceLock::new(), math_byte_ranges_cache: OnceLock::new(), list_blocks,
1056 char_frequency,
1057 html_tags_cache: OnceLock::new(),
1058 jsx_component_tags_cache: OnceLock::new(),
1059 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
1060 bare_urls_cache: OnceLock::new(),
1061 has_mixed_list_nesting_cache: OnceLock::new(),
1062 html_comment_ranges,
1063 table_blocks,
1064 line_index,
1065 jinja_ranges,
1066 flavor,
1067 source_file,
1068 jsx_expression_ranges,
1069 mdx_comment_ranges,
1070 citation_ranges,
1071 pandoc_div_ranges,
1072 colon_fence_details,
1073 inline_footnote_ranges,
1074 pandoc_header_slugs,
1075 example_list_marker_ranges,
1076 example_reference_ranges,
1077 sub_super_ranges,
1078 inline_code_attr_ranges,
1079 bracketed_span_ranges,
1080 line_block_ranges,
1081 pipe_table_caption_ranges,
1082 pandoc_metadata_ranges,
1083 grid_table_ranges,
1084 multi_line_table_ranges,
1085 shortcode_ranges,
1086 link_title_ranges,
1087 code_span_byte_ranges: code_span_ranges,
1088 inline_config,
1089 obsidian_comment_ranges,
1090 unterminated_html_comment,
1091 unterminated_obsidian_comment,
1092 lazy_cont_lines_cache: OnceLock::new(),
1093 myst_directive_ranges,
1094 myst_comment_ranges,
1095 myst_role_ranges,
1096 front_matter_end,
1097 }
1098 }
1099
1100 pub fn front_matter_end_line(&self) -> usize {
1105 self.front_matter_end
1106 }
1107
1108 #[inline]
1111 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
1112 let idx = ranges.partition_point(|&(start, _)| start <= pos);
1114 idx > 0 && pos < ranges[idx - 1].1
1116 }
1117
1118 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
1120 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
1121 }
1122
1123 pub fn is_in_link(&self, pos: usize) -> bool {
1125 let idx = self.links.partition_point(|link| link.byte_offset <= pos);
1126 if idx > 0 && pos < self.links[idx - 1].byte_end {
1127 return true;
1128 }
1129 let idx = self.images.partition_point(|img| img.byte_offset <= pos);
1130 if idx > 0 && pos < self.images[idx - 1].byte_end {
1131 return true;
1132 }
1133 self.is_in_reference_def(pos)
1134 }
1135
1136 pub fn is_in_bare_url(&self, pos: usize) -> bool {
1138 let bare_urls = self.bare_urls();
1139 let idx = bare_urls.partition_point(|url| url.byte_offset <= pos);
1141 idx > 0 && pos < bare_urls[idx - 1].byte_end
1142 }
1143
1144 pub fn inline_config(&self) -> &InlineConfig {
1146 &self.inline_config
1147 }
1148
1149 pub fn colon_fence_details(&self) -> &[CodeBlockDetail] {
1154 &self.colon_fence_details
1155 }
1156
1157 pub fn raw_lines(&self) -> &[&'a str] {
1161 &self.content_lines
1162 }
1163
1164 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
1169 self.inline_config.is_rule_disabled(rule_name, line_number)
1170 }
1171
1172 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1174 Arc::clone(
1175 self.code_spans_cache
1176 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1177 )
1178 }
1179
1180 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1184 self.math_byte_ranges_cache
1185 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1186 }
1187
1188 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1190 Arc::clone(
1191 self.math_spans_cache
1192 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1193 )
1194 }
1195
1196 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1198 let math_spans = self.math_spans();
1199 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1201 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1202 }
1203
1204 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1206 &self.html_comment_ranges
1207 }
1208
1209 pub fn unterminated_html_comment(&self) -> Option<usize> {
1214 self.unterminated_html_comment
1215 }
1216
1217 pub fn unterminated_obsidian_comment(&self) -> Option<usize> {
1221 self.unterminated_obsidian_comment
1222 }
1223
1224 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1228 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1229 }
1230
1231 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1236 if self.obsidian_comment_ranges.is_empty() {
1237 return false;
1238 }
1239
1240 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1242 self.is_in_obsidian_comment(byte_pos)
1243 }
1244
1245 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1247 &self.myst_directive_ranges
1248 }
1249
1250 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1252 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1253 }
1254
1255 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1257 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1258 }
1259
1260 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1267 if !self.flavor.supports_myst_directives() {
1268 return false;
1269 }
1270 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1271 info.in_myst_directive
1272 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1273 })
1274 }
1275
1276 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1278 tags.into_iter()
1279 .filter(|tag| {
1280 !self
1281 .lines
1282 .get(tag.line - 1)
1283 .is_some_and(|l| l.in_kramdown_extension_block)
1284 })
1285 .collect()
1286 }
1287
1288 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1294 Arc::clone(self.html_tags_cache.get_or_init(|| {
1295 let (html_tags, jsx_component_tags) =
1296 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1297 let _ = self
1299 .jsx_component_tags_cache
1300 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1301 Arc::new(self.filter_kramdown_tags(html_tags))
1302 }))
1303 }
1304
1305 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1308 if let Some(cached) = self.jsx_component_tags_cache.get() {
1309 return Arc::clone(cached);
1310 }
1311 let _ = self.html_tags();
1313 Arc::clone(
1314 self.jsx_component_tags_cache
1315 .get()
1316 .expect("html_tags() populates jsx_component_tags_cache"),
1317 )
1318 }
1319
1320 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1322 Arc::clone(
1323 self.emphasis_spans_cache
1324 .get()
1325 .expect("emphasis_spans_cache initialized during construction"),
1326 )
1327 }
1328
1329 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1331 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1332 Arc::new(element_parsers::parse_bare_urls(
1333 self.content,
1334 &self.lines,
1335 &self.code_blocks,
1336 ))
1337 }))
1338 }
1339
1340 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1342 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1343 Arc::new(element_parsers::detect_lazy_continuation_lines(
1344 self.content,
1345 &self.lines,
1346 &self.line_offsets,
1347 ))
1348 }))
1349 }
1350
1351 pub fn has_mixed_list_nesting(&self) -> bool {
1355 *self
1356 .has_mixed_list_nesting_cache
1357 .get_or_init(|| self.compute_mixed_list_nesting())
1358 }
1359
1360 fn compute_mixed_list_nesting(&self) -> bool {
1362 let mut stack: Vec<(usize, bool)> = Vec::new();
1367 let mut last_was_blank = false;
1368
1369 for line_info in &self.lines {
1370 if line_info.in_code_block
1372 || line_info.in_front_matter
1373 || line_info.in_mkdocstrings
1374 || line_info.in_html_comment
1375 || line_info.in_mdx_comment
1376 || line_info.in_esm_block
1377 {
1378 continue;
1379 }
1380
1381 if line_info.is_blank {
1383 last_was_blank = true;
1384 continue;
1385 }
1386
1387 if let Some(list_item) = &line_info.list_item {
1388 let current_pos = if list_item.marker_column == 1 {
1390 0
1391 } else {
1392 list_item.marker_column
1393 };
1394
1395 if last_was_blank && current_pos == 0 {
1397 stack.clear();
1398 }
1399 last_was_blank = false;
1400
1401 while let Some(&(pos, _)) = stack.last() {
1403 if pos >= current_pos {
1404 stack.pop();
1405 } else {
1406 break;
1407 }
1408 }
1409
1410 if let Some(&(_, parent_is_ordered)) = stack.last()
1412 && parent_is_ordered != list_item.is_ordered
1413 {
1414 return true; }
1416
1417 stack.push((current_pos, list_item.is_ordered));
1418 } else {
1419 last_was_blank = false;
1421 }
1422 }
1423
1424 false
1425 }
1426
1427 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1433 match self.line_offsets.binary_search(&offset) {
1434 Ok(line) => (line + 1, 1),
1435 Err(line) => {
1436 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1437 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1439 (line, col)
1440 }
1441 }
1442 }
1443
1444 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1446 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1448 return true;
1449 }
1450
1451 self.is_byte_offset_in_code_span(pos)
1453 }
1454
1455 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1457 if line_num > 0 {
1458 self.lines.get(line_num - 1)
1459 } else {
1460 None
1461 }
1462 }
1463
1464 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1466 let normalized_id = ref_id.to_lowercase();
1467 self.reference_defs_map
1468 .get(&normalized_id)
1469 .map(|&idx| self.reference_defs[idx].url.as_str())
1470 }
1471
1472 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1474 if line_num == 0 || line_num > self.lines.len() {
1475 return false;
1476 }
1477 self.lines[line_num - 1].in_list_block
1478 }
1479
1480 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1482 if line_num == 0 || line_num > self.lines.len() {
1483 return false;
1484 }
1485 self.lines[line_num - 1].in_html_block
1486 }
1487
1488 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1494 if line_num == 0 || line_num > self.lines.len() {
1495 return false;
1496 }
1497 self.lines[line_num - 1].in_table_block
1498 }
1499
1500 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1502 if line_num == 0 || line_num > self.lines.len() {
1503 return false;
1504 }
1505
1506 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1510 let code_spans = self.code_spans();
1511 code_spans.iter().any(|span| {
1512 if line_num < span.line || line_num > span.end_line {
1514 return false;
1515 }
1516
1517 if span.line == span.end_line {
1518 col_0indexed >= span.start_col && col_0indexed < span.end_col
1520 } else if line_num == span.line {
1521 col_0indexed >= span.start_col
1523 } else if line_num == span.end_line {
1524 col_0indexed < span.end_col
1526 } else {
1527 true
1529 }
1530 })
1531 }
1532
1533 #[inline]
1535 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1536 let code_spans = self.code_spans();
1537 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1538 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1539 }
1540
1541 #[inline]
1543 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1544 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1545 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1546 }
1547
1548 #[inline]
1550 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1551 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1552 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1553 }
1554
1555 #[inline]
1558 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1559 let tags = self.html_tags();
1560 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1561 idx > 0 && byte_pos < tags[idx - 1].byte_end
1562 }
1563
1564 #[inline]
1568 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1569 if !self.flavor.supports_jsx() {
1570 return false;
1571 }
1572 let tags = self.jsx_component_tags();
1573 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1574 idx > 0 && byte_pos < tags[idx - 1].byte_end
1575 }
1576
1577 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1579 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1580 }
1581
1582 #[inline]
1584 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1585 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1586 }
1587
1588 #[inline]
1590 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1591 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1592 }
1593
1594 #[inline]
1597 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1598 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1599 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1600 }
1601
1602 #[inline]
1604 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1605 &self.citation_ranges
1606 }
1607
1608 #[inline]
1611 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1612 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1613 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1614 }
1615
1616 #[inline]
1619 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1620 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1621 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1622 }
1623
1624 #[inline]
1627 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
1628 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
1629 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
1630 }
1631
1632 #[inline]
1635 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
1636 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
1637 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
1638 }
1639
1640 #[inline]
1643 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
1644 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
1645 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
1646 }
1647
1648 #[inline]
1652 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
1653 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
1654 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
1655 }
1656
1657 #[inline]
1660 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
1661 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
1662 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
1663 }
1664
1665 #[inline]
1668 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
1669 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
1670 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
1671 }
1672
1673 #[inline]
1677 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
1678 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
1679 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
1680 }
1681
1682 #[inline]
1685 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
1686 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
1687 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
1688 }
1689
1690 #[inline]
1693 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
1694 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
1695 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
1696 }
1697
1698 #[inline]
1701 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
1702 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
1703 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
1704 }
1705
1706 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
1711 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
1712 self.pandoc_header_slugs.contains(&slug)
1713 }
1714
1715 #[inline]
1721 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
1722 self.pandoc_header_slugs.contains(slug)
1723 }
1724
1725 #[inline]
1727 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
1728 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
1729 }
1730
1731 #[inline]
1733 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
1734 &self.shortcode_ranges
1735 }
1736
1737 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
1739 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
1740 }
1741
1742 pub fn has_char(&self, ch: char) -> bool {
1744 match ch {
1745 '#' => self.char_frequency.hash_count > 0,
1746 '*' => self.char_frequency.asterisk_count > 0,
1747 '_' => self.char_frequency.underscore_count > 0,
1748 '-' => self.char_frequency.hyphen_count > 0,
1749 '+' => self.char_frequency.plus_count > 0,
1750 '>' => self.char_frequency.gt_count > 0,
1751 '|' => self.char_frequency.pipe_count > 0,
1752 '[' => self.char_frequency.bracket_count > 0,
1753 '`' => self.char_frequency.backtick_count > 0,
1754 '<' => self.char_frequency.lt_count > 0,
1755 '!' => self.char_frequency.exclamation_count > 0,
1756 '\n' => self.char_frequency.newline_count > 0,
1757 _ => self.content.contains(ch), }
1759 }
1760
1761 pub fn char_count(&self, ch: char) -> usize {
1763 match ch {
1764 '#' => self.char_frequency.hash_count,
1765 '*' => self.char_frequency.asterisk_count,
1766 '_' => self.char_frequency.underscore_count,
1767 '-' => self.char_frequency.hyphen_count,
1768 '+' => self.char_frequency.plus_count,
1769 '>' => self.char_frequency.gt_count,
1770 '|' => self.char_frequency.pipe_count,
1771 '[' => self.char_frequency.bracket_count,
1772 '`' => self.char_frequency.backtick_count,
1773 '<' => self.char_frequency.lt_count,
1774 '!' => self.char_frequency.exclamation_count,
1775 '\n' => self.char_frequency.newline_count,
1776 _ => self.content.matches(ch).count(), }
1778 }
1779
1780 pub fn likely_has_headings(&self) -> bool {
1782 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 2 || self.content.contains('=') }
1784
1785 pub fn likely_has_lists(&self) -> bool {
1787 self.char_frequency.asterisk_count > 0
1788 || self.char_frequency.hyphen_count > 0
1789 || self.char_frequency.plus_count > 0
1790 }
1791
1792 pub fn likely_has_emphasis(&self) -> bool {
1794 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
1795 }
1796
1797 pub fn likely_has_tables(&self) -> bool {
1799 self.char_frequency.pipe_count > 2
1800 }
1801
1802 pub fn likely_has_blockquotes(&self) -> bool {
1804 self.char_frequency.gt_count > 0
1805 }
1806
1807 pub fn likely_has_code(&self) -> bool {
1809 self.char_frequency.backtick_count > 0
1810 }
1811
1812 pub fn likely_has_links_or_images(&self) -> bool {
1814 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
1815 }
1816
1817 pub fn likely_has_html(&self) -> bool {
1819 self.char_frequency.lt_count > 0
1820 }
1821
1822 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
1827 if let Some(line_info) = self.lines.get(line_idx)
1828 && let Some(ref bq) = line_info.blockquote
1829 {
1830 bq.prefix.trim_end().to_string()
1831 } else {
1832 String::new()
1833 }
1834 }
1835
1836 #[inline]
1847 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
1848 let idx = match lines.binary_search_by(|line| {
1850 if byte_offset < line.byte_offset {
1851 std::cmp::Ordering::Greater
1852 } else if byte_offset > line.byte_offset + line.byte_len {
1853 std::cmp::Ordering::Less
1854 } else {
1855 std::cmp::Ordering::Equal
1856 }
1857 }) {
1858 Ok(idx) => idx,
1859 Err(idx) => idx.saturating_sub(1),
1860 };
1861
1862 let line = &lines[idx];
1863 let line_num = idx + 1;
1864 let byte_col = byte_offset.saturating_sub(line.byte_offset);
1865 let col = byte_to_char_count(line.content(content), byte_col) - 1;
1868
1869 (idx, line_num, col)
1870 }
1871
1872 #[inline]
1874 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
1875 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
1877
1878 if idx > 0 {
1880 let span = &code_spans[idx - 1];
1881 if offset >= span.byte_offset && offset < span.byte_end {
1882 return true;
1883 }
1884 }
1885
1886 false
1887 }
1888
1889 #[must_use]
1909 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
1910 ValidHeadingsIter::new(&self.lines)
1911 }
1912
1913 #[must_use]
1917 pub fn has_valid_headings(&self) -> bool {
1918 self.lines
1919 .iter()
1920 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
1921 }
1922}
1923
1924fn container_comment_range(
1936 opener: usize,
1937 containers: &flavor_detection::ContainerLines,
1938 lines: &[types::LineInfo],
1939 content: &str,
1940) -> Option<crate::utils::skip_context::ByteRange> {
1941 let line_index = lines
1942 .partition_point(|line| line.byte_offset <= opener)
1943 .checked_sub(1)?;
1944 let line = lines.get(line_index)?;
1945 if line.byte_offset + line.indent != opener {
1946 return None;
1947 }
1948 if !containers.is_container_body(line_index) {
1949 return None;
1950 }
1951 let end_line = lines.get(containers.body_end_line(line_index)?)?;
1952 Some(crate::utils::skip_context::ByteRange {
1953 start: opener,
1954 end: (end_line.byte_offset + end_line.byte_len).min(content.len()),
1955 })
1956}
1957
1958fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
1967 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
1968
1969 let options = crate::utils::rumdl_parser_options();
1970 let parser = Parser::new_ext(content, options).into_offset_iter();
1971
1972 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
1974 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
1975 let mut in_footnote = false;
1976
1977 for (event, range) in parser {
1978 match event {
1979 Event::Start(Tag::FootnoteDefinition(_)) => {
1980 in_footnote = true;
1981 footnote_ranges.push((range.start, range.end));
1982 }
1983 Event::End(TagEnd::FootnoteDefinition) => {
1984 in_footnote = false;
1985 }
1986 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
1987 fenced_code_ranges.push((range.start, range.end));
1988 }
1989 _ => {}
1990 }
1991 }
1992
1993 let byte_to_line = |byte_offset: usize| -> usize {
1994 line_offsets
1995 .partition_point(|&offset| offset <= byte_offset)
1996 .saturating_sub(1)
1997 };
1998
1999 for &(start, end) in &footnote_ranges {
2001 let start_line = byte_to_line(start);
2002 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2003
2004 for line in &mut lines[start_line..end_line] {
2005 line.in_footnote_definition = true;
2006 line.in_code_block = false;
2007 }
2008 }
2009
2010 for &(start, end) in &fenced_code_ranges {
2012 let start_line = byte_to_line(start);
2013 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
2014
2015 for line in &mut lines[start_line..end_line] {
2016 line.in_code_block = true;
2017 }
2018 }
2019}