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 std::collections::HashMap;
18use std::path::PathBuf;
19
20#[cfg(not(target_arch = "wasm32"))]
22macro_rules! profile_section {
23 ($name:expr, $profile:expr, $code:expr) => {{
24 let start = std::time::Instant::now();
25 let result = $code;
26 if $profile {
27 eprintln!("[PROFILE] {}: {:?}", $name, start.elapsed());
28 }
29 result
30 }};
31}
32
33#[cfg(target_arch = "wasm32")]
34macro_rules! profile_section {
35 ($name:expr, $profile:expr, $code:expr) => {{ $code }};
36}
37
38pub(super) struct SkipByteRanges<'a> {
41 pub(super) html_comment_ranges: &'a [crate::utils::skip_context::ByteRange],
42 pub(super) autodoc_ranges: &'a [crate::utils::skip_context::ByteRange],
43 pub(super) pandoc_div_ranges: &'a [crate::utils::skip_context::ByteRange],
44 pub(super) pymdown_block_ranges: &'a [crate::utils::skip_context::ByteRange],
45}
46
47use std::sync::{Arc, OnceLock};
48
49pub(super) type ListItemMap = std::collections::HashMap<usize, (bool, String, usize, usize, Option<usize>)>;
51
52pub(super) type ByteRanges = Vec<(usize, usize)>;
54
55pub struct LintContext<'a> {
56 pub content: &'a str,
57 content_lines: Vec<&'a str>, pub line_offsets: Vec<usize>,
59 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, }
116
117impl<'a> LintContext<'a> {
118 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
119 #[cfg(not(target_arch = "wasm32"))]
120 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
121
122 let line_offsets = profile_section!("Line offsets", profile, {
123 let mut offsets = vec![0];
124 for (i, c) in content.char_indices() {
125 if c == '\n' {
126 offsets.push(i + 1);
127 }
128 }
129 offsets
130 });
131
132 let content_lines: Vec<&str> = content.lines().collect();
134
135 #[allow(clippy::disallowed_methods)]
139 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
140
141 let parse_result = profile_section!(
143 "Code blocks",
144 profile,
145 CodeBlockUtils::detect_code_blocks_and_spans(content)
146 );
147 let mut code_blocks = parse_result.code_blocks;
148 let code_span_ranges = parse_result.code_spans;
149 let code_block_details = parse_result.code_block_details;
150 let strong_spans = parse_result.strong_spans;
151 let line_to_list = parse_result.line_to_list;
152 let list_start_values = parse_result.list_start_values;
153
154 let html_comment_ranges = profile_section!(
156 "HTML comment ranges",
157 profile,
158 crate::utils::skip_context::compute_html_comment_ranges(content)
159 );
160
161 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
165 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
166 Vec::new()
167 } else {
168 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
169 }
170 });
171
172 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
174 if flavor.is_pandoc_compatible() {
175 crate::utils::pandoc::detect_div_block_ranges(content)
176 } else {
177 Vec::new()
178 }
179 });
180
181 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
183 if flavor == MarkdownFlavor::MkDocs {
184 crate::utils::pymdown_blocks::detect_block_ranges(content)
185 } else {
186 Vec::new()
187 }
188 });
189
190 let skip_ranges = SkipByteRanges {
193 html_comment_ranges: &html_comment_ranges,
194 autodoc_ranges: &autodoc_ranges,
195 pandoc_div_ranges: &pandoc_div_ranges,
196 pymdown_block_ranges: &pymdown_block_ranges,
197 };
198 let (mut lines, emphasis_spans) = profile_section!(
199 "Basic line info",
200 profile,
201 line_computation::compute_basic_line_info(
202 content,
203 &content_lines,
204 &line_offsets,
205 &code_blocks,
206 flavor,
207 &skip_ranges,
208 front_matter_end,
209 )
210 );
211
212 profile_section!(
214 "HTML blocks",
215 profile,
216 heading_detection::detect_html_blocks(content, &mut lines)
217 );
218
219 profile_section!(
221 "ESM blocks",
222 profile,
223 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
224 );
225
226 profile_section!(
228 "JSX block detection",
229 profile,
230 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
231 );
232
233 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
235 "JSX/MDX detection",
236 profile,
237 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
238 );
239
240 profile_section!(
245 "Markdown-in-HTML blocks",
246 profile,
247 flavor_detection::detect_markdown_html_blocks(&content_lines, &mut lines)
248 );
249
250 profile_section!(
252 "MkDocs constructs",
253 profile,
254 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor)
255 );
256
257 profile_section!(
262 "Footnote definitions",
263 profile,
264 detect_footnote_definitions(content, &mut lines, &line_offsets)
265 );
266
267 {
270 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
271 for &(start, end) in &code_blocks {
272 let start_line = line_offsets
273 .partition_point(|&offset| offset <= start)
274 .saturating_sub(1);
275 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
276
277 let mut sub_start: Option<usize> = None;
278 for (i, &offset) in line_offsets[start_line..end_line]
279 .iter()
280 .enumerate()
281 .map(|(j, o)| (j + start_line, o))
282 {
283 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
284 if is_real_code && sub_start.is_none() {
285 let byte_start = if i == start_line { start } else { offset };
286 sub_start = Some(byte_start);
287 } else if !is_real_code && sub_start.is_some() {
288 new_code_blocks.push((sub_start.unwrap(), offset));
289 sub_start = None;
290 }
291 }
292 if let Some(s) = sub_start {
293 new_code_blocks.push((s, end));
294 }
295 }
296 code_blocks = new_code_blocks;
297 }
298
299 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
307 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
308 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
309 for &(start, end) in &code_blocks {
310 let start_line = line_offsets
311 .partition_point(|&offset| offset <= start)
312 .saturating_sub(1);
313 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
314
315 let mut sub_start: Option<usize> = None;
317 for (i, &offset) in line_offsets[start_line..end_line]
318 .iter()
319 .enumerate()
320 .map(|(j, o)| (j + start_line, o))
321 {
322 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
323 if is_real_code && sub_start.is_none() {
324 let byte_start = if i == start_line { start } else { offset };
325 sub_start = Some(byte_start);
326 } else if !is_real_code && sub_start.is_some() {
327 new_code_blocks.push((sub_start.unwrap(), offset));
328 sub_start = None;
329 }
330 }
331 if let Some(s) = sub_start {
332 new_code_blocks.push((s, end));
333 }
334 }
335 code_blocks = new_code_blocks;
336 }
337
338 if flavor.supports_jsx() {
342 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
343 for &(start, end) in &code_blocks {
344 let start_line = line_offsets
345 .partition_point(|&offset| offset <= start)
346 .saturating_sub(1);
347 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
348
349 let mut sub_start: Option<usize> = None;
350 for (i, &offset) in line_offsets[start_line..end_line]
351 .iter()
352 .enumerate()
353 .map(|(j, o)| (j + start_line, o))
354 {
355 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
356 if is_real_code && sub_start.is_none() {
357 let byte_start = if i == start_line { start } else { offset };
358 sub_start = Some(byte_start);
359 } else if !is_real_code && sub_start.is_some() {
360 new_code_blocks.push((sub_start.unwrap(), offset));
361 sub_start = None;
362 }
363 }
364 if let Some(s) = sub_start {
365 new_code_blocks.push((s, end));
366 }
367 }
368 code_blocks = new_code_blocks;
369 }
370
371 let colon_fence_ranges = profile_section!(
374 "Azure colon fence detection",
375 profile,
376 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
377 );
378 if !colon_fence_ranges.is_empty() {
379 code_blocks.extend(colon_fence_ranges.iter().copied());
380 code_blocks.sort_by_key(|&(start, _)| start);
381 }
382
383 let myst_directive_ranges = profile_section!(
386 "MyST colon directives",
387 profile,
388 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
389 );
390
391 let myst_comment_ranges = profile_section!(
393 "MyST comments",
394 profile,
395 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
396 );
397
398 profile_section!(
401 "MyST backtick directives",
402 profile,
403 flavor_detection::detect_myst_backtick_directives(
404 content,
405 &mut lines,
406 flavor,
407 &code_block_details,
408 &line_offsets
409 )
410 );
411
412 if flavor.supports_myst_directives() {
415 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
416 for &(start, end) in &code_blocks {
417 let start_line = line_offsets
418 .partition_point(|&offset| offset <= start)
419 .saturating_sub(1);
420 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
421
422 let mut sub_start: Option<usize> = None;
423 for (i, &offset) in line_offsets[start_line..end_line]
424 .iter()
425 .enumerate()
426 .map(|(j, o)| (j + start_line, o))
427 {
428 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
429 if is_real_code && sub_start.is_none() {
430 let byte_start = if i == start_line { start } else { offset };
431 sub_start = Some(byte_start);
432 } else if !is_real_code && sub_start.is_some() {
433 new_code_blocks.push((sub_start.unwrap(), offset));
434 sub_start = None;
435 }
436 }
437 if let Some(s) = sub_start {
438 new_code_blocks.push((s, end));
439 }
440 }
441 code_blocks = new_code_blocks;
442 }
443
444 profile_section!(
446 "Kramdown constructs",
447 profile,
448 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
449 );
450
451 for line in &mut lines {
456 if line.in_kramdown_extension_block {
457 line.list_item = None;
458 line.is_horizontal_rule = false;
459 line.blockquote = None;
460 line.is_kramdown_block_ial = false;
461 }
462 }
463
464 let obsidian_comment_ranges = profile_section!(
466 "Obsidian comments",
467 profile,
468 flavor_detection::detect_obsidian_comments(content, &mut lines, flavor, &code_span_ranges)
469 );
470
471 let myst_role_ranges = profile_section!(
473 "MyST roles",
474 profile,
475 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
476 );
477
478 let pulldown_result = profile_section!(
482 "Links, images & link ranges",
483 profile,
484 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
485 );
486
487 profile_section!(
489 "Headings & blockquotes",
490 profile,
491 heading_detection::detect_headings_and_blockquotes(
492 &content_lines,
493 &mut lines,
494 flavor,
495 &html_comment_ranges,
496 &pulldown_result.link_byte_ranges,
497 front_matter_end,
498 )
499 );
500
501 for line in &mut lines {
503 if line.in_kramdown_extension_block {
504 line.heading = None;
505 }
506 }
507
508 let mut code_spans = profile_section!(
510 "Code spans",
511 profile,
512 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
513 );
514
515 if flavor == MarkdownFlavor::MkDocs {
519 let extra = profile_section!(
520 "MkDocs code spans",
521 profile,
522 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
523 );
524 if !extra.is_empty() {
525 code_spans.extend(extra);
526 code_spans.sort_by_key(|span| span.byte_offset);
527 }
528 }
529
530 if flavor == MarkdownFlavor::MDX {
535 let extra = profile_section!(
536 "MDX JSX code spans",
537 profile,
538 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
539 );
540 if !extra.is_empty() {
541 code_spans.extend(extra);
542 code_spans.sort_by_key(|span| span.byte_offset);
543 }
544 }
545
546 for span in &code_spans {
549 if span.end_line > span.line {
550 for line_num in (span.line + 1)..=span.end_line {
552 if let Some(line_info) = lines.get_mut(line_num - 1) {
553 line_info.in_code_span_continuation = true;
554 }
555 }
556 }
557 }
558
559 let (links, images, broken_links, footnote_refs) = profile_section!(
561 "Links & images finalize",
562 profile,
563 link_parser::finalize_links_and_images(
564 content,
565 &lines,
566 &code_blocks,
567 &code_spans,
568 flavor,
569 &html_comment_ranges,
570 pulldown_result
571 )
572 );
573
574 let reference_defs = profile_section!(
575 "Reference defs",
576 profile,
577 link_parser::parse_reference_defs(content, &lines)
578 );
579
580 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
581
582 let char_frequency = profile_section!(
584 "Char frequency",
585 profile,
586 line_computation::compute_char_frequency(content)
587 );
588
589 let table_blocks = profile_section!(
591 "Table blocks",
592 profile,
593 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
594 content,
595 &code_blocks,
596 &code_spans,
597 &html_comment_ranges,
598 )
599 );
600
601 let links = links
604 .into_iter()
605 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
606 .collect::<Vec<_>>();
607 let images = images
608 .into_iter()
609 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
610 .collect::<Vec<_>>();
611 let broken_links = broken_links
612 .into_iter()
613 .filter(|bl| {
614 let line_idx = line_offsets
616 .partition_point(|&offset| offset <= bl.span.start)
617 .saturating_sub(1);
618 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
619 })
620 .collect::<Vec<_>>();
621 let footnote_refs = footnote_refs
622 .into_iter()
623 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
624 .collect::<Vec<_>>();
625 let reference_defs = reference_defs
626 .into_iter()
627 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
628 .collect::<Vec<_>>();
629 let list_blocks = list_blocks
630 .into_iter()
631 .filter(|block| {
632 !lines
633 .get(block.start_line - 1)
634 .is_some_and(|l| l.in_kramdown_extension_block)
635 })
636 .collect::<Vec<_>>();
637 let table_blocks = table_blocks
638 .into_iter()
639 .filter(|block| {
640 !lines
642 .get(block.start_line)
643 .is_some_and(|l| l.in_kramdown_extension_block)
644 })
645 .collect::<Vec<_>>();
646 let emphasis_spans = emphasis_spans
647 .into_iter()
648 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
649 .collect::<Vec<_>>();
650
651 for block in &list_blocks {
655 for line_num in block.start_line..=block.end_line {
657 if let Some(li) = lines.get_mut(line_num - 1) {
658 li.in_list_block = true;
659 }
660 }
661 }
662 for block in &table_blocks {
663 for idx in block.start_line..=block.end_line {
665 if let Some(li) = lines.get_mut(idx) {
666 li.in_table_block = true;
667 }
668 }
669 }
670
671 let reference_defs_map: HashMap<String, usize> = reference_defs
673 .iter()
674 .enumerate()
675 .map(|(idx, def)| (def.id.to_lowercase(), idx))
676 .collect();
677
678 let link_title_ranges: Vec<(usize, usize)> = reference_defs
680 .iter()
681 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
682 (Some(start), Some(end)) => Some((start, end)),
683 _ => None,
684 })
685 .collect();
686
687 let line_index = profile_section!(
689 "Line index",
690 profile,
691 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
692 content,
693 line_offsets.clone(),
694 &code_blocks,
695 )
696 );
697
698 let jinja_ranges = profile_section!(
700 "Jinja ranges",
701 profile,
702 crate::utils::jinja_utils::find_jinja_ranges(content)
703 );
704
705 let citation_ranges = profile_section!("Citation ranges", profile, {
707 if flavor.is_pandoc_compatible() {
708 crate::utils::pandoc::find_citation_ranges(content)
709 } else {
710 Vec::new()
711 }
712 });
713
714 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
716 if flavor.is_pandoc_compatible() {
717 crate::utils::pandoc::detect_inline_footnote_ranges(content)
718 } else {
719 Vec::new()
720 }
721 });
722
723 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
725 if flavor.is_pandoc_compatible() {
726 crate::utils::pandoc::collect_pandoc_header_slugs(content)
727 } else {
728 std::collections::HashSet::new()
729 }
730 });
731
732 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
734 if flavor.is_pandoc_compatible() {
735 crate::utils::pandoc::detect_example_list_marker_ranges(content)
736 } else {
737 Vec::new()
738 }
739 });
740
741 let example_reference_ranges = profile_section!("Example references", profile, {
743 if flavor.is_pandoc_compatible() {
744 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
745 } else {
746 Vec::new()
747 }
748 });
749
750 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
752 if flavor.is_pandoc_compatible() {
753 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
754 } else {
755 Vec::new()
756 }
757 });
758
759 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
761 if flavor.is_pandoc_compatible() {
762 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
763 } else {
764 Vec::new()
765 }
766 });
767
768 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
770 if flavor.is_pandoc_compatible() {
771 crate::utils::pandoc::detect_bracketed_span_ranges(content)
772 } else {
773 Vec::new()
774 }
775 });
776
777 let line_block_ranges = profile_section!("Line block ranges", profile, {
779 if flavor.is_pandoc_compatible() {
780 crate::utils::pandoc::detect_line_block_ranges(content)
781 } else {
782 Vec::new()
783 }
784 });
785
786 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
788 if flavor.is_pandoc_compatible() {
789 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
790 } else {
791 Vec::new()
792 }
793 });
794
795 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
797 if flavor.is_pandoc_compatible() {
798 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
799 } else {
800 Vec::new()
801 }
802 });
803
804 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
806 if flavor.is_pandoc_compatible() {
807 crate::utils::pandoc::detect_grid_table_ranges(content)
808 } else {
809 Vec::new()
810 }
811 });
812
813 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
815 if flavor.is_pandoc_compatible() {
816 crate::utils::pandoc::detect_multi_line_table_ranges(content)
817 } else {
818 Vec::new()
819 }
820 });
821
822 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
824 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
825 let mut ranges = Vec::new();
826 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
827 ranges.push((mat.start(), mat.end()));
828 }
829 ranges
830 });
831
832 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks);
833
834 Self {
835 content,
836 content_lines,
837 line_offsets,
838 code_blocks,
839 code_block_details,
840 strong_spans,
841 line_to_list,
842 list_start_values,
843 lines,
844 links,
845 images,
846 broken_links,
847 footnote_refs,
848 reference_defs,
849 reference_defs_map,
850 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
851 math_spans_cache: OnceLock::new(), math_byte_ranges_cache: OnceLock::new(), list_blocks,
854 char_frequency,
855 html_tags_cache: OnceLock::new(),
856 jsx_component_tags_cache: OnceLock::new(),
857 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
858 table_rows_cache: OnceLock::new(),
859 bare_urls_cache: OnceLock::new(),
860 has_mixed_list_nesting_cache: OnceLock::new(),
861 html_comment_ranges,
862 table_blocks,
863 line_index,
864 jinja_ranges,
865 flavor,
866 source_file,
867 jsx_expression_ranges,
868 mdx_comment_ranges,
869 citation_ranges,
870 pandoc_div_ranges,
871 colon_fence_ranges,
872 inline_footnote_ranges,
873 pandoc_header_slugs,
874 example_list_marker_ranges,
875 example_reference_ranges,
876 sub_super_ranges,
877 inline_code_attr_ranges,
878 bracketed_span_ranges,
879 line_block_ranges,
880 pipe_table_caption_ranges,
881 pandoc_metadata_ranges,
882 grid_table_ranges,
883 multi_line_table_ranges,
884 shortcode_ranges,
885 link_title_ranges,
886 code_span_byte_ranges: code_span_ranges,
887 inline_config,
888 obsidian_comment_ranges,
889 lazy_cont_lines_cache: OnceLock::new(),
890 myst_directive_ranges,
891 myst_comment_ranges,
892 myst_role_ranges,
893 front_matter_end,
894 }
895 }
896
897 pub fn front_matter_end_line(&self) -> usize {
902 self.front_matter_end
903 }
904
905 #[inline]
908 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
909 let idx = ranges.partition_point(|&(start, _)| start <= pos);
911 idx > 0 && pos < ranges[idx - 1].1
913 }
914
915 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
917 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
918 }
919
920 pub fn is_in_link(&self, pos: usize) -> bool {
922 let idx = self.links.partition_point(|link| link.byte_offset <= pos);
923 if idx > 0 && pos < self.links[idx - 1].byte_end {
924 return true;
925 }
926 let idx = self.images.partition_point(|img| img.byte_offset <= pos);
927 if idx > 0 && pos < self.images[idx - 1].byte_end {
928 return true;
929 }
930 self.is_in_reference_def(pos)
931 }
932
933 pub fn inline_config(&self) -> &InlineConfig {
935 &self.inline_config
936 }
937
938 pub fn colon_fence_ranges(&self) -> &[(usize, usize)] {
941 &self.colon_fence_ranges
942 }
943
944 pub fn raw_lines(&self) -> &[&'a str] {
948 &self.content_lines
949 }
950
951 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
956 self.inline_config.is_rule_disabled(rule_name, line_number)
957 }
958
959 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
961 Arc::clone(
962 self.code_spans_cache
963 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
964 )
965 }
966
967 pub fn math_byte_ranges(&self) -> &[(usize, usize)] {
971 self.math_byte_ranges_cache
972 .get_or_init(|| crate::utils::skip_context::math_byte_ranges(self.content))
973 }
974
975 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
977 Arc::clone(
978 self.math_spans_cache
979 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
980 )
981 }
982
983 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
985 let math_spans = self.math_spans();
986 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
988 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
989 }
990
991 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
993 &self.html_comment_ranges
994 }
995
996 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
1000 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
1001 }
1002
1003 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
1008 if self.obsidian_comment_ranges.is_empty() {
1009 return false;
1010 }
1011
1012 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
1014 self.is_in_obsidian_comment(byte_pos)
1015 }
1016
1017 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
1019 &self.myst_directive_ranges
1020 }
1021
1022 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
1024 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
1025 }
1026
1027 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
1029 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
1030 }
1031
1032 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
1039 if !self.flavor.supports_myst_directives() {
1040 return false;
1041 }
1042 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
1043 info.in_myst_directive
1044 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1045 })
1046 }
1047
1048 fn filter_kramdown_tags(&self, tags: Vec<HtmlTag>) -> Vec<HtmlTag> {
1050 tags.into_iter()
1051 .filter(|tag| {
1052 !self
1053 .lines
1054 .get(tag.line - 1)
1055 .is_some_and(|l| l.in_kramdown_extension_block)
1056 })
1057 .collect()
1058 }
1059
1060 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1066 Arc::clone(self.html_tags_cache.get_or_init(|| {
1067 let (html_tags, jsx_component_tags) =
1068 element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1069 let _ = self
1071 .jsx_component_tags_cache
1072 .set(Arc::new(self.filter_kramdown_tags(jsx_component_tags)));
1073 Arc::new(self.filter_kramdown_tags(html_tags))
1074 }))
1075 }
1076
1077 pub fn jsx_component_tags(&self) -> Arc<Vec<HtmlTag>> {
1080 if let Some(cached) = self.jsx_component_tags_cache.get() {
1081 return Arc::clone(cached);
1082 }
1083 let _ = self.html_tags();
1085 Arc::clone(
1086 self.jsx_component_tags_cache
1087 .get()
1088 .expect("html_tags() populates jsx_component_tags_cache"),
1089 )
1090 }
1091
1092 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1094 Arc::clone(
1095 self.emphasis_spans_cache
1096 .get()
1097 .expect("emphasis_spans_cache initialized during construction"),
1098 )
1099 }
1100
1101 pub fn table_rows(&self) -> Arc<Vec<TableRow>> {
1103 Arc::clone(
1104 self.table_rows_cache
1105 .get_or_init(|| Arc::new(element_parsers::parse_table_rows(self.content, &self.lines))),
1106 )
1107 }
1108
1109 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1111 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1112 Arc::new(element_parsers::parse_bare_urls(
1113 self.content,
1114 &self.lines,
1115 &self.code_blocks,
1116 ))
1117 }))
1118 }
1119
1120 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1122 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1123 Arc::new(element_parsers::detect_lazy_continuation_lines(
1124 self.content,
1125 &self.lines,
1126 &self.line_offsets,
1127 ))
1128 }))
1129 }
1130
1131 pub fn has_mixed_list_nesting(&self) -> bool {
1135 *self
1136 .has_mixed_list_nesting_cache
1137 .get_or_init(|| self.compute_mixed_list_nesting())
1138 }
1139
1140 fn compute_mixed_list_nesting(&self) -> bool {
1142 let mut stack: Vec<(usize, bool)> = Vec::new();
1147 let mut last_was_blank = false;
1148
1149 for line_info in &self.lines {
1150 if line_info.in_code_block
1152 || line_info.in_front_matter
1153 || line_info.in_mkdocstrings
1154 || line_info.in_html_comment
1155 || line_info.in_mdx_comment
1156 || line_info.in_esm_block
1157 {
1158 continue;
1159 }
1160
1161 if line_info.is_blank {
1163 last_was_blank = true;
1164 continue;
1165 }
1166
1167 if let Some(list_item) = &line_info.list_item {
1168 let current_pos = if list_item.marker_column == 1 {
1170 0
1171 } else {
1172 list_item.marker_column
1173 };
1174
1175 if last_was_blank && current_pos == 0 {
1177 stack.clear();
1178 }
1179 last_was_blank = false;
1180
1181 while let Some(&(pos, _)) = stack.last() {
1183 if pos >= current_pos {
1184 stack.pop();
1185 } else {
1186 break;
1187 }
1188 }
1189
1190 if let Some(&(_, parent_is_ordered)) = stack.last()
1192 && parent_is_ordered != list_item.is_ordered
1193 {
1194 return true; }
1196
1197 stack.push((current_pos, list_item.is_ordered));
1198 } else {
1199 last_was_blank = false;
1201 }
1202 }
1203
1204 false
1205 }
1206
1207 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1209 match self.line_offsets.binary_search(&offset) {
1210 Ok(line) => (line + 1, 1),
1211 Err(line) => {
1212 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1213 (line, offset - line_start + 1)
1214 }
1215 }
1216 }
1217
1218 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1220 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1222 return true;
1223 }
1224
1225 self.is_byte_offset_in_code_span(pos)
1227 }
1228
1229 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1231 if line_num > 0 {
1232 self.lines.get(line_num - 1)
1233 } else {
1234 None
1235 }
1236 }
1237
1238 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1240 let normalized_id = ref_id.to_lowercase();
1241 self.reference_defs_map
1242 .get(&normalized_id)
1243 .map(|&idx| self.reference_defs[idx].url.as_str())
1244 }
1245
1246 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1248 if line_num == 0 || line_num > self.lines.len() {
1249 return false;
1250 }
1251 self.lines[line_num - 1].in_list_block
1252 }
1253
1254 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1256 if line_num == 0 || line_num > self.lines.len() {
1257 return false;
1258 }
1259 self.lines[line_num - 1].in_html_block
1260 }
1261
1262 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1268 if line_num == 0 || line_num > self.lines.len() {
1269 return false;
1270 }
1271 self.lines[line_num - 1].in_table_block
1272 }
1273
1274 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1276 if line_num == 0 || line_num > self.lines.len() {
1277 return false;
1278 }
1279
1280 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1284 let code_spans = self.code_spans();
1285 code_spans.iter().any(|span| {
1286 if line_num < span.line || line_num > span.end_line {
1288 return false;
1289 }
1290
1291 if span.line == span.end_line {
1292 col_0indexed >= span.start_col && col_0indexed < span.end_col
1294 } else if line_num == span.line {
1295 col_0indexed >= span.start_col
1297 } else if line_num == span.end_line {
1298 col_0indexed < span.end_col
1300 } else {
1301 true
1303 }
1304 })
1305 }
1306
1307 #[inline]
1309 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1310 let code_spans = self.code_spans();
1311 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1312 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1313 }
1314
1315 #[inline]
1317 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1318 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1319 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1320 }
1321
1322 #[inline]
1324 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1325 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1326 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1327 }
1328
1329 #[inline]
1332 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1333 let tags = self.html_tags();
1334 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1335 idx > 0 && byte_pos < tags[idx - 1].byte_end
1336 }
1337
1338 #[inline]
1342 pub fn is_in_jsx_component_tag(&self, byte_pos: usize) -> bool {
1343 if !self.flavor.supports_jsx() {
1344 return false;
1345 }
1346 let tags = self.jsx_component_tags();
1347 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1348 idx > 0 && byte_pos < tags[idx - 1].byte_end
1349 }
1350
1351 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1353 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1354 }
1355
1356 #[inline]
1358 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1359 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1360 }
1361
1362 #[inline]
1364 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1365 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1366 }
1367
1368 #[inline]
1371 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1372 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1373 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1374 }
1375
1376 #[inline]
1378 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1379 &self.citation_ranges
1380 }
1381
1382 #[inline]
1385 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1386 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1387 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1388 }
1389
1390 #[inline]
1393 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1394 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1395 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1396 }
1397
1398 #[inline]
1401 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
1402 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
1403 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
1404 }
1405
1406 #[inline]
1409 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
1410 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
1411 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
1412 }
1413
1414 #[inline]
1417 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
1418 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
1419 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
1420 }
1421
1422 #[inline]
1426 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
1427 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
1428 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
1429 }
1430
1431 #[inline]
1434 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
1435 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
1436 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
1437 }
1438
1439 #[inline]
1442 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
1443 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
1444 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
1445 }
1446
1447 #[inline]
1451 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
1452 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
1453 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
1454 }
1455
1456 #[inline]
1459 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
1460 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
1461 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
1462 }
1463
1464 #[inline]
1467 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
1468 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
1469 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
1470 }
1471
1472 #[inline]
1475 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
1476 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
1477 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
1478 }
1479
1480 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
1485 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
1486 self.pandoc_header_slugs.contains(&slug)
1487 }
1488
1489 #[inline]
1495 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
1496 self.pandoc_header_slugs.contains(slug)
1497 }
1498
1499 #[inline]
1501 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
1502 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
1503 }
1504
1505 #[inline]
1507 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
1508 &self.shortcode_ranges
1509 }
1510
1511 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
1513 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
1514 }
1515
1516 pub fn has_char(&self, ch: char) -> bool {
1518 match ch {
1519 '#' => self.char_frequency.hash_count > 0,
1520 '*' => self.char_frequency.asterisk_count > 0,
1521 '_' => self.char_frequency.underscore_count > 0,
1522 '-' => self.char_frequency.hyphen_count > 0,
1523 '+' => self.char_frequency.plus_count > 0,
1524 '>' => self.char_frequency.gt_count > 0,
1525 '|' => self.char_frequency.pipe_count > 0,
1526 '[' => self.char_frequency.bracket_count > 0,
1527 '`' => self.char_frequency.backtick_count > 0,
1528 '<' => self.char_frequency.lt_count > 0,
1529 '!' => self.char_frequency.exclamation_count > 0,
1530 '\n' => self.char_frequency.newline_count > 0,
1531 _ => self.content.contains(ch), }
1533 }
1534
1535 pub fn char_count(&self, ch: char) -> usize {
1537 match ch {
1538 '#' => self.char_frequency.hash_count,
1539 '*' => self.char_frequency.asterisk_count,
1540 '_' => self.char_frequency.underscore_count,
1541 '-' => self.char_frequency.hyphen_count,
1542 '+' => self.char_frequency.plus_count,
1543 '>' => self.char_frequency.gt_count,
1544 '|' => self.char_frequency.pipe_count,
1545 '[' => self.char_frequency.bracket_count,
1546 '`' => self.char_frequency.backtick_count,
1547 '<' => self.char_frequency.lt_count,
1548 '!' => self.char_frequency.exclamation_count,
1549 '\n' => self.char_frequency.newline_count,
1550 _ => self.content.matches(ch).count(), }
1552 }
1553
1554 pub fn likely_has_headings(&self) -> bool {
1556 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 2 || self.content.contains('=') }
1558
1559 pub fn likely_has_lists(&self) -> bool {
1561 self.char_frequency.asterisk_count > 0
1562 || self.char_frequency.hyphen_count > 0
1563 || self.char_frequency.plus_count > 0
1564 }
1565
1566 pub fn likely_has_emphasis(&self) -> bool {
1568 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
1569 }
1570
1571 pub fn likely_has_tables(&self) -> bool {
1573 self.char_frequency.pipe_count > 2
1574 }
1575
1576 pub fn likely_has_blockquotes(&self) -> bool {
1578 self.char_frequency.gt_count > 0
1579 }
1580
1581 pub fn likely_has_code(&self) -> bool {
1583 self.char_frequency.backtick_count > 0
1584 }
1585
1586 pub fn likely_has_links_or_images(&self) -> bool {
1588 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
1589 }
1590
1591 pub fn likely_has_html(&self) -> bool {
1593 self.char_frequency.lt_count > 0
1594 }
1595
1596 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
1601 if let Some(line_info) = self.lines.get(line_idx)
1602 && let Some(ref bq) = line_info.blockquote
1603 {
1604 bq.prefix.trim_end().to_string()
1605 } else {
1606 String::new()
1607 }
1608 }
1609
1610 #[inline]
1616 fn find_line_for_offset(lines: &[LineInfo], byte_offset: usize) -> (usize, usize, usize) {
1617 let idx = match lines.binary_search_by(|line| {
1619 if byte_offset < line.byte_offset {
1620 std::cmp::Ordering::Greater
1621 } else if byte_offset > line.byte_offset + line.byte_len {
1622 std::cmp::Ordering::Less
1623 } else {
1624 std::cmp::Ordering::Equal
1625 }
1626 }) {
1627 Ok(idx) => idx,
1628 Err(idx) => idx.saturating_sub(1),
1629 };
1630
1631 let line = &lines[idx];
1632 let line_num = idx + 1;
1633 let col = byte_offset.saturating_sub(line.byte_offset);
1634
1635 (idx, line_num, col)
1636 }
1637
1638 #[inline]
1640 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
1641 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
1643
1644 if idx > 0 {
1646 let span = &code_spans[idx - 1];
1647 if offset >= span.byte_offset && offset < span.byte_end {
1648 return true;
1649 }
1650 }
1651
1652 false
1653 }
1654
1655 #[must_use]
1675 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
1676 ValidHeadingsIter::new(&self.lines)
1677 }
1678
1679 #[must_use]
1683 pub fn has_valid_headings(&self) -> bool {
1684 self.lines
1685 .iter()
1686 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
1687 }
1688}
1689
1690fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
1699 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
1700
1701 let options = crate::utils::rumdl_parser_options();
1702 let parser = Parser::new_ext(content, options).into_offset_iter();
1703
1704 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
1706 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
1707 let mut in_footnote = false;
1708
1709 for (event, range) in parser {
1710 match event {
1711 Event::Start(Tag::FootnoteDefinition(_)) => {
1712 in_footnote = true;
1713 footnote_ranges.push((range.start, range.end));
1714 }
1715 Event::End(TagEnd::FootnoteDefinition) => {
1716 in_footnote = false;
1717 }
1718 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
1719 fenced_code_ranges.push((range.start, range.end));
1720 }
1721 _ => {}
1722 }
1723 }
1724
1725 let byte_to_line = |byte_offset: usize| -> usize {
1726 line_offsets
1727 .partition_point(|&offset| offset <= byte_offset)
1728 .saturating_sub(1)
1729 };
1730
1731 for &(start, end) in &footnote_ranges {
1733 let start_line = byte_to_line(start);
1734 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1735
1736 for line in &mut lines[start_line..end_line] {
1737 line.in_footnote_definition = true;
1738 line.in_code_block = false;
1739 }
1740 }
1741
1742 for &(start, end) in &fenced_code_ranges {
1744 let start_line = byte_to_line(start);
1745 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1746
1747 for line in &mut lines[start_line..end_line] {
1748 line.in_code_block = true;
1749 }
1750 }
1751}