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>>>, table_rows_cache: OnceLock<Arc<Vec<TableRow>>>, 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)>, 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, }
117
118impl<'a> LintContext<'a> {
119 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
120 #[cfg(not(target_arch = "wasm32"))]
121 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
122
123 let line_offsets = profile_section!("Line offsets", profile, {
124 let mut offsets = vec![0];
125 for (i, c) in content.char_indices() {
126 if c == '\n' {
127 offsets.push(i + 1);
128 }
129 }
130 offsets
131 });
132
133 let content_lines: Vec<&str> = content.lines().collect();
135
136 #[allow(clippy::disallowed_methods)]
140 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
141
142 let parse_result = profile_section!(
144 "Code blocks",
145 profile,
146 CodeBlockUtils::detect_code_blocks_and_spans(content)
147 );
148 let mut code_blocks = parse_result.code_blocks;
149 let code_span_ranges = parse_result.code_spans;
150 let code_block_details = parse_result.code_block_details;
151 let strong_spans = parse_result.strong_spans;
152 let line_to_list = parse_result.line_to_list;
153 let list_start_values = parse_result.list_start_values;
154
155 let fenced_code_block_ranges: Vec<(usize, usize)> = code_block_details
163 .iter()
164 .filter(|detail| detail.is_fenced)
165 .map(|detail| (detail.start, detail.end))
166 .collect();
167 let html_comment_ranges = profile_section!(
168 "HTML comment ranges",
169 profile,
170 crate::utils::skip_context::compute_html_comment_ranges_filtered(
171 content,
172 &code_span_ranges,
173 &fenced_code_block_ranges
174 )
175 );
176
177 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
181 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
182 Vec::new()
183 } else {
184 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
185 }
186 });
187
188 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
190 if flavor.is_pandoc_compatible() {
191 crate::utils::pandoc::detect_div_block_ranges(content)
192 } else {
193 Vec::new()
194 }
195 });
196
197 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
199 if flavor == MarkdownFlavor::MkDocs {
200 crate::utils::pymdown_blocks::detect_block_ranges(content)
201 } else {
202 Vec::new()
203 }
204 });
205
206 let skip_ranges = SkipByteRanges {
209 html_comment_ranges: &html_comment_ranges,
210 autodoc_ranges: &autodoc_ranges,
211 pandoc_div_ranges: &pandoc_div_ranges,
212 pymdown_block_ranges: &pymdown_block_ranges,
213 };
214 let (mut lines, emphasis_spans) = profile_section!(
215 "Basic line info",
216 profile,
217 line_computation::compute_basic_line_info(
218 content,
219 &content_lines,
220 &line_offsets,
221 &code_blocks,
222 flavor,
223 &skip_ranges,
224 front_matter_end,
225 )
226 );
227
228 profile_section!(
230 "HTML blocks",
231 profile,
232 heading_detection::detect_html_blocks(content, &mut lines)
233 );
234
235 profile_section!(
237 "ESM blocks",
238 profile,
239 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
240 );
241
242 profile_section!(
244 "JSX block detection",
245 profile,
246 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
247 );
248
249 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
251 "JSX/MDX detection",
252 profile,
253 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
254 );
255
256 profile_section!(
261 "Markdown-in-HTML blocks",
262 profile,
263 flavor_detection::detect_markdown_html_blocks(&content_lines, &mut lines)
264 );
265
266 profile_section!(
268 "MkDocs constructs",
269 profile,
270 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor)
271 );
272
273 profile_section!(
278 "Footnote definitions",
279 profile,
280 detect_footnote_definitions(content, &mut lines, &line_offsets)
281 );
282
283 {
286 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
287 for &(start, end) in &code_blocks {
288 let start_line = line_offsets
289 .partition_point(|&offset| offset <= start)
290 .saturating_sub(1);
291 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
292
293 let mut sub_start: Option<usize> = None;
294 for (i, &offset) in line_offsets[start_line..end_line]
295 .iter()
296 .enumerate()
297 .map(|(j, o)| (j + start_line, o))
298 {
299 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
300 if is_real_code && sub_start.is_none() {
301 let byte_start = if i == start_line { start } else { offset };
302 sub_start = Some(byte_start);
303 } else if !is_real_code && sub_start.is_some() {
304 new_code_blocks.push((sub_start.unwrap(), offset));
305 sub_start = None;
306 }
307 }
308 if let Some(s) = sub_start {
309 new_code_blocks.push((s, end));
310 }
311 }
312 code_blocks = new_code_blocks;
313 }
314
315 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
323 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
324 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
325 for &(start, end) in &code_blocks {
326 let start_line = line_offsets
327 .partition_point(|&offset| offset <= start)
328 .saturating_sub(1);
329 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
330
331 let mut sub_start: Option<usize> = None;
333 for (i, &offset) in line_offsets[start_line..end_line]
334 .iter()
335 .enumerate()
336 .map(|(j, o)| (j + start_line, o))
337 {
338 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
339 if is_real_code && sub_start.is_none() {
340 let byte_start = if i == start_line { start } else { offset };
341 sub_start = Some(byte_start);
342 } else if !is_real_code && sub_start.is_some() {
343 new_code_blocks.push((sub_start.unwrap(), offset));
344 sub_start = None;
345 }
346 }
347 if let Some(s) = sub_start {
348 new_code_blocks.push((s, end));
349 }
350 }
351 code_blocks = new_code_blocks;
352 }
353
354 if flavor.supports_jsx() {
358 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
359 for &(start, end) in &code_blocks {
360 let start_line = line_offsets
361 .partition_point(|&offset| offset <= start)
362 .saturating_sub(1);
363 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
364
365 let mut sub_start: Option<usize> = None;
366 for (i, &offset) in line_offsets[start_line..end_line]
367 .iter()
368 .enumerate()
369 .map(|(j, o)| (j + start_line, o))
370 {
371 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
372 if is_real_code && sub_start.is_none() {
373 let byte_start = if i == start_line { start } else { offset };
374 sub_start = Some(byte_start);
375 } else if !is_real_code && sub_start.is_some() {
376 new_code_blocks.push((sub_start.unwrap(), offset));
377 sub_start = None;
378 }
379 }
380 if let Some(s) = sub_start {
381 new_code_blocks.push((s, end));
382 }
383 }
384 code_blocks = new_code_blocks;
385
386 let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new();
393 let mut run: Option<(usize, usize)> = None;
394 for line in &lines {
395 if line.in_jsx_block && line.in_code_block {
396 let line_end = line.byte_offset + line.byte_len;
397 match &mut run {
398 Some((_, end)) => *end = line_end,
399 None => run = Some((line.byte_offset, line_end)),
400 }
401 } else if let Some(r) = run.take() {
402 jsx_fence_ranges.push(r);
403 }
404 }
405 if let Some(r) = run.take() {
406 jsx_fence_ranges.push(r);
407 }
408 if !jsx_fence_ranges.is_empty() {
409 code_blocks.extend(jsx_fence_ranges);
410 code_blocks.sort_by_key(|&(start, _)| start);
411 }
412 }
413
414 let colon_fence_ranges = profile_section!(
417 "Azure colon fence detection",
418 profile,
419 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
420 );
421 if !colon_fence_ranges.is_empty() {
422 code_blocks.extend(colon_fence_ranges.iter().copied());
423 code_blocks.sort_by_key(|&(start, _)| start);
424 }
425
426 let myst_directive_ranges = profile_section!(
429 "MyST colon directives",
430 profile,
431 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
432 );
433
434 let myst_comment_ranges = profile_section!(
436 "MyST comments",
437 profile,
438 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
439 );
440
441 profile_section!(
444 "MyST backtick directives",
445 profile,
446 flavor_detection::detect_myst_backtick_directives(
447 content,
448 &mut lines,
449 flavor,
450 &code_block_details,
451 &line_offsets
452 )
453 );
454
455 if flavor.supports_myst_directives() {
458 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
459 for &(start, end) in &code_blocks {
460 let start_line = line_offsets
461 .partition_point(|&offset| offset <= start)
462 .saturating_sub(1);
463 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
464
465 let mut sub_start: Option<usize> = None;
466 for (i, &offset) in line_offsets[start_line..end_line]
467 .iter()
468 .enumerate()
469 .map(|(j, o)| (j + start_line, o))
470 {
471 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
472 if is_real_code && sub_start.is_none() {
473 let byte_start = if i == start_line { start } else { offset };
474 sub_start = Some(byte_start);
475 } else if !is_real_code && sub_start.is_some() {
476 new_code_blocks.push((sub_start.unwrap(), offset));
477 sub_start = None;
478 }
479 }
480 if let Some(s) = sub_start {
481 new_code_blocks.push((s, end));
482 }
483 }
484 code_blocks = new_code_blocks;
485 }
486
487 profile_section!(
489 "Kramdown constructs",
490 profile,
491 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
492 );
493
494 for line in &mut lines {
499 if line.in_kramdown_extension_block {
500 line.list_item = None;
501 line.is_horizontal_rule = false;
502 line.blockquote = None;
503 line.is_kramdown_block_ial = false;
504 }
505 }
506
507 let obsidian_comment_ranges = profile_section!(
509 "Obsidian comments",
510 profile,
511 flavor_detection::detect_obsidian_comments(content, &mut lines, flavor, &code_span_ranges)
512 );
513
514 let myst_role_ranges = profile_section!(
516 "MyST roles",
517 profile,
518 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
519 );
520
521 let pulldown_result = profile_section!(
525 "Links, images & link ranges",
526 profile,
527 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
528 );
529
530 profile_section!(
532 "Headings & blockquotes",
533 profile,
534 heading_detection::detect_headings_and_blockquotes(
535 &content_lines,
536 &mut lines,
537 flavor,
538 &html_comment_ranges,
539 &pulldown_result.link_byte_ranges,
540 front_matter_end,
541 )
542 );
543
544 for line in &mut lines {
546 if line.in_kramdown_extension_block {
547 line.heading = None;
548 }
549 }
550
551 let mut code_spans = profile_section!(
553 "Code spans",
554 profile,
555 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
556 );
557
558 if flavor == MarkdownFlavor::MkDocs {
562 let extra = profile_section!(
563 "MkDocs code spans",
564 profile,
565 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
566 );
567 if !extra.is_empty() {
568 code_spans.extend(extra);
569 code_spans.sort_by_key(|span| span.byte_offset);
570 }
571 }
572
573 if flavor == MarkdownFlavor::MDX {
578 let extra = profile_section!(
579 "MDX JSX code spans",
580 profile,
581 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
582 );
583 if !extra.is_empty() {
584 code_spans.extend(extra);
585 code_spans.sort_by_key(|span| span.byte_offset);
586 }
587 }
588
589 for span in &code_spans {
592 if span.end_line > span.line {
593 for line_num in (span.line + 1)..=span.end_line {
595 if let Some(line_info) = lines.get_mut(line_num - 1) {
596 line_info.in_code_span_continuation = true;
597 }
598 }
599 }
600 }
601
602 let (links, images, broken_links, footnote_refs) = profile_section!(
604 "Links & images finalize",
605 profile,
606 link_parser::finalize_links_and_images(
607 content,
608 &lines,
609 &code_blocks,
610 &code_spans,
611 flavor,
612 &html_comment_ranges,
613 pulldown_result
614 )
615 );
616
617 let reference_defs = profile_section!(
618 "Reference defs",
619 profile,
620 link_parser::parse_reference_defs(content, &lines)
621 );
622
623 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
624
625 let char_frequency = profile_section!(
627 "Char frequency",
628 profile,
629 line_computation::compute_char_frequency(content)
630 );
631
632 let table_blocks = profile_section!(
634 "Table blocks",
635 profile,
636 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
637 content,
638 &code_blocks,
639 &code_spans,
640 &html_comment_ranges,
641 )
642 );
643
644 let links = links
647 .into_iter()
648 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
649 .collect::<Vec<_>>();
650 let images = images
651 .into_iter()
652 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
653 .collect::<Vec<_>>();
654 let broken_links = broken_links
655 .into_iter()
656 .filter(|bl| {
657 let line_idx = line_offsets
659 .partition_point(|&offset| offset <= bl.span.start)
660 .saturating_sub(1);
661 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
662 })
663 .collect::<Vec<_>>();
664 let footnote_refs = footnote_refs
665 .into_iter()
666 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
667 .collect::<Vec<_>>();
668 let reference_defs = reference_defs
669 .into_iter()
670 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
671 .collect::<Vec<_>>();
672 let list_blocks = list_blocks
673 .into_iter()
674 .filter(|block| {
675 !lines
676 .get(block.start_line - 1)
677 .is_some_and(|l| l.in_kramdown_extension_block)
678 })
679 .collect::<Vec<_>>();
680 let table_blocks = table_blocks
681 .into_iter()
682 .filter(|block| {
683 !lines
685 .get(block.start_line)
686 .is_some_and(|l| l.in_kramdown_extension_block)
687 })
688 .collect::<Vec<_>>();
689 let emphasis_spans = emphasis_spans
690 .into_iter()
691 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
692 .collect::<Vec<_>>();
693
694 for block in &list_blocks {
698 for line_num in block.start_line..=block.end_line {
700 if let Some(li) = lines.get_mut(line_num - 1) {
701 li.in_list_block = true;
702 }
703 }
704 }
705 for block in &table_blocks {
706 for idx in block.start_line..=block.end_line {
708 if let Some(li) = lines.get_mut(idx) {
709 li.in_table_block = true;
710 }
711 }
712 }
713
714 let reference_defs_map: HashMap<String, usize> = reference_defs
716 .iter()
717 .enumerate()
718 .map(|(idx, def)| (def.id.to_lowercase(), idx))
719 .collect();
720
721 let link_title_ranges: Vec<(usize, usize)> = reference_defs
723 .iter()
724 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
725 (Some(start), Some(end)) => Some((start, end)),
726 _ => None,
727 })
728 .collect();
729
730 let line_index = profile_section!(
732 "Line index",
733 profile,
734 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
735 content,
736 line_offsets.clone(),
737 &code_blocks,
738 )
739 );
740
741 let jinja_ranges = profile_section!(
743 "Jinja ranges",
744 profile,
745 crate::utils::jinja_utils::find_jinja_ranges(content)
746 );
747
748 let citation_ranges = profile_section!("Citation ranges", profile, {
750 if flavor.is_pandoc_compatible() {
751 crate::utils::pandoc::find_citation_ranges(content)
752 } else {
753 Vec::new()
754 }
755 });
756
757 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
759 if flavor.is_pandoc_compatible() {
760 crate::utils::pandoc::detect_inline_footnote_ranges(content)
761 } else {
762 Vec::new()
763 }
764 });
765
766 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
768 if flavor.is_pandoc_compatible() {
769 crate::utils::pandoc::collect_pandoc_header_slugs(content)
770 } else {
771 std::collections::HashSet::new()
772 }
773 });
774
775 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
777 if flavor.is_pandoc_compatible() {
778 crate::utils::pandoc::detect_example_list_marker_ranges(content)
779 } else {
780 Vec::new()
781 }
782 });
783
784 let example_reference_ranges = profile_section!("Example references", profile, {
786 if flavor.is_pandoc_compatible() {
787 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
788 } else {
789 Vec::new()
790 }
791 });
792
793 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
795 if flavor.is_pandoc_compatible() {
796 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
797 } else {
798 Vec::new()
799 }
800 });
801
802 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
804 if flavor.is_pandoc_compatible() {
805 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
806 } else {
807 Vec::new()
808 }
809 });
810
811 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
813 if flavor.is_pandoc_compatible() {
814 crate::utils::pandoc::detect_bracketed_span_ranges(content)
815 } else {
816 Vec::new()
817 }
818 });
819
820 let line_block_ranges = profile_section!("Line block ranges", profile, {
822 if flavor.is_pandoc_compatible() {
823 crate::utils::pandoc::detect_line_block_ranges(content)
824 } else {
825 Vec::new()
826 }
827 });
828
829 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
831 if flavor.is_pandoc_compatible() {
832 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
833 } else {
834 Vec::new()
835 }
836 });
837
838 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
840 if flavor.is_pandoc_compatible() {
841 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
842 } else {
843 Vec::new()
844 }
845 });
846
847 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
849 if flavor.is_pandoc_compatible() {
850 crate::utils::pandoc::detect_grid_table_ranges(content)
851 } else {
852 Vec::new()
853 }
854 });
855
856 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
858 if flavor.is_pandoc_compatible() {
859 crate::utils::pandoc::detect_multi_line_table_ranges(content)
860 } else {
861 Vec::new()
862 }
863 });
864
865 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
867 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
868 let mut ranges = Vec::new();
869 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
870 ranges.push((mat.start(), mat.end()));
871 }
872 ranges
873 });
874
875 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks);
876
877 Self {
878 content,
879 content_lines,
880 line_offsets,
881 code_blocks,
882 code_block_details,
883 strong_spans,
884 line_to_list,
885 list_start_values,
886 lines,
887 links,
888 images,
889 broken_links,
890 footnote_refs,
891 reference_defs,
892 reference_defs_map,
893 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
894 math_spans_cache: OnceLock::new(), math_byte_ranges_cache: OnceLock::new(), list_blocks,
897 char_frequency,
898 html_tags_cache: OnceLock::new(),
899 jsx_component_tags_cache: OnceLock::new(),
900 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
901 table_rows_cache: OnceLock::new(),
902 bare_urls_cache: OnceLock::new(),
903 has_mixed_list_nesting_cache: OnceLock::new(),
904 html_comment_ranges,
905 table_blocks,
906 line_index,
907 jinja_ranges,
908 flavor,
909 source_file,
910 jsx_expression_ranges,
911 mdx_comment_ranges,
912 citation_ranges,
913 pandoc_div_ranges,
914 colon_fence_ranges,
915 inline_footnote_ranges,
916 pandoc_header_slugs,
917 example_list_marker_ranges,
918 example_reference_ranges,
919 sub_super_ranges,
920 inline_code_attr_ranges,
921 bracketed_span_ranges,
922 line_block_ranges,
923 pipe_table_caption_ranges,
924 pandoc_metadata_ranges,
925 grid_table_ranges,
926 multi_line_table_ranges,
927 shortcode_ranges,
928 link_title_ranges,
929 code_span_byte_ranges: code_span_ranges,
930 inline_config,
931 obsidian_comment_ranges,
932 lazy_cont_lines_cache: OnceLock::new(),
933 myst_directive_ranges,
934 myst_comment_ranges,
935 myst_role_ranges,
936 front_matter_end,
937 }
938 }
939
940 pub fn front_matter_end_line(&self) -> usize {
945 self.front_matter_end
946 }
947
948 #[inline]
951 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
952 let idx = ranges.partition_point(|&(start, _)| start <= pos);
954 idx > 0 && pos < ranges[idx - 1].1
956 }
957
958 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
960 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
961 }
962
963 pub fn is_in_link(&self, pos: usize) -> bool {
965 let idx = self.links.partition_point(|link| link.byte_offset <= pos);
966 if idx > 0 && pos < self.links[idx - 1].byte_end {
967 return true;
968 }
969 let idx = self.images.partition_point(|img| img.byte_offset <= pos);
970 if idx > 0 && pos < self.images[idx - 1].byte_end {
971 return true;
972 }
973 self.is_in_reference_def(pos)
974 }
975
976 pub fn inline_config(&self) -> &InlineConfig {
978 &self.inline_config
979 }
980
981 pub fn colon_fence_ranges(&self) -> &[(usize, usize)] {
984 &self.colon_fence_ranges
985 }
986
987 pub fn raw_lines(&self) -> &[&'a str] {
991 &self.content_lines
992 }
993
994 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
999 self.inline_config.is_rule_disabled(rule_name, line_number)
1000 }
1001
1002 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
1004 Arc::clone(
1005 self.code_spans_cache
1006 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
1007 )
1008 }
1009
1010 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
1014 self.math_byte_ranges_cache
1015 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
1016 }
1017
1018 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
1020 Arc::clone(
1021 self.math_spans_cache
1022 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
1023 )
1024 }
1025
1026 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
1028 let math_spans = self.math_spans();
1029 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
1031 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
1032 }
1033
1034 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1036 &self.html_comment_ranges
1037 }
1038
1039 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1043 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1044 }
1045
1046 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1051 if self.obsidian_comment_ranges.is_empty() {
1052 return false;
1053 }
1054
1055 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1057 self.is_in_obsidian_comment(byte_pos)
1058 }
1059
1060 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1062 &self.myst_directive_ranges
1063 }
1064
1065 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1067 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1068 }
1069
1070 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1072 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1073 }
1074
1075 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1082 if !self.flavor.supports_myst_directives() {
1083 return false;
1084 }
1085 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1086 info.in_myst_directive
1087 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1088 })
1089 }
1090
1091 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1093 tags.into_iter()
1094 .filter(|tag| {
1095 !self
1096 .lines
1097 .get(tag.line - 1)
1098 .is_some_and(|l| l.in_kramdown_extension_block)
1099 })
1100 .collect()
1101 }
1102
1103 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1109 Arc::clone(self.html_tags_cache.get_or_init(|| {
1110 let (html_tags, jsx_component_tags) =
1111 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1112 let _ = self
1114 .jsx_component_tags_cache
1115 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1116 Arc::new(self.filter_kramdown_tags(html_tags))
1117 }))
1118 }
1119
1120 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1123 if let Some(cached) = self.jsx_component_tags_cache.get() {
1124 return Arc::clone(cached);
1125 }
1126 let _ = self.html_tags();
1128 Arc::clone(
1129 self.jsx_component_tags_cache
1130 .get()
1131 .expect("html_tags() populates jsx_component_tags_cache"),
1132 )
1133 }
1134
1135 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1137 Arc::clone(
1138 self.emphasis_spans_cache
1139 .get()
1140 .expect("emphasis_spans_cache initialized during construction"),
1141 )
1142 }
1143
1144 pub fn table_rows(&self) -> Arc<Vec<TableRow>> {
1146 Arc::clone(
1147 self.table_rows_cache
1148 .get_or_init(|| Arc::new(element_parsers::parse_table_rows(self.content, &self.lines))),
1149 )
1150 }
1151
1152 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1154 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1155 Arc::new(element_parsers::parse_bare_urls(
1156 self.content,
1157 &self.lines,
1158 &self.code_blocks,
1159 ))
1160 }))
1161 }
1162
1163 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1165 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1166 Arc::new(element_parsers::detect_lazy_continuation_lines(
1167 self.content,
1168 &self.lines,
1169 &self.line_offsets,
1170 ))
1171 }))
1172 }
1173
1174 pub fn has_mixed_list_nesting(&self) -> bool {
1178 *self
1179 .has_mixed_list_nesting_cache
1180 .get_or_init(|| self.compute_mixed_list_nesting())
1181 }
1182
1183 fn compute_mixed_list_nesting(&self) -> bool {
1185 let mut stack: Vec<(usize, bool)> = Vec::new();
1190 let mut last_was_blank = false;
1191
1192 for line_info in &self.lines {
1193 if line_info.in_code_block
1195 || line_info.in_front_matter
1196 || line_info.in_mkdocstrings
1197 || line_info.in_html_comment
1198 || line_info.in_mdx_comment
1199 || line_info.in_esm_block
1200 {
1201 continue;
1202 }
1203
1204 if line_info.is_blank {
1206 last_was_blank = true;
1207 continue;
1208 }
1209
1210 if let Some(list_item) = &line_info.list_item {
1211 let current_pos = if list_item.marker_column == 1 {
1213 0
1214 } else {
1215 list_item.marker_column
1216 };
1217
1218 if last_was_blank && current_pos == 0 {
1220 stack.clear();
1221 }
1222 last_was_blank = false;
1223
1224 while let Some(&(pos, _)) = stack.last() {
1226 if pos >= current_pos {
1227 stack.pop();
1228 } else {
1229 break;
1230 }
1231 }
1232
1233 if let Some(&(_, parent_is_ordered)) = stack.last()
1235 && parent_is_ordered != list_item.is_ordered
1236 {
1237 return true; }
1239
1240 stack.push((current_pos, list_item.is_ordered));
1241 } else {
1242 last_was_blank = false;
1244 }
1245 }
1246
1247 false
1248 }
1249
1250 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1256 match self.line_offsets.binary_search(&offset) {
1257 Ok(line) => (line + 1, 1),
1258 Err(line) => {
1259 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1260 let col = byte_to_char_count(&self.content[line_start..], offset.saturating_sub(line_start));
1262 (line, col)
1263 }
1264 }
1265 }
1266
1267 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1269 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1271 return true;
1272 }
1273
1274 self.is_byte_offset_in_code_span(pos)
1276 }
1277
1278 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1280 if line_num > 0 {
1281 self.lines.get(line_num - 1)
1282 } else {
1283 None
1284 }
1285 }
1286
1287 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1289 let normalized_id = ref_id.to_lowercase();
1290 self.reference_defs_map
1291 .get(&normalized_id)
1292 .map(|&idx| self.reference_defs[idx].url.as_str())
1293 }
1294
1295 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1297 if line_num == 0 || line_num > self.lines.len() {
1298 return false;
1299 }
1300 self.lines[line_num - 1].in_list_block
1301 }
1302
1303 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1305 if line_num == 0 || line_num > self.lines.len() {
1306 return false;
1307 }
1308 self.lines[line_num - 1].in_html_block
1309 }
1310
1311 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1317 if line_num == 0 || line_num > self.lines.len() {
1318 return false;
1319 }
1320 self.lines[line_num - 1].in_table_block
1321 }
1322
1323 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1325 if line_num == 0 || line_num > self.lines.len() {
1326 return false;
1327 }
1328
1329 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1333 let code_spans = self.code_spans();
1334 code_spans.iter().any(|span| {
1335 if line_num < span.line || line_num > span.end_line {
1337 return false;
1338 }
1339
1340 if span.line == span.end_line {
1341 col_0indexed >= span.start_col && col_0indexed < span.end_col
1343 } else if line_num == span.line {
1344 col_0indexed >= span.start_col
1346 } else if line_num == span.end_line {
1347 col_0indexed < span.end_col
1349 } else {
1350 true
1352 }
1353 })
1354 }
1355
1356 #[inline]
1358 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1359 let code_spans = self.code_spans();
1360 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1361 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1362 }
1363
1364 #[inline]
1366 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1367 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1368 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1369 }
1370
1371 #[inline]
1373 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1374 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1375 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1376 }
1377
1378 #[inline]
1381 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1382 let tags = self.html_tags();
1383 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1384 idx > 0 && byte_pos < tags[idx - 1].byte_end
1385 }
1386
1387 #[inline]
1391 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1392 if !self.flavor.supports_jsx() {
1393 return false;
1394 }
1395 let tags = self.jsx_component_tags();
1396 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1397 idx > 0 && byte_pos < tags[idx - 1].byte_end
1398 }
1399
1400 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1402 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1403 }
1404
1405 #[inline]
1407 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1408 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1409 }
1410
1411 #[inline]
1413 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1414 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1415 }
1416
1417 #[inline]
1420 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1421 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1422 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1423 }
1424
1425 #[inline]
1427 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1428 &self.citation_ranges
1429 }
1430
1431 #[inline]
1434 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1435 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1436 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1437 }
1438
1439 #[inline]
1442 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1443 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1444 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1445 }
1446
1447 #[inline]
1450 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
1451 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
1452 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
1453 }
1454
1455 #[inline]
1458 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
1459 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
1460 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
1461 }
1462
1463 #[inline]
1466 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
1467 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
1468 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
1469 }
1470
1471 #[inline]
1475 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
1476 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
1477 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
1478 }
1479
1480 #[inline]
1483 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
1484 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
1485 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
1486 }
1487
1488 #[inline]
1491 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
1492 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
1493 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
1494 }
1495
1496 #[inline]
1500 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
1501 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
1502 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
1503 }
1504
1505 #[inline]
1508 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
1509 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
1510 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
1511 }
1512
1513 #[inline]
1516 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
1517 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
1518 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
1519 }
1520
1521 #[inline]
1524 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
1525 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
1526 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
1527 }
1528
1529 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
1534 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
1535 self.pandoc_header_slugs.contains(&slug)
1536 }
1537
1538 #[inline]
1544 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
1545 self.pandoc_header_slugs.contains(slug)
1546 }
1547
1548 #[inline]
1550 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
1551 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
1552 }
1553
1554 #[inline]
1556 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
1557 &self.shortcode_ranges
1558 }
1559
1560 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
1562 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
1563 }
1564
1565 pub fn has_char(&self, ch: char) -> bool {
1567 match ch {
1568 '#' => self.char_frequency.hash_count > 0,
1569 '*' => self.char_frequency.asterisk_count > 0,
1570 '_' => self.char_frequency.underscore_count > 0,
1571 '-' => self.char_frequency.hyphen_count > 0,
1572 '+' => self.char_frequency.plus_count > 0,
1573 '>' => self.char_frequency.gt_count > 0,
1574 '|' => self.char_frequency.pipe_count > 0,
1575 '[' => self.char_frequency.bracket_count > 0,
1576 '`' => self.char_frequency.backtick_count > 0,
1577 '<' => self.char_frequency.lt_count > 0,
1578 '!' => self.char_frequency.exclamation_count > 0,
1579 '\n' => self.char_frequency.newline_count > 0,
1580 _ => self.content.contains(ch), }
1582 }
1583
1584 pub fn char_count(&self, ch: char) -> usize {
1586 match ch {
1587 '#' => self.char_frequency.hash_count,
1588 '*' => self.char_frequency.asterisk_count,
1589 '_' => self.char_frequency.underscore_count,
1590 '-' => self.char_frequency.hyphen_count,
1591 '+' => self.char_frequency.plus_count,
1592 '>' => self.char_frequency.gt_count,
1593 '|' => self.char_frequency.pipe_count,
1594 '[' => self.char_frequency.bracket_count,
1595 '`' => self.char_frequency.backtick_count,
1596 '<' => self.char_frequency.lt_count,
1597 '!' => self.char_frequency.exclamation_count,
1598 '\n' => self.char_frequency.newline_count,
1599 _ => self.content.matches(ch).count(), }
1601 }
1602
1603 pub fn likely_has_headings(&self) -> bool {
1605 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 2 || self.content.contains('=') }
1607
1608 pub fn likely_has_lists(&self) -> bool {
1610 self.char_frequency.asterisk_count > 0
1611 || self.char_frequency.hyphen_count > 0
1612 || self.char_frequency.plus_count > 0
1613 }
1614
1615 pub fn likely_has_emphasis(&self) -> bool {
1617 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
1618 }
1619
1620 pub fn likely_has_tables(&self) -> bool {
1622 self.char_frequency.pipe_count > 2
1623 }
1624
1625 pub fn likely_has_blockquotes(&self) -> bool {
1627 self.char_frequency.gt_count > 0
1628 }
1629
1630 pub fn likely_has_code(&self) -> bool {
1632 self.char_frequency.backtick_count > 0
1633 }
1634
1635 pub fn likely_has_links_or_images(&self) -> bool {
1637 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
1638 }
1639
1640 pub fn likely_has_html(&self) -> bool {
1642 self.char_frequency.lt_count > 0
1643 }
1644
1645 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
1650 if let Some(line_info) = self.lines.get(line_idx)
1651 && let Some(ref bq) = line_info.blockquote
1652 {
1653 bq.prefix.trim_end().to_string()
1654 } else {
1655 String::new()
1656 }
1657 }
1658
1659 #[inline]
1670 fn find_line_for_offset(lines: &[LineInfo], content: &str, byte_offset: usize) -> (usize, usize, usize) {
1671 let idx = match lines.binary_search_by(|line| {
1673 if byte_offset < line.byte_offset {
1674 std::cmp::Ordering::Greater
1675 } else if byte_offset > line.byte_offset + line.byte_len {
1676 std::cmp::Ordering::Less
1677 } else {
1678 std::cmp::Ordering::Equal
1679 }
1680 }) {
1681 Ok(idx) => idx,
1682 Err(idx) => idx.saturating_sub(1),
1683 };
1684
1685 let line = &lines[idx];
1686 let line_num = idx + 1;
1687 let byte_col = byte_offset.saturating_sub(line.byte_offset);
1688 let col = byte_to_char_count(line.content(content), byte_col) - 1;
1691
1692 (idx, line_num, col)
1693 }
1694
1695 #[inline]
1697 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
1698 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
1700
1701 if idx > 0 {
1703 let span = &code_spans[idx - 1];
1704 if offset >= span.byte_offset && offset < span.byte_end {
1705 return true;
1706 }
1707 }
1708
1709 false
1710 }
1711
1712 #[must_use]
1732 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
1733 ValidHeadingsIter::new(&self.lines)
1734 }
1735
1736 #[must_use]
1740 pub fn has_valid_headings(&self) -> bool {
1741 self.lines
1742 .iter()
1743 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
1744 }
1745}
1746
1747fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
1756 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
1757
1758 let options = crate::utils::rumdl_parser_options();
1759 let parser = Parser::new_ext(content, options).into_offset_iter();
1760
1761 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
1763 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
1764 let mut in_footnote = false;
1765
1766 for (event, range) in parser {
1767 match event {
1768 Event::Start(Tag::FootnoteDefinition(_)) => {
1769 in_footnote = true;
1770 footnote_ranges.push((range.start, range.end));
1771 }
1772 Event::End(TagEnd::FootnoteDefinition) => {
1773 in_footnote = false;
1774 }
1775 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
1776 fenced_code_ranges.push((range.start, range.end));
1777 }
1778 _ => {}
1779 }
1780 }
1781
1782 let byte_to_line = |byte_offset: usize| -> usize {
1783 line_offsets
1784 .partition_point(|&offset| offset <= byte_offset)
1785 .saturating_sub(1)
1786 };
1787
1788 for &(start, end) in &footnote_ranges {
1790 let start_line = byte_to_line(start);
1791 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1792
1793 for line in &mut lines[start_line..end_line] {
1794 line.in_footnote_definition = true;
1795 line.in_code_block = false;
1796 }
1797 }
1798
1799 for &(start, end) in &fenced_code_ranges {
1801 let start_line = byte_to_line(start);
1802 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1803
1804 for line in &mut lines[start_line..end_line] {
1805 line.in_code_block = true;
1806 }
1807 }
1808}