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>>>, pub list_blocks: Vec<ListBlock>, pub char_frequency: CharFrequency, html_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)>, }
113
114impl<'a> LintContext<'a> {
115 pub fn new(content: &'a str, flavor: MarkdownFlavor, source_file: Option<PathBuf>) -> Self {
116 #[cfg(not(target_arch = "wasm32"))]
117 let profile = std::env::var("RUMDL_PROFILE_QUADRATIC").is_ok();
118
119 let line_offsets = profile_section!("Line offsets", profile, {
120 let mut offsets = vec![0];
121 for (i, c) in content.char_indices() {
122 if c == '\n' {
123 offsets.push(i + 1);
124 }
125 }
126 offsets
127 });
128
129 let content_lines: Vec<&str> = content.lines().collect();
131
132 let front_matter_end = FrontMatterUtils::get_front_matter_end_line(content);
134
135 let parse_result = profile_section!(
137 "Code blocks",
138 profile,
139 CodeBlockUtils::detect_code_blocks_and_spans(content)
140 );
141 let mut code_blocks = parse_result.code_blocks;
142 let code_span_ranges = parse_result.code_spans;
143 let code_block_details = parse_result.code_block_details;
144 let strong_spans = parse_result.strong_spans;
145 let line_to_list = parse_result.line_to_list;
146 let list_start_values = parse_result.list_start_values;
147
148 let html_comment_ranges = profile_section!(
150 "HTML comment ranges",
151 profile,
152 crate::utils::skip_context::compute_html_comment_ranges(content)
153 );
154
155 let autodoc_ranges = profile_section!("Autodoc block ranges", profile, {
159 if flavor.supports_colon_code_fences() || flavor.supports_myst_directives() {
160 Vec::new()
161 } else {
162 crate::utils::mkdocstrings_refs::detect_autodoc_block_ranges(content)
163 }
164 });
165
166 let pandoc_div_ranges = profile_section!("Pandoc div ranges", profile, {
168 if flavor.is_pandoc_compatible() {
169 crate::utils::pandoc::detect_div_block_ranges(content)
170 } else {
171 Vec::new()
172 }
173 });
174
175 let pymdown_block_ranges = profile_section!("PyMdown block ranges", profile, {
177 if flavor == MarkdownFlavor::MkDocs {
178 crate::utils::pymdown_blocks::detect_block_ranges(content)
179 } else {
180 Vec::new()
181 }
182 });
183
184 let skip_ranges = SkipByteRanges {
187 html_comment_ranges: &html_comment_ranges,
188 autodoc_ranges: &autodoc_ranges,
189 pandoc_div_ranges: &pandoc_div_ranges,
190 pymdown_block_ranges: &pymdown_block_ranges,
191 };
192 let (mut lines, emphasis_spans) = profile_section!(
193 "Basic line info",
194 profile,
195 line_computation::compute_basic_line_info(
196 content,
197 &content_lines,
198 &line_offsets,
199 &code_blocks,
200 flavor,
201 &skip_ranges,
202 front_matter_end,
203 )
204 );
205
206 profile_section!(
208 "HTML blocks",
209 profile,
210 heading_detection::detect_html_blocks(content, &mut lines)
211 );
212
213 profile_section!(
215 "ESM blocks",
216 profile,
217 flavor_detection::detect_esm_blocks(content, &mut lines, flavor)
218 );
219
220 profile_section!(
222 "JSX block detection",
223 profile,
224 flavor_detection::detect_jsx_blocks(content, &mut lines, flavor)
225 );
226
227 let (jsx_expression_ranges, mdx_comment_ranges) = profile_section!(
229 "JSX/MDX detection",
230 profile,
231 flavor_detection::detect_jsx_and_mdx_comments(content, &mut lines, flavor, &code_blocks)
232 );
233
234 profile_section!(
239 "Markdown-in-HTML blocks",
240 profile,
241 flavor_detection::detect_markdown_html_blocks(&content_lines, &mut lines)
242 );
243
244 profile_section!(
246 "MkDocs constructs",
247 profile,
248 flavor_detection::detect_mkdocs_line_info(&content_lines, &mut lines, flavor)
249 );
250
251 profile_section!(
256 "Footnote definitions",
257 profile,
258 detect_footnote_definitions(content, &mut lines, &line_offsets)
259 );
260
261 {
264 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
265 for &(start, end) in &code_blocks {
266 let start_line = line_offsets
267 .partition_point(|&offset| offset <= start)
268 .saturating_sub(1);
269 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
270
271 let mut sub_start: Option<usize> = None;
272 for (i, &offset) in line_offsets[start_line..end_line]
273 .iter()
274 .enumerate()
275 .map(|(j, o)| (j + start_line, o))
276 {
277 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
278 if is_real_code && sub_start.is_none() {
279 let byte_start = if i == start_line { start } else { offset };
280 sub_start = Some(byte_start);
281 } else if !is_real_code && sub_start.is_some() {
282 new_code_blocks.push((sub_start.unwrap(), offset));
283 sub_start = None;
284 }
285 }
286 if let Some(s) = sub_start {
287 new_code_blocks.push((s, end));
288 }
289 }
290 code_blocks = new_code_blocks;
291 }
292
293 let has_markdown_html = lines.iter().any(|l| l.in_mkdocs_html_markdown);
301 if flavor == MarkdownFlavor::MkDocs || has_markdown_html {
302 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
303 for &(start, end) in &code_blocks {
304 let start_line = line_offsets
305 .partition_point(|&offset| offset <= start)
306 .saturating_sub(1);
307 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
308
309 let mut sub_start: Option<usize> = None;
311 for (i, &offset) in line_offsets[start_line..end_line]
312 .iter()
313 .enumerate()
314 .map(|(j, o)| (j + start_line, o))
315 {
316 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
317 if is_real_code && sub_start.is_none() {
318 let byte_start = if i == start_line { start } else { offset };
319 sub_start = Some(byte_start);
320 } else if !is_real_code && sub_start.is_some() {
321 new_code_blocks.push((sub_start.unwrap(), offset));
322 sub_start = None;
323 }
324 }
325 if let Some(s) = sub_start {
326 new_code_blocks.push((s, end));
327 }
328 }
329 code_blocks = new_code_blocks;
330 }
331
332 if flavor.supports_jsx() {
336 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
337 for &(start, end) in &code_blocks {
338 let start_line = line_offsets
339 .partition_point(|&offset| offset <= start)
340 .saturating_sub(1);
341 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
342
343 let mut sub_start: Option<usize> = None;
344 for (i, &offset) in line_offsets[start_line..end_line]
345 .iter()
346 .enumerate()
347 .map(|(j, o)| (j + start_line, o))
348 {
349 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
350 if is_real_code && sub_start.is_none() {
351 let byte_start = if i == start_line { start } else { offset };
352 sub_start = Some(byte_start);
353 } else if !is_real_code && sub_start.is_some() {
354 new_code_blocks.push((sub_start.unwrap(), offset));
355 sub_start = None;
356 }
357 }
358 if let Some(s) = sub_start {
359 new_code_blocks.push((s, end));
360 }
361 }
362 code_blocks = new_code_blocks;
363 }
364
365 let colon_fence_ranges = profile_section!(
368 "Azure colon fence detection",
369 profile,
370 flavor_detection::detect_azure_colon_fences(content, &mut lines, flavor)
371 );
372 if !colon_fence_ranges.is_empty() {
373 code_blocks.extend(colon_fence_ranges.iter().copied());
374 code_blocks.sort_by_key(|&(start, _)| start);
375 }
376
377 let myst_directive_ranges = profile_section!(
380 "MyST colon directives",
381 profile,
382 flavor_detection::detect_myst_colon_directives(content, &mut lines, flavor)
383 );
384
385 let myst_comment_ranges = profile_section!(
387 "MyST comments",
388 profile,
389 flavor_detection::detect_myst_comments(content, &mut lines, flavor)
390 );
391
392 profile_section!(
395 "MyST backtick directives",
396 profile,
397 flavor_detection::detect_myst_backtick_directives(
398 content,
399 &mut lines,
400 flavor,
401 &code_block_details,
402 &line_offsets
403 )
404 );
405
406 if flavor.supports_myst_directives() {
409 let mut new_code_blocks = Vec::with_capacity(code_blocks.len());
410 for &(start, end) in &code_blocks {
411 let start_line = line_offsets
412 .partition_point(|&offset| offset <= start)
413 .saturating_sub(1);
414 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
415
416 let mut sub_start: Option<usize> = None;
417 for (i, &offset) in line_offsets[start_line..end_line]
418 .iter()
419 .enumerate()
420 .map(|(j, o)| (j + start_line, o))
421 {
422 let is_real_code = lines.get(i).is_some_and(|info| info.in_code_block);
423 if is_real_code && sub_start.is_none() {
424 let byte_start = if i == start_line { start } else { offset };
425 sub_start = Some(byte_start);
426 } else if !is_real_code && sub_start.is_some() {
427 new_code_blocks.push((sub_start.unwrap(), offset));
428 sub_start = None;
429 }
430 }
431 if let Some(s) = sub_start {
432 new_code_blocks.push((s, end));
433 }
434 }
435 code_blocks = new_code_blocks;
436 }
437
438 profile_section!(
440 "Kramdown constructs",
441 profile,
442 flavor_detection::detect_kramdown_line_info(content, &mut lines, flavor)
443 );
444
445 for line in &mut lines {
450 if line.in_kramdown_extension_block {
451 line.list_item = None;
452 line.is_horizontal_rule = false;
453 line.blockquote = None;
454 line.is_kramdown_block_ial = false;
455 }
456 }
457
458 let obsidian_comment_ranges = profile_section!(
460 "Obsidian comments",
461 profile,
462 flavor_detection::detect_obsidian_comments(content, &mut lines, flavor, &code_span_ranges)
463 );
464
465 let myst_role_ranges = profile_section!(
467 "MyST roles",
468 profile,
469 flavor_detection::detect_myst_role_ranges(content, &lines, flavor, &code_blocks)
470 );
471
472 let pulldown_result = profile_section!(
476 "Links, images & link ranges",
477 profile,
478 link_parser::parse_links_images_pulldown(content, &lines, &code_blocks, flavor, &html_comment_ranges)
479 );
480
481 profile_section!(
483 "Headings & blockquotes",
484 profile,
485 heading_detection::detect_headings_and_blockquotes(
486 &content_lines,
487 &mut lines,
488 flavor,
489 &html_comment_ranges,
490 &pulldown_result.link_byte_ranges,
491 front_matter_end,
492 )
493 );
494
495 for line in &mut lines {
497 if line.in_kramdown_extension_block {
498 line.heading = None;
499 }
500 }
501
502 let mut code_spans = profile_section!(
504 "Code spans",
505 profile,
506 element_parsers::build_code_spans_from_ranges(content, &lines, &code_span_ranges)
507 );
508
509 if flavor == MarkdownFlavor::MkDocs {
513 let extra = profile_section!(
514 "MkDocs code spans",
515 profile,
516 element_parsers::scan_mkdocs_container_code_spans(content, &lines, &code_span_ranges,)
517 );
518 if !extra.is_empty() {
519 code_spans.extend(extra);
520 code_spans.sort_by_key(|span| span.byte_offset);
521 }
522 }
523
524 if flavor == MarkdownFlavor::MDX {
529 let extra = profile_section!(
530 "MDX JSX code spans",
531 profile,
532 element_parsers::scan_jsx_block_code_spans(content, &lines, &code_span_ranges)
533 );
534 if !extra.is_empty() {
535 code_spans.extend(extra);
536 code_spans.sort_by_key(|span| span.byte_offset);
537 }
538 }
539
540 for span in &code_spans {
543 if span.end_line > span.line {
544 for line_num in (span.line + 1)..=span.end_line {
546 if let Some(line_info) = lines.get_mut(line_num - 1) {
547 line_info.in_code_span_continuation = true;
548 }
549 }
550 }
551 }
552
553 let (links, images, broken_links, footnote_refs) = profile_section!(
555 "Links & images finalize",
556 profile,
557 link_parser::finalize_links_and_images(
558 content,
559 &lines,
560 &code_blocks,
561 &code_spans,
562 flavor,
563 &html_comment_ranges,
564 pulldown_result
565 )
566 );
567
568 let reference_defs = profile_section!(
569 "Reference defs",
570 profile,
571 link_parser::parse_reference_defs(content, &lines)
572 );
573
574 let list_blocks = profile_section!("List blocks", profile, list_blocks::parse_list_blocks(content, &lines));
575
576 let char_frequency = profile_section!(
578 "Char frequency",
579 profile,
580 line_computation::compute_char_frequency(content)
581 );
582
583 let table_blocks = profile_section!(
585 "Table blocks",
586 profile,
587 crate::utils::table_utils::TableUtils::find_table_blocks_with_code_info(
588 content,
589 &code_blocks,
590 &code_spans,
591 &html_comment_ranges,
592 )
593 );
594
595 let links = links
598 .into_iter()
599 .filter(|link| !lines.get(link.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
600 .collect::<Vec<_>>();
601 let images = images
602 .into_iter()
603 .filter(|img| !lines.get(img.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
604 .collect::<Vec<_>>();
605 let broken_links = broken_links
606 .into_iter()
607 .filter(|bl| {
608 let line_idx = line_offsets
610 .partition_point(|&offset| offset <= bl.span.start)
611 .saturating_sub(1);
612 !lines.get(line_idx).is_some_and(|l| l.in_kramdown_extension_block)
613 })
614 .collect::<Vec<_>>();
615 let footnote_refs = footnote_refs
616 .into_iter()
617 .filter(|fr| !lines.get(fr.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
618 .collect::<Vec<_>>();
619 let reference_defs = reference_defs
620 .into_iter()
621 .filter(|def| !lines.get(def.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
622 .collect::<Vec<_>>();
623 let list_blocks = list_blocks
624 .into_iter()
625 .filter(|block| {
626 !lines
627 .get(block.start_line - 1)
628 .is_some_and(|l| l.in_kramdown_extension_block)
629 })
630 .collect::<Vec<_>>();
631 let table_blocks = table_blocks
632 .into_iter()
633 .filter(|block| {
634 !lines
636 .get(block.start_line)
637 .is_some_and(|l| l.in_kramdown_extension_block)
638 })
639 .collect::<Vec<_>>();
640 let emphasis_spans = emphasis_spans
641 .into_iter()
642 .filter(|span| !lines.get(span.line - 1).is_some_and(|l| l.in_kramdown_extension_block))
643 .collect::<Vec<_>>();
644
645 let reference_defs_map: HashMap<String, usize> = reference_defs
647 .iter()
648 .enumerate()
649 .map(|(idx, def)| (def.id.to_lowercase(), idx))
650 .collect();
651
652 let link_title_ranges: Vec<(usize, usize)> = reference_defs
654 .iter()
655 .filter_map(|def| match (def.title_byte_start, def.title_byte_end) {
656 (Some(start), Some(end)) => Some((start, end)),
657 _ => None,
658 })
659 .collect();
660
661 let line_index = profile_section!(
663 "Line index",
664 profile,
665 crate::utils::range_utils::LineIndex::with_line_starts_and_code_blocks(
666 content,
667 line_offsets.clone(),
668 &code_blocks,
669 )
670 );
671
672 let jinja_ranges = profile_section!(
674 "Jinja ranges",
675 profile,
676 crate::utils::jinja_utils::find_jinja_ranges(content)
677 );
678
679 let citation_ranges = profile_section!("Citation ranges", profile, {
681 if flavor.is_pandoc_compatible() {
682 crate::utils::pandoc::find_citation_ranges(content)
683 } else {
684 Vec::new()
685 }
686 });
687
688 let inline_footnote_ranges = profile_section!("Inline footnote ranges", profile, {
690 if flavor.is_pandoc_compatible() {
691 crate::utils::pandoc::detect_inline_footnote_ranges(content)
692 } else {
693 Vec::new()
694 }
695 });
696
697 let pandoc_header_slugs = profile_section!("Pandoc header slugs", profile, {
699 if flavor.is_pandoc_compatible() {
700 crate::utils::pandoc::collect_pandoc_header_slugs(content)
701 } else {
702 std::collections::HashSet::new()
703 }
704 });
705
706 let example_list_marker_ranges = profile_section!("Example list markers", profile, {
708 if flavor.is_pandoc_compatible() {
709 crate::utils::pandoc::detect_example_list_marker_ranges(content)
710 } else {
711 Vec::new()
712 }
713 });
714
715 let example_reference_ranges = profile_section!("Example references", profile, {
717 if flavor.is_pandoc_compatible() {
718 crate::utils::pandoc::detect_example_reference_ranges(content, &example_list_marker_ranges)
719 } else {
720 Vec::new()
721 }
722 });
723
724 let sub_super_ranges = profile_section!("Subscript/superscript ranges", profile, {
726 if flavor.is_pandoc_compatible() {
727 crate::utils::pandoc::detect_subscript_superscript_ranges(content)
728 } else {
729 Vec::new()
730 }
731 });
732
733 let inline_code_attr_ranges = profile_section!("Inline code attribute ranges", profile, {
735 if flavor.is_pandoc_compatible() {
736 crate::utils::pandoc::detect_inline_code_attr_ranges(content)
737 } else {
738 Vec::new()
739 }
740 });
741
742 let bracketed_span_ranges = profile_section!("Bracketed span ranges", profile, {
744 if flavor.is_pandoc_compatible() {
745 crate::utils::pandoc::detect_bracketed_span_ranges(content)
746 } else {
747 Vec::new()
748 }
749 });
750
751 let line_block_ranges = profile_section!("Line block ranges", profile, {
753 if flavor.is_pandoc_compatible() {
754 crate::utils::pandoc::detect_line_block_ranges(content)
755 } else {
756 Vec::new()
757 }
758 });
759
760 let pipe_table_caption_ranges = profile_section!("Pipe-table caption ranges", profile, {
762 if flavor.is_pandoc_compatible() {
763 crate::utils::pandoc::detect_pipe_table_caption_ranges(content)
764 } else {
765 Vec::new()
766 }
767 });
768
769 let pandoc_metadata_ranges = profile_section!("Pandoc metadata ranges", profile, {
771 if flavor.is_pandoc_compatible() {
772 crate::utils::pandoc::detect_yaml_metadata_block_ranges(content)
773 } else {
774 Vec::new()
775 }
776 });
777
778 let grid_table_ranges = profile_section!("Grid table ranges", profile, {
780 if flavor.is_pandoc_compatible() {
781 crate::utils::pandoc::detect_grid_table_ranges(content)
782 } else {
783 Vec::new()
784 }
785 });
786
787 let multi_line_table_ranges = profile_section!("Multi-line table ranges", profile, {
789 if flavor.is_pandoc_compatible() {
790 crate::utils::pandoc::detect_multi_line_table_ranges(content)
791 } else {
792 Vec::new()
793 }
794 });
795
796 let shortcode_ranges = profile_section!("Shortcode ranges", profile, {
798 use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
799 let mut ranges = Vec::new();
800 for mat in HUGO_SHORTCODE_REGEX.find_iter(content) {
801 ranges.push((mat.start(), mat.end()));
802 }
803 ranges
804 });
805
806 let inline_config = InlineConfig::from_content_with_code_blocks(content, &code_blocks);
807
808 Self {
809 content,
810 content_lines,
811 line_offsets,
812 code_blocks,
813 code_block_details,
814 strong_spans,
815 line_to_list,
816 list_start_values,
817 lines,
818 links,
819 images,
820 broken_links,
821 footnote_refs,
822 reference_defs,
823 reference_defs_map,
824 code_spans_cache: OnceLock::from(Arc::new(code_spans)),
825 math_spans_cache: OnceLock::new(), list_blocks,
827 char_frequency,
828 html_tags_cache: OnceLock::new(),
829 emphasis_spans_cache: OnceLock::from(Arc::new(emphasis_spans)),
830 table_rows_cache: OnceLock::new(),
831 bare_urls_cache: OnceLock::new(),
832 has_mixed_list_nesting_cache: OnceLock::new(),
833 html_comment_ranges,
834 table_blocks,
835 line_index,
836 jinja_ranges,
837 flavor,
838 source_file,
839 jsx_expression_ranges,
840 mdx_comment_ranges,
841 citation_ranges,
842 pandoc_div_ranges,
843 colon_fence_ranges,
844 inline_footnote_ranges,
845 pandoc_header_slugs,
846 example_list_marker_ranges,
847 example_reference_ranges,
848 sub_super_ranges,
849 inline_code_attr_ranges,
850 bracketed_span_ranges,
851 line_block_ranges,
852 pipe_table_caption_ranges,
853 pandoc_metadata_ranges,
854 grid_table_ranges,
855 multi_line_table_ranges,
856 shortcode_ranges,
857 link_title_ranges,
858 code_span_byte_ranges: code_span_ranges,
859 inline_config,
860 obsidian_comment_ranges,
861 lazy_cont_lines_cache: OnceLock::new(),
862 myst_directive_ranges,
863 myst_comment_ranges,
864 myst_role_ranges,
865 }
866 }
867
868 #[inline]
871 fn binary_search_ranges(ranges: &[(usize, usize)], pos: usize) -> bool {
872 let idx = ranges.partition_point(|&(start, _)| start <= pos);
874 idx > 0 && pos < ranges[idx - 1].1
876 }
877
878 pub fn is_in_code_span_byte(&self, pos: usize) -> bool {
880 Self::binary_search_ranges(&self.code_span_byte_ranges, pos)
881 }
882
883 pub fn is_in_link(&self, pos: usize) -> bool {
885 let idx = self.links.partition_point(|link| link.byte_offset <= pos);
886 if idx > 0 && pos < self.links[idx - 1].byte_end {
887 return true;
888 }
889 let idx = self.images.partition_point(|img| img.byte_offset <= pos);
890 if idx > 0 && pos < self.images[idx - 1].byte_end {
891 return true;
892 }
893 self.is_in_reference_def(pos)
894 }
895
896 pub fn inline_config(&self) -> &InlineConfig {
898 &self.inline_config
899 }
900
901 pub fn colon_fence_ranges(&self) -> &[(usize, usize)] {
904 &self.colon_fence_ranges
905 }
906
907 pub fn raw_lines(&self) -> &[&'a str] {
911 &self.content_lines
912 }
913
914 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
919 self.inline_config.is_rule_disabled(rule_name, line_number)
920 }
921
922 pub fn code_spans(&self) -> Arc<Vec<CodeSpan>> {
924 Arc::clone(
925 self.code_spans_cache
926 .get_or_init(|| Arc::new(element_parsers::parse_code_spans(self.content, &self.lines))),
927 )
928 }
929
930 pub fn math_spans(&self) -> Arc<Vec<MathSpan>> {
932 Arc::clone(
933 self.math_spans_cache
934 .get_or_init(|| Arc::new(element_parsers::parse_math_spans(self.content, &self.lines))),
935 )
936 }
937
938 pub fn is_in_math_span(&self, byte_pos: usize) -> bool {
940 let math_spans = self.math_spans();
941 let idx = math_spans.partition_point(|span| span.byte_offset <= byte_pos);
943 idx > 0 && byte_pos < math_spans[idx - 1].byte_end
944 }
945
946 pub fn html_comment_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
948 &self.html_comment_ranges
949 }
950
951 pub fn is_in_obsidian_comment(&self, byte_pos: usize) -> bool {
955 Self::binary_search_ranges(&self.obsidian_comment_ranges, byte_pos)
956 }
957
958 pub fn is_position_in_obsidian_comment(&self, line_num: usize, col: usize) -> bool {
963 if self.obsidian_comment_ranges.is_empty() {
964 return false;
965 }
966
967 let byte_pos = self.line_index.line_col_to_byte_range(line_num, col).start;
969 self.is_in_obsidian_comment(byte_pos)
970 }
971
972 pub fn myst_directive_ranges(&self) -> &[(usize, usize)] {
974 &self.myst_directive_ranges
975 }
976
977 pub fn is_in_myst_role(&self, byte_pos: usize) -> bool {
979 Self::binary_search_ranges(&self.myst_role_ranges, byte_pos)
980 }
981
982 pub fn is_in_myst_comment(&self, byte_pos: usize) -> bool {
984 Self::binary_search_ranges(&self.myst_comment_ranges, byte_pos)
985 }
986
987 pub fn is_myst_colon_directive_opener_line(&self, line_num: usize) -> bool {
994 if !self.flavor.supports_myst_directives() {
995 return false;
996 }
997 self.lines.get(line_num.wrapping_sub(1)).is_some_and(|info| {
998 info.in_myst_directive
999 && flavor_detection::myst_colon_directive_opener(info.content(self.content)).is_some()
1000 })
1001 }
1002
1003 pub fn html_tags(&self) -> Arc<Vec<HtmlTag>> {
1005 Arc::clone(self.html_tags_cache.get_or_init(|| {
1006 let tags = element_parsers::parse_html_tags(self.content, &self.lines, &self.code_blocks, self.flavor);
1007 Arc::new(
1009 tags.into_iter()
1010 .filter(|tag| {
1011 !self
1012 .lines
1013 .get(tag.line - 1)
1014 .is_some_and(|l| l.in_kramdown_extension_block)
1015 })
1016 .collect(),
1017 )
1018 }))
1019 }
1020
1021 pub fn emphasis_spans(&self) -> Arc<Vec<EmphasisSpan>> {
1023 Arc::clone(
1024 self.emphasis_spans_cache
1025 .get()
1026 .expect("emphasis_spans_cache initialized during construction"),
1027 )
1028 }
1029
1030 pub fn table_rows(&self) -> Arc<Vec<TableRow>> {
1032 Arc::clone(
1033 self.table_rows_cache
1034 .get_or_init(|| Arc::new(element_parsers::parse_table_rows(self.content, &self.lines))),
1035 )
1036 }
1037
1038 pub fn bare_urls(&self) -> Arc<Vec<BareUrl>> {
1040 Arc::clone(self.bare_urls_cache.get_or_init(|| {
1041 Arc::new(element_parsers::parse_bare_urls(
1042 self.content,
1043 &self.lines,
1044 &self.code_blocks,
1045 ))
1046 }))
1047 }
1048
1049 pub fn lazy_continuation_lines(&self) -> Arc<Vec<LazyContLine>> {
1051 Arc::clone(self.lazy_cont_lines_cache.get_or_init(|| {
1052 Arc::new(element_parsers::detect_lazy_continuation_lines(
1053 self.content,
1054 &self.lines,
1055 &self.line_offsets,
1056 ))
1057 }))
1058 }
1059
1060 pub fn has_mixed_list_nesting(&self) -> bool {
1064 *self
1065 .has_mixed_list_nesting_cache
1066 .get_or_init(|| self.compute_mixed_list_nesting())
1067 }
1068
1069 fn compute_mixed_list_nesting(&self) -> bool {
1071 let mut stack: Vec<(usize, bool)> = Vec::new();
1076 let mut last_was_blank = false;
1077
1078 for line_info in &self.lines {
1079 if line_info.in_code_block
1081 || line_info.in_front_matter
1082 || line_info.in_mkdocstrings
1083 || line_info.in_html_comment
1084 || line_info.in_mdx_comment
1085 || line_info.in_esm_block
1086 {
1087 continue;
1088 }
1089
1090 if line_info.is_blank {
1092 last_was_blank = true;
1093 continue;
1094 }
1095
1096 if let Some(list_item) = &line_info.list_item {
1097 let current_pos = if list_item.marker_column == 1 {
1099 0
1100 } else {
1101 list_item.marker_column
1102 };
1103
1104 if last_was_blank && current_pos == 0 {
1106 stack.clear();
1107 }
1108 last_was_blank = false;
1109
1110 while let Some(&(pos, _)) = stack.last() {
1112 if pos >= current_pos {
1113 stack.pop();
1114 } else {
1115 break;
1116 }
1117 }
1118
1119 if let Some(&(_, parent_is_ordered)) = stack.last()
1121 && parent_is_ordered != list_item.is_ordered
1122 {
1123 return true; }
1125
1126 stack.push((current_pos, list_item.is_ordered));
1127 } else {
1128 last_was_blank = false;
1130 }
1131 }
1132
1133 false
1134 }
1135
1136 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
1138 match self.line_offsets.binary_search(&offset) {
1139 Ok(line) => (line + 1, 1),
1140 Err(line) => {
1141 let line_start = self.line_offsets.get(line.wrapping_sub(1)).copied().unwrap_or(0);
1142 (line, offset - line_start + 1)
1143 }
1144 }
1145 }
1146
1147 pub fn is_in_code_block_or_span(&self, pos: usize) -> bool {
1149 if CodeBlockUtils::is_in_code_block_or_span(&self.code_blocks, pos) {
1151 return true;
1152 }
1153
1154 self.is_byte_offset_in_code_span(pos)
1156 }
1157
1158 pub fn line_info(&self, line_num: usize) -> Option<&LineInfo> {
1160 if line_num > 0 {
1161 self.lines.get(line_num - 1)
1162 } else {
1163 None
1164 }
1165 }
1166
1167 pub fn get_reference_url(&self, ref_id: &str) -> Option<&str> {
1169 let normalized_id = ref_id.to_lowercase();
1170 self.reference_defs_map
1171 .get(&normalized_id)
1172 .map(|&idx| self.reference_defs[idx].url.as_str())
1173 }
1174
1175 pub fn is_in_list_block(&self, line_num: usize) -> bool {
1177 self.list_blocks
1178 .iter()
1179 .any(|block| line_num >= block.start_line && line_num <= block.end_line)
1180 }
1181
1182 pub fn is_in_html_block(&self, line_num: usize) -> bool {
1184 if line_num == 0 || line_num > self.lines.len() {
1185 return false;
1186 }
1187 self.lines[line_num - 1].in_html_block
1188 }
1189
1190 pub fn is_in_table_block(&self, line_num: usize) -> bool {
1196 if line_num == 0 {
1197 return false;
1198 }
1199 let line_idx = line_num - 1;
1200 self.table_blocks
1201 .iter()
1202 .any(|block| line_idx >= block.start_line && line_idx <= block.end_line)
1203 }
1204
1205 pub fn is_in_code_span(&self, line_num: usize, col: usize) -> bool {
1207 if line_num == 0 || line_num > self.lines.len() {
1208 return false;
1209 }
1210
1211 let col_0indexed = if col > 0 { col - 1 } else { 0 };
1215 let code_spans = self.code_spans();
1216 code_spans.iter().any(|span| {
1217 if line_num < span.line || line_num > span.end_line {
1219 return false;
1220 }
1221
1222 if span.line == span.end_line {
1223 col_0indexed >= span.start_col && col_0indexed < span.end_col
1225 } else if line_num == span.line {
1226 col_0indexed >= span.start_col
1228 } else if line_num == span.end_line {
1229 col_0indexed < span.end_col
1231 } else {
1232 true
1234 }
1235 })
1236 }
1237
1238 #[inline]
1240 pub fn is_byte_offset_in_code_span(&self, byte_offset: usize) -> bool {
1241 let code_spans = self.code_spans();
1242 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_offset);
1243 idx > 0 && byte_offset < code_spans[idx - 1].byte_end
1244 }
1245
1246 #[inline]
1248 pub fn is_in_reference_def(&self, byte_pos: usize) -> bool {
1249 let idx = self.reference_defs.partition_point(|rd| rd.byte_offset <= byte_pos);
1250 idx > 0 && byte_pos < self.reference_defs[idx - 1].byte_end
1251 }
1252
1253 #[inline]
1255 pub fn is_in_html_comment(&self, byte_pos: usize) -> bool {
1256 let idx = self.html_comment_ranges.partition_point(|r| r.start <= byte_pos);
1257 idx > 0 && byte_pos < self.html_comment_ranges[idx - 1].end
1258 }
1259
1260 #[inline]
1263 pub fn is_in_html_tag(&self, byte_pos: usize) -> bool {
1264 let tags = self.html_tags();
1265 let idx = tags.partition_point(|tag| tag.byte_offset <= byte_pos);
1266 idx > 0 && byte_pos < tags[idx - 1].byte_end
1267 }
1268
1269 pub fn is_in_jinja_range(&self, byte_pos: usize) -> bool {
1271 Self::binary_search_ranges(&self.jinja_ranges, byte_pos)
1272 }
1273
1274 #[inline]
1276 pub fn is_in_jsx_expression(&self, byte_pos: usize) -> bool {
1277 Self::binary_search_ranges(&self.jsx_expression_ranges, byte_pos)
1278 }
1279
1280 #[inline]
1282 pub fn is_in_mdx_comment(&self, byte_pos: usize) -> bool {
1283 Self::binary_search_ranges(&self.mdx_comment_ranges, byte_pos)
1284 }
1285
1286 #[inline]
1289 pub fn is_in_citation(&self, byte_pos: usize) -> bool {
1290 let idx = self.citation_ranges.partition_point(|r| r.start <= byte_pos);
1291 idx > 0 && byte_pos < self.citation_ranges[idx - 1].end
1292 }
1293
1294 #[inline]
1296 pub fn citation_ranges(&self) -> &[crate::utils::skip_context::ByteRange] {
1297 &self.citation_ranges
1298 }
1299
1300 #[inline]
1303 pub fn is_in_div_block(&self, byte_pos: usize) -> bool {
1304 let idx = self.pandoc_div_ranges.partition_point(|r| r.start <= byte_pos);
1305 idx > 0 && byte_pos < self.pandoc_div_ranges[idx - 1].end
1306 }
1307
1308 #[inline]
1311 pub fn is_in_inline_footnote(&self, byte_pos: usize) -> bool {
1312 let idx = self.inline_footnote_ranges.partition_point(|r| r.start <= byte_pos);
1313 idx > 0 && byte_pos < self.inline_footnote_ranges[idx - 1].end
1314 }
1315
1316 #[inline]
1319 pub fn is_in_example_list_marker(&self, byte_pos: usize) -> bool {
1320 let idx = self.example_list_marker_ranges.partition_point(|r| r.start <= byte_pos);
1321 idx > 0 && byte_pos < self.example_list_marker_ranges[idx - 1].end
1322 }
1323
1324 #[inline]
1327 pub fn is_in_example_reference(&self, byte_pos: usize) -> bool {
1328 let idx = self.example_reference_ranges.partition_point(|r| r.start <= byte_pos);
1329 idx > 0 && byte_pos < self.example_reference_ranges[idx - 1].end
1330 }
1331
1332 #[inline]
1335 pub fn is_in_subscript_or_superscript(&self, byte_pos: usize) -> bool {
1336 let idx = self.sub_super_ranges.partition_point(|r| r.start <= byte_pos);
1337 idx > 0 && byte_pos < self.sub_super_ranges[idx - 1].end
1338 }
1339
1340 #[inline]
1344 pub fn is_in_inline_code_attr(&self, byte_pos: usize) -> bool {
1345 let idx = self.inline_code_attr_ranges.partition_point(|r| r.start <= byte_pos);
1346 idx > 0 && byte_pos < self.inline_code_attr_ranges[idx - 1].end
1347 }
1348
1349 #[inline]
1352 pub fn is_in_bracketed_span(&self, byte_pos: usize) -> bool {
1353 let idx = self.bracketed_span_ranges.partition_point(|r| r.start <= byte_pos);
1354 idx > 0 && byte_pos < self.bracketed_span_ranges[idx - 1].end
1355 }
1356
1357 #[inline]
1360 pub fn is_in_line_block(&self, byte_pos: usize) -> bool {
1361 let idx = self.line_block_ranges.partition_point(|r| r.start <= byte_pos);
1362 idx > 0 && byte_pos < self.line_block_ranges[idx - 1].end
1363 }
1364
1365 #[inline]
1369 pub fn is_in_pipe_table_caption(&self, byte_pos: usize) -> bool {
1370 let idx = self.pipe_table_caption_ranges.partition_point(|r| r.start <= byte_pos);
1371 idx > 0 && byte_pos < self.pipe_table_caption_ranges[idx - 1].end
1372 }
1373
1374 #[inline]
1377 pub fn is_in_pandoc_metadata(&self, byte_pos: usize) -> bool {
1378 let idx = self.pandoc_metadata_ranges.partition_point(|r| r.start <= byte_pos);
1379 idx > 0 && byte_pos < self.pandoc_metadata_ranges[idx - 1].end
1380 }
1381
1382 #[inline]
1385 pub fn is_in_grid_table(&self, byte_pos: usize) -> bool {
1386 let idx = self.grid_table_ranges.partition_point(|r| r.start <= byte_pos);
1387 idx > 0 && byte_pos < self.grid_table_ranges[idx - 1].end
1388 }
1389
1390 #[inline]
1393 pub fn is_in_multi_line_table(&self, byte_pos: usize) -> bool {
1394 let idx = self.multi_line_table_ranges.partition_point(|r| r.start <= byte_pos);
1395 idx > 0 && byte_pos < self.multi_line_table_ranges[idx - 1].end
1396 }
1397
1398 pub fn matches_implicit_header_reference(&self, link_text: &str) -> bool {
1403 let slug = crate::utils::pandoc::pandoc_header_slug(link_text);
1404 self.pandoc_header_slugs.contains(&slug)
1405 }
1406
1407 #[inline]
1413 pub fn has_pandoc_slug(&self, slug: &str) -> bool {
1414 self.pandoc_header_slugs.contains(slug)
1415 }
1416
1417 #[inline]
1419 pub fn is_in_shortcode(&self, byte_pos: usize) -> bool {
1420 Self::binary_search_ranges(&self.shortcode_ranges, byte_pos)
1421 }
1422
1423 #[inline]
1425 pub fn shortcode_ranges(&self) -> &[(usize, usize)] {
1426 &self.shortcode_ranges
1427 }
1428
1429 pub fn is_in_link_title(&self, byte_pos: usize) -> bool {
1431 Self::binary_search_ranges(&self.link_title_ranges, byte_pos)
1432 }
1433
1434 pub fn has_char(&self, ch: char) -> bool {
1436 match ch {
1437 '#' => self.char_frequency.hash_count > 0,
1438 '*' => self.char_frequency.asterisk_count > 0,
1439 '_' => self.char_frequency.underscore_count > 0,
1440 '-' => self.char_frequency.hyphen_count > 0,
1441 '+' => self.char_frequency.plus_count > 0,
1442 '>' => self.char_frequency.gt_count > 0,
1443 '|' => self.char_frequency.pipe_count > 0,
1444 '[' => self.char_frequency.bracket_count > 0,
1445 '`' => self.char_frequency.backtick_count > 0,
1446 '<' => self.char_frequency.lt_count > 0,
1447 '!' => self.char_frequency.exclamation_count > 0,
1448 '\n' => self.char_frequency.newline_count > 0,
1449 _ => self.content.contains(ch), }
1451 }
1452
1453 pub fn char_count(&self, ch: char) -> usize {
1455 match ch {
1456 '#' => self.char_frequency.hash_count,
1457 '*' => self.char_frequency.asterisk_count,
1458 '_' => self.char_frequency.underscore_count,
1459 '-' => self.char_frequency.hyphen_count,
1460 '+' => self.char_frequency.plus_count,
1461 '>' => self.char_frequency.gt_count,
1462 '|' => self.char_frequency.pipe_count,
1463 '[' => self.char_frequency.bracket_count,
1464 '`' => self.char_frequency.backtick_count,
1465 '<' => self.char_frequency.lt_count,
1466 '!' => self.char_frequency.exclamation_count,
1467 '\n' => self.char_frequency.newline_count,
1468 _ => self.content.matches(ch).count(), }
1470 }
1471
1472 pub fn likely_has_headings(&self) -> bool {
1474 self.char_frequency.hash_count > 0 || self.char_frequency.hyphen_count > 2 || self.content.contains('=') }
1476
1477 pub fn likely_has_lists(&self) -> bool {
1479 self.char_frequency.asterisk_count > 0
1480 || self.char_frequency.hyphen_count > 0
1481 || self.char_frequency.plus_count > 0
1482 }
1483
1484 pub fn likely_has_emphasis(&self) -> bool {
1486 self.char_frequency.asterisk_count > 1 || self.char_frequency.underscore_count > 1
1487 }
1488
1489 pub fn likely_has_tables(&self) -> bool {
1491 self.char_frequency.pipe_count > 2
1492 }
1493
1494 pub fn likely_has_blockquotes(&self) -> bool {
1496 self.char_frequency.gt_count > 0
1497 }
1498
1499 pub fn likely_has_code(&self) -> bool {
1501 self.char_frequency.backtick_count > 0
1502 }
1503
1504 pub fn likely_has_links_or_images(&self) -> bool {
1506 self.char_frequency.bracket_count > 0 || self.char_frequency.exclamation_count > 0
1507 }
1508
1509 pub fn likely_has_html(&self) -> bool {
1511 self.char_frequency.lt_count > 0
1512 }
1513
1514 pub fn blockquote_prefix_for_blank_line(&self, line_idx: usize) -> String {
1519 if let Some(line_info) = self.lines.get(line_idx)
1520 && let Some(ref bq) = line_info.blockquote
1521 {
1522 bq.prefix.trim_end().to_string()
1523 } else {
1524 String::new()
1525 }
1526 }
1527
1528 #[inline]
1534 fn find_line_for_offset(lines: &[LineInfo], byte_offset: usize) -> (usize, usize, usize) {
1535 let idx = match lines.binary_search_by(|line| {
1537 if byte_offset < line.byte_offset {
1538 std::cmp::Ordering::Greater
1539 } else if byte_offset > line.byte_offset + line.byte_len {
1540 std::cmp::Ordering::Less
1541 } else {
1542 std::cmp::Ordering::Equal
1543 }
1544 }) {
1545 Ok(idx) => idx,
1546 Err(idx) => idx.saturating_sub(1),
1547 };
1548
1549 let line = &lines[idx];
1550 let line_num = idx + 1;
1551 let col = byte_offset.saturating_sub(line.byte_offset);
1552
1553 (idx, line_num, col)
1554 }
1555
1556 #[inline]
1558 fn is_offset_in_code_span(code_spans: &[CodeSpan], offset: usize) -> bool {
1559 let idx = code_spans.partition_point(|span| span.byte_offset <= offset);
1561
1562 if idx > 0 {
1564 let span = &code_spans[idx - 1];
1565 if offset >= span.byte_offset && offset < span.byte_end {
1566 return true;
1567 }
1568 }
1569
1570 false
1571 }
1572
1573 #[must_use]
1593 pub fn valid_headings(&self) -> ValidHeadingsIter<'_> {
1594 ValidHeadingsIter::new(&self.lines)
1595 }
1596
1597 #[must_use]
1601 pub fn has_valid_headings(&self) -> bool {
1602 self.lines
1603 .iter()
1604 .any(|line| line.heading.as_ref().is_some_and(|h| h.is_valid))
1605 }
1606}
1607
1608fn detect_footnote_definitions(content: &str, lines: &mut [types::LineInfo], line_offsets: &[usize]) {
1617 use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
1618
1619 let options = crate::utils::rumdl_parser_options();
1620 let parser = Parser::new_ext(content, options).into_offset_iter();
1621
1622 let mut footnote_ranges: Vec<(usize, usize)> = Vec::new();
1624 let mut fenced_code_ranges: Vec<(usize, usize)> = Vec::new();
1625 let mut in_footnote = false;
1626
1627 for (event, range) in parser {
1628 match event {
1629 Event::Start(Tag::FootnoteDefinition(_)) => {
1630 in_footnote = true;
1631 footnote_ranges.push((range.start, range.end));
1632 }
1633 Event::End(TagEnd::FootnoteDefinition) => {
1634 in_footnote = false;
1635 }
1636 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_))) if in_footnote => {
1637 fenced_code_ranges.push((range.start, range.end));
1638 }
1639 _ => {}
1640 }
1641 }
1642
1643 let byte_to_line = |byte_offset: usize| -> usize {
1644 line_offsets
1645 .partition_point(|&offset| offset <= byte_offset)
1646 .saturating_sub(1)
1647 };
1648
1649 for &(start, end) in &footnote_ranges {
1651 let start_line = byte_to_line(start);
1652 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1653
1654 for line in &mut lines[start_line..end_line] {
1655 line.in_footnote_definition = true;
1656 line.in_code_block = false;
1657 }
1658 }
1659
1660 for &(start, end) in &fenced_code_ranges {
1662 let start_line = byte_to_line(start);
1663 let end_line = line_offsets.partition_point(|&offset| offset < end).min(lines.len());
1664
1665 for line in &mut lines[start_line..end_line] {
1666 line.in_code_block = true;
1667 }
1668 }
1669}