1use crate::lint_context::{LineInfo, LintContext};
36
37#[derive(Debug, Clone)]
39pub struct FilteredLine<'a> {
40 pub line_num: usize,
42 pub line_info: &'a LineInfo,
44 pub content: &'a str,
46}
47
48#[derive(Debug, Clone, Default)]
65pub struct LineFilterConfig {
66 pub skip_front_matter: bool,
68 pub skip_code_blocks: bool,
70 pub skip_html_blocks: bool,
72 pub skip_html_comments: bool,
74 pub skip_mkdocstrings: bool,
76 pub skip_esm_blocks: bool,
78 pub skip_math_blocks: bool,
80 pub skip_quarto_divs: bool,
82 pub skip_jsx_expressions: bool,
84 pub skip_mdx_comments: bool,
86 pub skip_admonitions: bool,
88 pub skip_content_tabs: bool,
90 pub skip_mkdocs_html_markdown: bool,
92 pub skip_definition_lists: bool,
94 pub skip_obsidian_comments: bool,
96 pub skip_pymdown_blocks: bool,
98 pub skip_div_markers: bool,
102 pub skip_jsx_blocks: bool,
104}
105
106impl LineFilterConfig {
107 #[must_use]
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 #[must_use]
118 pub fn skip_front_matter(mut self) -> Self {
119 self.skip_front_matter = true;
120 self
121 }
122
123 #[must_use]
128 pub fn skip_code_blocks(mut self) -> Self {
129 self.skip_code_blocks = true;
130 self
131 }
132
133 #[must_use]
138 pub fn skip_html_blocks(mut self) -> Self {
139 self.skip_html_blocks = true;
140 self
141 }
142
143 #[must_use]
148 pub fn skip_html_comments(mut self) -> Self {
149 self.skip_html_comments = true;
150 self
151 }
152
153 #[must_use]
158 pub fn skip_mkdocstrings(mut self) -> Self {
159 self.skip_mkdocstrings = true;
160 self
161 }
162
163 #[must_use]
168 pub fn skip_esm_blocks(mut self) -> Self {
169 self.skip_esm_blocks = true;
170 self
171 }
172
173 #[must_use]
178 pub fn skip_math_blocks(mut self) -> Self {
179 self.skip_math_blocks = true;
180 self
181 }
182
183 #[must_use]
188 pub fn skip_quarto_divs(mut self) -> Self {
189 self.skip_quarto_divs = true;
190 self
191 }
192
193 #[must_use]
198 pub fn skip_jsx_expressions(mut self) -> Self {
199 self.skip_jsx_expressions = true;
200 self
201 }
202
203 #[must_use]
208 pub fn skip_mdx_comments(mut self) -> Self {
209 self.skip_mdx_comments = true;
210 self
211 }
212
213 #[must_use]
218 pub fn skip_admonitions(mut self) -> Self {
219 self.skip_admonitions = true;
220 self
221 }
222
223 #[must_use]
227 pub fn skip_content_tabs(mut self) -> Self {
228 self.skip_content_tabs = true;
229 self
230 }
231
232 #[must_use]
236 pub fn skip_mkdocs_html_markdown(mut self) -> Self {
237 self.skip_mkdocs_html_markdown = true;
238 self
239 }
240
241 #[must_use]
247 pub fn skip_mkdocs_containers(mut self) -> Self {
248 self.skip_admonitions = true;
249 self.skip_content_tabs = true;
250 self.skip_mkdocs_html_markdown = true;
251 self
252 }
253
254 #[must_use]
259 pub fn skip_definition_lists(mut self) -> Self {
260 self.skip_definition_lists = true;
261 self
262 }
263
264 #[must_use]
269 pub fn skip_obsidian_comments(mut self) -> Self {
270 self.skip_obsidian_comments = true;
271 self
272 }
273
274 #[must_use]
280 pub fn skip_pymdown_blocks(mut self) -> Self {
281 self.skip_pymdown_blocks = true;
282 self
283 }
284
285 #[must_use]
291 pub fn skip_div_markers(mut self) -> Self {
292 self.skip_div_markers = true;
293 self
294 }
295
296 #[must_use]
301 pub fn skip_jsx_blocks(mut self) -> Self {
302 self.skip_jsx_blocks = true;
303 self
304 }
305
306 fn should_filter(&self, line_info: &LineInfo) -> bool {
308 line_info.in_kramdown_extension_block
311 || (self.skip_front_matter && line_info.in_front_matter)
312 || (self.skip_code_blocks && line_info.in_code_block)
313 || (self.skip_html_blocks && line_info.in_html_block)
314 || (self.skip_html_comments && line_info.in_html_comment)
315 || (self.skip_mkdocstrings && line_info.in_mkdocstrings)
316 || (self.skip_esm_blocks && line_info.in_esm_block)
317 || (self.skip_math_blocks && line_info.in_math_block)
318 || (self.skip_quarto_divs && line_info.in_pandoc_div)
319 || (self.skip_jsx_expressions && line_info.in_jsx_expression)
320 || (self.skip_mdx_comments && line_info.in_mdx_comment)
321 || (self.skip_admonitions && line_info.in_admonition)
322 || (self.skip_content_tabs && line_info.in_content_tab)
323 || (self.skip_mkdocs_html_markdown && line_info.in_mkdocs_html_markdown)
324 || (self.skip_definition_lists && line_info.in_definition_list)
325 || (self.skip_obsidian_comments && line_info.in_obsidian_comment)
326 || (self.skip_pymdown_blocks && line_info.in_pymdown_block)
327 || (self.skip_div_markers && line_info.is_div_marker)
328 || (self.skip_jsx_blocks && line_info.in_jsx_block)
329 }
330}
331
332pub struct FilteredLinesIter<'a> {
334 ctx: &'a LintContext<'a>,
335 config: LineFilterConfig,
336 current_index: usize,
337}
338
339impl<'a> FilteredLinesIter<'a> {
340 fn new(ctx: &'a LintContext<'a>, config: LineFilterConfig) -> Self {
342 Self {
343 ctx,
344 config,
345 current_index: 0,
346 }
347 }
348}
349
350impl<'a> Iterator for FilteredLinesIter<'a> {
351 type Item = FilteredLine<'a>;
352
353 fn next(&mut self) -> Option<Self::Item> {
354 let lines = &self.ctx.lines;
355 let raw_lines = self.ctx.raw_lines();
356
357 while self.current_index < lines.len() {
358 let idx = self.current_index;
359 self.current_index += 1;
360
361 if self.config.should_filter(&lines[idx]) {
363 continue;
364 }
365
366 let line_content = raw_lines.get(idx).copied().unwrap_or("");
368
369 return Some(FilteredLine {
371 line_num: idx + 1, line_info: &lines[idx],
373 content: line_content,
374 });
375 }
376
377 None
378 }
379}
380
381pub trait FilteredLinesExt {
386 fn filtered_lines(&self) -> FilteredLinesBuilder<'_>;
405
406 fn content_lines(&self) -> FilteredLinesIter<'_>;
429}
430
431pub struct FilteredLinesBuilder<'a> {
433 ctx: &'a LintContext<'a>,
434 config: LineFilterConfig,
435}
436
437impl<'a> FilteredLinesBuilder<'a> {
438 fn new(ctx: &'a LintContext<'a>) -> Self {
439 Self {
440 ctx,
441 config: LineFilterConfig::new(),
442 }
443 }
444
445 #[must_use]
447 pub fn skip_front_matter(mut self) -> Self {
448 self.config = self.config.skip_front_matter();
449 self
450 }
451
452 #[must_use]
454 pub fn skip_code_blocks(mut self) -> Self {
455 self.config = self.config.skip_code_blocks();
456 self
457 }
458
459 #[must_use]
461 pub fn skip_html_blocks(mut self) -> Self {
462 self.config = self.config.skip_html_blocks();
463 self
464 }
465
466 #[must_use]
468 pub fn skip_html_comments(mut self) -> Self {
469 self.config = self.config.skip_html_comments();
470 self
471 }
472
473 #[must_use]
475 pub fn skip_mkdocstrings(mut self) -> Self {
476 self.config = self.config.skip_mkdocstrings();
477 self
478 }
479
480 #[must_use]
482 pub fn skip_esm_blocks(mut self) -> Self {
483 self.config = self.config.skip_esm_blocks();
484 self
485 }
486
487 #[must_use]
489 pub fn skip_math_blocks(mut self) -> Self {
490 self.config = self.config.skip_math_blocks();
491 self
492 }
493
494 #[must_use]
496 pub fn skip_quarto_divs(mut self) -> Self {
497 self.config = self.config.skip_quarto_divs();
498 self
499 }
500
501 #[must_use]
503 pub fn skip_jsx_expressions(mut self) -> Self {
504 self.config = self.config.skip_jsx_expressions();
505 self
506 }
507
508 #[must_use]
510 pub fn skip_mdx_comments(mut self) -> Self {
511 self.config = self.config.skip_mdx_comments();
512 self
513 }
514
515 #[must_use]
517 pub fn skip_admonitions(mut self) -> Self {
518 self.config = self.config.skip_admonitions();
519 self
520 }
521
522 #[must_use]
524 pub fn skip_content_tabs(mut self) -> Self {
525 self.config = self.config.skip_content_tabs();
526 self
527 }
528
529 #[must_use]
531 pub fn skip_mkdocs_html_markdown(mut self) -> Self {
532 self.config = self.config.skip_mkdocs_html_markdown();
533 self
534 }
535
536 #[must_use]
542 pub fn skip_mkdocs_containers(mut self) -> Self {
543 self.config = self.config.skip_mkdocs_containers();
544 self
545 }
546
547 #[must_use]
549 pub fn skip_definition_lists(mut self) -> Self {
550 self.config = self.config.skip_definition_lists();
551 self
552 }
553
554 #[must_use]
556 pub fn skip_obsidian_comments(mut self) -> Self {
557 self.config = self.config.skip_obsidian_comments();
558 self
559 }
560
561 #[must_use]
563 pub fn skip_pymdown_blocks(mut self) -> Self {
564 self.config = self.config.skip_pymdown_blocks();
565 self
566 }
567
568 #[must_use]
573 pub fn skip_div_markers(mut self) -> Self {
574 self.config = self.config.skip_div_markers();
575 self
576 }
577
578 #[must_use]
580 pub fn skip_jsx_blocks(mut self) -> Self {
581 self.config = self.config.skip_jsx_blocks();
582 self
583 }
584}
585
586impl<'a> IntoIterator for FilteredLinesBuilder<'a> {
587 type Item = FilteredLine<'a>;
588 type IntoIter = FilteredLinesIter<'a>;
589
590 fn into_iter(self) -> Self::IntoIter {
591 FilteredLinesIter::new(self.ctx, self.config)
592 }
593}
594
595impl<'a> FilteredLinesExt for LintContext<'a> {
596 fn filtered_lines(&self) -> FilteredLinesBuilder<'_> {
597 FilteredLinesBuilder::new(self)
598 }
599
600 fn content_lines(&self) -> FilteredLinesIter<'_> {
601 FilteredLinesIter::new(self, LineFilterConfig::new().skip_front_matter())
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use crate::config::MarkdownFlavor;
609
610 #[test]
611 fn test_filtered_line_structure() {
612 let content = "# Title\n\nContent";
613 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
614
615 let line = ctx.content_lines().next().unwrap();
616 assert_eq!(line.line_num, 1);
617 assert_eq!(line.content, "# Title");
618 assert!(!line.line_info.in_front_matter);
619 }
620
621 #[test]
622 fn test_skip_front_matter_yaml() {
623 let content = "---\ntitle: Test\nurl: http://example.com\n---\n\n# Content\n\nMore content";
624 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
625
626 let lines: Vec<_> = ctx.content_lines().collect();
627 assert_eq!(lines.len(), 4);
629 assert_eq!(lines[0].line_num, 5); assert_eq!(lines[0].content, "");
631 assert_eq!(lines[1].line_num, 6);
632 assert_eq!(lines[1].content, "# Content");
633 assert_eq!(lines[2].line_num, 7);
634 assert_eq!(lines[2].content, "");
635 assert_eq!(lines[3].line_num, 8);
636 assert_eq!(lines[3].content, "More content");
637 }
638
639 #[test]
640 fn test_skip_front_matter_toml() {
641 let content = "+++\ntitle = \"Test\"\nurl = \"http://example.com\"\n+++\n\n# Content";
642 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
643
644 let lines: Vec<_> = ctx.content_lines().collect();
645 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
647 assert_eq!(lines[1].line_num, 6);
648 assert_eq!(lines[1].content, "# Content");
649 }
650
651 #[test]
652 fn test_skip_front_matter_json() {
653 let content = "{\n\"title\": \"Test\",\n\"url\": \"http://example.com\"\n}\n\n# Content";
654 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
655
656 let lines: Vec<_> = ctx.content_lines().collect();
657 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
659 assert_eq!(lines[1].line_num, 6);
660 assert_eq!(lines[1].content, "# Content");
661 }
662
663 #[test]
664 fn test_skip_code_blocks() {
665 let content = "# Title\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nContent";
666 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
667
668 let lines: Vec<_> = ctx.filtered_lines().skip_code_blocks().into_iter().collect();
669
670 assert!(lines.iter().any(|l| l.content == "# Title"));
675 assert!(lines.iter().any(|l| l.content == "Content"));
676 assert!(!lines.iter().any(|l| l.content == "let x = 1;"));
678 assert!(!lines.iter().any(|l| l.content == "let y = 2;"));
679 }
680
681 #[test]
682 fn test_no_filters() {
683 let content = "---\ntitle: Test\n---\n\n# Content";
684 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
685
686 let lines: Vec<_> = ctx.filtered_lines().into_iter().collect();
688 assert_eq!(lines.len(), ctx.lines.len());
689 }
690
691 #[test]
692 fn test_multiple_filters() {
693 let content = "---\ntitle: Test\n---\n\n# Title\n\n```rust\ncode\n```\n\nContent";
694 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
695
696 let lines: Vec<_> = ctx
697 .filtered_lines()
698 .skip_front_matter()
699 .skip_code_blocks()
700 .into_iter()
701 .collect();
702
703 assert!(lines.iter().any(|l| l.content == "# Title"));
705 assert!(lines.iter().any(|l| l.content == "Content"));
706 assert!(!lines.iter().any(|l| l.content == "title: Test"));
707 assert!(!lines.iter().any(|l| l.content == "code"));
708 }
709
710 #[test]
711 fn test_line_numbering_is_1_indexed() {
712 let content = "First\nSecond\nThird";
713 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
714
715 let lines: Vec<_> = ctx.content_lines().collect();
716 assert_eq!(lines[0].line_num, 1);
717 assert_eq!(lines[0].content, "First");
718 assert_eq!(lines[1].line_num, 2);
719 assert_eq!(lines[1].content, "Second");
720 assert_eq!(lines[2].line_num, 3);
721 assert_eq!(lines[2].content, "Third");
722 }
723
724 #[test]
725 fn test_content_lines_convenience_method() {
726 let content = "---\nfoo: bar\n---\n\nContent";
727 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
728
729 let lines: Vec<_> = ctx.content_lines().collect();
731 assert!(!lines.iter().any(|l| l.content.contains("foo")));
732 assert!(lines.iter().any(|l| l.content == "Content"));
733 }
734
735 #[test]
736 fn test_empty_document() {
737 let content = "";
738 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
739
740 let lines: Vec<_> = ctx.content_lines().collect();
741 assert_eq!(lines.len(), 0);
742 }
743
744 #[test]
745 fn test_only_front_matter() {
746 let content = "---\ntitle: Test\n---";
747 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
748
749 let lines: Vec<_> = ctx.content_lines().collect();
750 assert_eq!(
751 lines.len(),
752 0,
753 "Document with only front matter should have no content lines"
754 );
755 }
756
757 #[test]
758 fn test_builder_pattern_ergonomics() {
759 let content = "# Title\n\n```\ncode\n```\n\nContent";
760 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
761
762 let _lines: Vec<_> = ctx
764 .filtered_lines()
765 .skip_front_matter()
766 .skip_code_blocks()
767 .skip_html_blocks()
768 .into_iter()
769 .collect();
770
771 }
773
774 #[test]
775 fn test_filtered_line_access_to_line_info() {
776 let content = "# Title\n\nContent";
777 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
778
779 for line in ctx.content_lines() {
780 assert!(!line.line_info.in_front_matter);
782 assert!(!line.line_info.in_code_block);
783 }
784 }
785
786 #[test]
787 fn test_skip_mkdocstrings() {
788 let content = r#"# API Documentation
789
790::: mymodule.MyClass
791 options:
792 show_root_heading: true
793 show_source: false
794
795Some regular content here.
796
797::: mymodule.function
798 options:
799 show_signature: true
800
801More content."#;
802 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
803 let lines: Vec<_> = ctx.filtered_lines().skip_mkdocstrings().into_iter().collect();
804
805 assert!(
807 lines.iter().any(|l| l.content.contains("# API Documentation")),
808 "Should include lines outside mkdocstrings blocks"
809 );
810 assert!(
811 lines.iter().any(|l| l.content.contains("Some regular content")),
812 "Should include content between mkdocstrings blocks"
813 );
814 assert!(
815 lines.iter().any(|l| l.content.contains("More content")),
816 "Should include content after mkdocstrings blocks"
817 );
818
819 assert!(
821 !lines.iter().any(|l| l.content.contains("::: mymodule")),
822 "Should exclude mkdocstrings marker lines"
823 );
824 assert!(
825 !lines.iter().any(|l| l.content.contains("show_root_heading")),
826 "Should exclude mkdocstrings option lines"
827 );
828 assert!(
829 !lines.iter().any(|l| l.content.contains("show_signature")),
830 "Should exclude all mkdocstrings option lines"
831 );
832
833 assert_eq!(lines[0].line_num, 1, "First line should be line 1");
835 }
836
837 #[test]
838 fn test_skip_esm_blocks() {
839 let content = r#"import {Chart} from './components.js'
841import {Table} from './table.js'
842export const year = 2023
843
844# Last year's snowfall
845
846Content about snowfall data.
847
848import {Footer} from './footer.js'
849
850More content."#;
851 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
852 let lines: Vec<_> = ctx.filtered_lines().skip_esm_blocks().into_iter().collect();
853
854 assert!(
856 lines.iter().any(|l| l.content.contains("# Last year's snowfall")),
857 "Should include markdown headings"
858 );
859 assert!(
860 lines.iter().any(|l| l.content.contains("Content about snowfall")),
861 "Should include markdown content"
862 );
863 assert!(
864 lines.iter().any(|l| l.content.contains("More content")),
865 "Should include content after ESM blocks"
866 );
867
868 assert!(
870 !lines.iter().any(|l| l.content.contains("import {Chart}")),
871 "Should exclude import statements at top of file"
872 );
873 assert!(
874 !lines.iter().any(|l| l.content.contains("import {Table}")),
875 "Should exclude all import statements at top of file"
876 );
877 assert!(
878 !lines.iter().any(|l| l.content.contains("export const year")),
879 "Should exclude export statements at top of file"
880 );
881 assert!(
883 !lines.iter().any(|l| l.content.contains("import {Footer}")),
884 "Should exclude import statements even after markdown content (MDX 2.0+ ESM anywhere)"
885 );
886
887 let heading_line = lines
889 .iter()
890 .find(|l| l.content.contains("# Last year's snowfall"))
891 .unwrap();
892 assert_eq!(heading_line.line_num, 5, "Heading should be on line 5");
893 }
894
895 #[test]
896 fn test_all_filters_combined() {
897 let content = r#"---
898title: Test
899---
900
901# Title
902
903```
904code
905```
906
907<!-- HTML comment here -->
908
909::: mymodule.Class
910 options:
911 show_root_heading: true
912
913<div>
914HTML block
915</div>
916
917Content"#;
918 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
919
920 let lines: Vec<_> = ctx
921 .filtered_lines()
922 .skip_front_matter()
923 .skip_code_blocks()
924 .skip_html_blocks()
925 .skip_html_comments()
926 .skip_mkdocstrings()
927 .into_iter()
928 .collect();
929
930 assert!(
932 lines.iter().any(|l| l.content == "# Title"),
933 "Should include markdown headings"
934 );
935 assert!(
936 lines.iter().any(|l| l.content == "Content"),
937 "Should include markdown content"
938 );
939
940 assert!(
942 !lines.iter().any(|l| l.content == "title: Test"),
943 "Should exclude front matter"
944 );
945 assert!(
946 !lines.iter().any(|l| l.content == "code"),
947 "Should exclude code block content"
948 );
949 assert!(
950 !lines.iter().any(|l| l.content.contains("HTML comment")),
951 "Should exclude HTML comments"
952 );
953 assert!(
954 !lines.iter().any(|l| l.content.contains("::: mymodule")),
955 "Should exclude mkdocstrings blocks"
956 );
957 assert!(
958 !lines.iter().any(|l| l.content.contains("show_root_heading")),
959 "Should exclude mkdocstrings options"
960 );
961 assert!(
962 !lines.iter().any(|l| l.content.contains("HTML block")),
963 "Should exclude HTML blocks"
964 );
965 }
966
967 #[test]
968 fn test_skip_math_blocks() {
969 let content = r#"# Heading
970
971Some regular text.
972
973$$
974A = \left[
975\begin{array}{c}
9761 \\
977-D
978\end{array}
979\right]
980$$
981
982More content after math."#;
983 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
984 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
985
986 assert!(
988 lines.iter().any(|l| l.content.contains("# Heading")),
989 "Should include markdown headings"
990 );
991 assert!(
992 lines.iter().any(|l| l.content.contains("Some regular text")),
993 "Should include regular text before math block"
994 );
995 assert!(
996 lines.iter().any(|l| l.content.contains("More content after math")),
997 "Should include content after math block"
998 );
999
1000 assert!(
1002 !lines.iter().any(|l| l.content == "$$"),
1003 "Should exclude math block delimiters"
1004 );
1005 assert!(
1006 !lines.iter().any(|l| l.content.contains("\\left[")),
1007 "Should exclude LaTeX content inside math block"
1008 );
1009 assert!(
1010 !lines.iter().any(|l| l.content.contains("-D")),
1011 "Should exclude content that looks like list items inside math block"
1012 );
1013 assert!(
1014 !lines.iter().any(|l| l.content.contains("\\begin{array}")),
1015 "Should exclude LaTeX array content"
1016 );
1017 }
1018
1019 #[test]
1020 fn test_math_blocks_not_confused_with_code_blocks() {
1021 let content = r#"# Title
1022
1023```python
1024# This $$ is inside a code block
1025x = 1
1026```
1027
1028$$
1029y = 2
1030$$
1031
1032Regular text."#;
1033 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1034
1035 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
1037
1038 assert!(
1041 lines.iter().any(|l| l.content.contains("# This $$")),
1042 "Code block content with $$ should not be detected as math block"
1043 );
1044
1045 assert!(
1047 !lines.iter().any(|l| l.content == "y = 2"),
1048 "Actual math block content should be excluded"
1049 );
1050 }
1051
1052 #[test]
1053 fn test_skip_quarto_divs() {
1054 let content = r#"# Heading
1055
1056::: {.callout-note}
1057This is a callout note.
1058With multiple lines.
1059:::
1060
1061Regular text outside.
1062
1063::: {.bordered}
1064Content inside bordered div.
1065:::
1066
1067More content."#;
1068 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1069 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1070
1071 assert!(
1073 lines.iter().any(|l| l.content.contains("# Heading")),
1074 "Should include markdown headings"
1075 );
1076 assert!(
1077 lines.iter().any(|l| l.content.contains("Regular text outside")),
1078 "Should include content between divs"
1079 );
1080 assert!(
1081 lines.iter().any(|l| l.content.contains("More content")),
1082 "Should include content after divs"
1083 );
1084
1085 assert!(
1087 !lines.iter().any(|l| l.content.contains("::: {.callout-note}")),
1088 "Should exclude callout div markers"
1089 );
1090 assert!(
1091 !lines.iter().any(|l| l.content.contains("This is a callout note")),
1092 "Should exclude callout content"
1093 );
1094 assert!(
1095 !lines.iter().any(|l| l.content.contains("Content inside bordered")),
1096 "Should exclude bordered div content"
1097 );
1098 }
1099
1100 #[test]
1101 fn test_skip_jsx_expressions() {
1102 let content = r#"# MDX Document
1103
1104Here is some content with {myVariable} inline.
1105
1106{items.map(item => (
1107 <Item key={item.id} />
1108))}
1109
1110Regular paragraph after expression.
1111
1112{/* This should NOT be skipped by jsx_expressions filter */}
1113{/* MDX comments have their own filter */}
1114
1115More content."#;
1116 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1117 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1118
1119 assert!(
1121 lines.iter().any(|l| l.content.contains("# MDX Document")),
1122 "Should include markdown headings"
1123 );
1124 assert!(
1125 lines.iter().any(|l| l.content.contains("Regular paragraph")),
1126 "Should include regular paragraphs"
1127 );
1128 assert!(
1129 lines.iter().any(|l| l.content.contains("More content")),
1130 "Should include content after expressions"
1131 );
1132
1133 assert!(
1135 !lines.iter().any(|l| l.content.contains("{myVariable}")),
1136 "Should exclude lines with inline JSX expressions"
1137 );
1138 assert!(
1139 !lines.iter().any(|l| l.content.contains("items.map")),
1140 "Should exclude multi-line JSX expression content"
1141 );
1142 assert!(
1143 !lines.iter().any(|l| l.content.contains("<Item key")),
1144 "Should exclude JSX inside expressions"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_skip_quarto_divs_nested() {
1150 let content = r#"# Title
1151
1152::: {.outer}
1153Outer content.
1154
1155::: {.inner}
1156Inner content.
1157:::
1158
1159Back to outer.
1160:::
1161
1162Outside text."#;
1163 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1164 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1165
1166 assert!(
1168 lines.iter().any(|l| l.content.contains("# Title")),
1169 "Should include heading"
1170 );
1171 assert!(
1172 lines.iter().any(|l| l.content.contains("Outside text")),
1173 "Should include text after divs"
1174 );
1175
1176 assert!(
1178 !lines.iter().any(|l| l.content.contains("Outer content")),
1179 "Should exclude outer div content"
1180 );
1181 assert!(
1182 !lines.iter().any(|l| l.content.contains("Inner content")),
1183 "Should exclude inner div content"
1184 );
1185 }
1186
1187 #[test]
1188 fn test_skip_quarto_divs_not_in_standard_flavor() {
1189 let content = r#"::: {.callout-note}
1190This should NOT be skipped in standard flavor.
1191:::"#;
1192 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1193 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1194
1195 assert!(
1197 lines.iter().any(|l| l.content.contains("This should NOT be skipped")),
1198 "Standard flavor should not detect Quarto divs"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_skip_mdx_comments() {
1204 let content = r#"# MDX Document
1205
1206{/* This is an MDX comment */}
1207
1208Regular content here.
1209
1210{/*
1211 Multi-line
1212 MDX comment
1213*/}
1214
1215More content after comment."#;
1216 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1217 let lines: Vec<_> = ctx.filtered_lines().skip_mdx_comments().into_iter().collect();
1218
1219 assert!(
1221 lines.iter().any(|l| l.content.contains("# MDX Document")),
1222 "Should include markdown headings"
1223 );
1224 assert!(
1225 lines.iter().any(|l| l.content.contains("Regular content")),
1226 "Should include regular content"
1227 );
1228 assert!(
1229 lines.iter().any(|l| l.content.contains("More content")),
1230 "Should include content after comments"
1231 );
1232
1233 assert!(
1235 !lines.iter().any(|l| l.content.contains("{/* This is")),
1236 "Should exclude single-line MDX comments"
1237 );
1238 assert!(
1239 !lines.iter().any(|l| l.content.contains("Multi-line")),
1240 "Should exclude multi-line MDX comment content"
1241 );
1242 }
1243
1244 #[test]
1245 fn test_jsx_expressions_with_nested_braces() {
1246 let content = r#"# Document
1248
1249{props.style || {color: "red", background: "blue"}}
1250
1251Regular content."#;
1252 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1253 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1254
1255 assert!(
1257 !lines.iter().any(|l| l.content.contains("props.style")),
1258 "Should exclude JSX expression with nested braces"
1259 );
1260 assert!(
1261 lines.iter().any(|l| l.content.contains("Regular content")),
1262 "Should include content after nested expression"
1263 );
1264 }
1265
1266 #[test]
1267 fn test_jsx_and_mdx_comments_combined() {
1268 let content = r#"# Title
1270
1271{variable}
1272
1273{/* comment */}
1274
1275Content."#;
1276 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1277 let lines: Vec<_> = ctx
1278 .filtered_lines()
1279 .skip_jsx_expressions()
1280 .skip_mdx_comments()
1281 .into_iter()
1282 .collect();
1283
1284 assert!(
1285 lines.iter().any(|l| l.content.contains("# Title")),
1286 "Should include heading"
1287 );
1288 assert!(
1289 lines.iter().any(|l| l.content.contains("Content")),
1290 "Should include regular content"
1291 );
1292 assert!(
1293 !lines.iter().any(|l| l.content.contains("{variable}")),
1294 "Should exclude JSX expression"
1295 );
1296 assert!(
1297 !lines.iter().any(|l| l.content.contains("{/* comment */")),
1298 "Should exclude MDX comment"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_jsx_expressions_not_detected_in_standard_flavor() {
1304 let content = r#"# Document
1306
1307{this is not JSX in standard markdown}
1308
1309Content."#;
1310 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1311 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1312
1313 assert!(
1315 lines.iter().any(|l| l.content.contains("{this is not JSX")),
1316 "Should NOT exclude brace content in standard markdown"
1317 );
1318 }
1319
1320 #[test]
1323 fn test_skip_obsidian_comments_simple_inline() {
1324 let content = r#"# Heading
1326
1327This is visible %%this is hidden%% and visible again.
1328
1329More content."#;
1330 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1331 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1332
1333 assert!(
1335 lines.iter().any(|l| l.content.contains("# Heading")),
1336 "Should include heading"
1337 );
1338 assert!(
1339 lines.iter().any(|l| l.content.contains("This is visible")),
1340 "Should include line with inline comment"
1341 );
1342 assert!(
1343 lines.iter().any(|l| l.content.contains("More content")),
1344 "Should include content after comment"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_skip_obsidian_comments_multiline_block() {
1350 let content = r#"# Heading
1352
1353%%
1354This is a multi-line
1355comment block
1356%%
1357
1358Content after."#;
1359 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1360 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1361
1362 assert!(
1364 lines.iter().any(|l| l.content.contains("# Heading")),
1365 "Should include heading"
1366 );
1367 assert!(
1368 lines.iter().any(|l| l.content.contains("Content after")),
1369 "Should include content after comment block"
1370 );
1371
1372 assert!(
1374 !lines.iter().any(|l| l.content.contains("This is a multi-line")),
1375 "Should exclude multi-line comment content"
1376 );
1377 assert!(
1378 !lines.iter().any(|l| l.content.contains("comment block")),
1379 "Should exclude multi-line comment content"
1380 );
1381 }
1382
1383 #[test]
1384 fn test_skip_obsidian_comments_in_code_block() {
1385 let content = r#"# Heading
1387
1388```
1389%% This is NOT a comment
1390It's inside a code block
1391%%
1392```
1393
1394Content."#;
1395 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1396 let lines: Vec<_> = ctx
1397 .filtered_lines()
1398 .skip_obsidian_comments()
1399 .skip_code_blocks()
1400 .into_iter()
1401 .collect();
1402
1403 assert!(
1405 lines.iter().any(|l| l.content.contains("# Heading")),
1406 "Should include heading"
1407 );
1408 assert!(
1409 lines.iter().any(|l| l.content.contains("Content")),
1410 "Should include content after code block"
1411 );
1412 }
1413
1414 #[test]
1415 fn test_skip_obsidian_comments_in_html_comment() {
1416 let content = r#"# Heading
1418
1419<!-- %% This is inside HTML comment %% -->
1420
1421Content."#;
1422 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1423 let lines: Vec<_> = ctx
1424 .filtered_lines()
1425 .skip_obsidian_comments()
1426 .skip_html_comments()
1427 .into_iter()
1428 .collect();
1429
1430 assert!(
1431 lines.iter().any(|l| l.content.contains("# Heading")),
1432 "Should include heading"
1433 );
1434 assert!(
1435 lines.iter().any(|l| l.content.contains("Content")),
1436 "Should include content"
1437 );
1438 }
1439
1440 #[test]
1441 fn test_skip_obsidian_comments_empty() {
1442 let content = r#"# Heading
1444
1445%%%% empty comment
1446
1447Content."#;
1448 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1449 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1450
1451 assert!(
1453 lines.iter().any(|l| l.content.contains("# Heading")),
1454 "Should include heading"
1455 );
1456 }
1457
1458 #[test]
1459 fn test_skip_obsidian_comments_unclosed() {
1460 let content = r#"# Heading
1462
1463%% starts but never ends
1464This should be hidden
1465Until end of document"#;
1466 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1467 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1468
1469 assert!(
1471 lines.iter().any(|l| l.content.contains("# Heading")),
1472 "Should include heading before unclosed comment"
1473 );
1474
1475 assert!(
1477 !lines.iter().any(|l| l.content.contains("This should be hidden")),
1478 "Should exclude content in unclosed comment"
1479 );
1480 assert!(
1481 !lines.iter().any(|l| l.content.contains("Until end of document")),
1482 "Should exclude content until end of document"
1483 );
1484 }
1485
1486 #[test]
1487 fn test_skip_obsidian_comments_multiple_on_same_line() {
1488 let content = r#"# Heading
1490
1491First %%hidden1%% middle %%hidden2%% last
1492
1493Content."#;
1494 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1495 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1496
1497 assert!(
1499 lines.iter().any(|l| l.content.contains("First")),
1500 "Should include line with multiple inline comments"
1501 );
1502 assert!(
1503 lines.iter().any(|l| l.content.contains("middle")),
1504 "Should include visible text between comments"
1505 );
1506 }
1507
1508 #[test]
1509 fn test_skip_obsidian_comments_at_start_of_line() {
1510 let content = r#"# Heading
1512
1513%%comment at start%%
1514
1515Content."#;
1516 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1517 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1518
1519 assert!(
1520 lines.iter().any(|l| l.content.contains("# Heading")),
1521 "Should include heading"
1522 );
1523 assert!(
1524 lines.iter().any(|l| l.content.contains("Content")),
1525 "Should include content"
1526 );
1527 }
1528
1529 #[test]
1530 fn test_skip_obsidian_comments_at_end_of_line() {
1531 let content = r#"# Heading
1533
1534Some text %%comment at end%%
1535
1536Content."#;
1537 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1538 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1539
1540 assert!(
1541 lines.iter().any(|l| l.content.contains("Some text")),
1542 "Should include text before comment"
1543 );
1544 }
1545
1546 #[test]
1547 fn test_skip_obsidian_comments_with_markdown_inside() {
1548 let content = r#"# Heading
1550
1551%%
1552# hidden heading
1553[hidden link](url)
1554**hidden bold**
1555%%
1556
1557Content."#;
1558 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1559 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1560
1561 assert!(
1562 !lines.iter().any(|l| l.content.contains("# hidden heading")),
1563 "Should exclude heading inside comment"
1564 );
1565 assert!(
1566 !lines.iter().any(|l| l.content.contains("[hidden link]")),
1567 "Should exclude link inside comment"
1568 );
1569 assert!(
1570 !lines.iter().any(|l| l.content.contains("**hidden bold**")),
1571 "Should exclude bold inside comment"
1572 );
1573 }
1574
1575 #[test]
1576 fn test_skip_obsidian_comments_with_unicode() {
1577 let content = r#"# Heading
1579
1580%%日本語コメント%%
1581
1582%%Комментарий%%
1583
1584Content."#;
1585 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1586 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1587
1588 assert!(
1590 lines.iter().any(|l| l.content.contains("# Heading")),
1591 "Should include heading"
1592 );
1593 assert!(
1594 lines.iter().any(|l| l.content.contains("Content")),
1595 "Should include content"
1596 );
1597 }
1598
1599 #[test]
1600 fn test_skip_obsidian_comments_triple_percent() {
1601 let content = r#"# Heading
1603
1604%%% odd percent
1605
1606Content."#;
1607 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1608 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1609
1610 assert!(
1612 lines.iter().any(|l| l.content.contains("# Heading")),
1613 "Should include heading"
1614 );
1615 }
1616
1617 #[test]
1618 fn test_skip_obsidian_comments_not_in_standard_flavor() {
1619 let content = r#"# Heading
1621
1622%%this is not hidden in standard%%
1623
1624Content."#;
1625 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1626 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1627
1628 assert!(
1630 lines.iter().any(|l| l.content.contains("%%this is not hidden")),
1631 "Should NOT hide %% content in Standard flavor"
1632 );
1633 }
1634
1635 #[test]
1636 fn test_skip_obsidian_comments_integration_with_other_filters() {
1637 let content = r#"---
1639title: Test
1640---
1641
1642# Heading
1643
1644```
1645code
1646```
1647
1648%%hidden comment%%
1649
1650Content."#;
1651 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1652 let lines: Vec<_> = ctx
1653 .filtered_lines()
1654 .skip_front_matter()
1655 .skip_code_blocks()
1656 .skip_obsidian_comments()
1657 .into_iter()
1658 .collect();
1659
1660 assert!(
1662 !lines.iter().any(|l| l.content.contains("title: Test")),
1663 "Should skip frontmatter"
1664 );
1665 assert!(
1666 !lines.iter().any(|l| l.content == "code"),
1667 "Should skip code block content"
1668 );
1669 assert!(
1670 lines.iter().any(|l| l.content.contains("# Heading")),
1671 "Should include heading"
1672 );
1673 assert!(
1674 lines.iter().any(|l| l.content.contains("Content")),
1675 "Should include content"
1676 );
1677 }
1678
1679 #[test]
1680 fn test_skip_obsidian_comments_whole_line_only() {
1681 let content = "start %%\nfully hidden\n%% end";
1683 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1684 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1685
1686 assert!(
1688 lines.iter().any(|l| l.content.contains("start")),
1689 "First line should be included (starts outside comment)"
1690 );
1691 assert!(
1693 !lines.iter().any(|l| l.content == "fully hidden"),
1694 "Middle line should be excluded (entirely within comment)"
1695 );
1696 assert!(
1698 lines.iter().any(|l| l.content.contains("end")),
1699 "Last line should be included (ends outside comment)"
1700 );
1701 }
1702
1703 #[test]
1704 fn test_skip_obsidian_comments_in_inline_code() {
1705 let content = r#"# Heading
1707
1708The syntax is `%%comment%%` in Obsidian.
1709
1710Content."#;
1711 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1712 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1713
1714 assert!(
1716 lines.iter().any(|l| l.content.contains("The syntax is")),
1717 "Should include line with %% in code span"
1718 );
1719 assert!(
1720 lines.iter().any(|l| l.content.contains("in Obsidian")),
1721 "Should include text after code span"
1722 );
1723 }
1724
1725 #[test]
1726 fn test_skip_obsidian_comments_in_inline_code_multi_backtick() {
1727 let content = r#"# Heading
1729
1730The syntax is ``%%comment%%`` in Obsidian.
1731
1732Content."#;
1733 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1734 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1735
1736 assert!(
1737 lines.iter().any(|l| l.content.contains("The syntax is")),
1738 "Should include line with %% in multi-backtick code span"
1739 );
1740 assert!(
1741 lines.iter().any(|l| l.content.contains("Content")),
1742 "Should include content after code span"
1743 );
1744 }
1745
1746 #[test]
1747 fn test_skip_obsidian_comments_consecutive_blocks() {
1748 let content = r#"# Heading
1750
1751%%comment 1%%
1752
1753%%comment 2%%
1754
1755Content."#;
1756 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1757 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1758
1759 assert!(
1760 lines.iter().any(|l| l.content.contains("# Heading")),
1761 "Should include heading"
1762 );
1763 assert!(
1764 lines.iter().any(|l| l.content.contains("Content")),
1765 "Should include content after comments"
1766 );
1767 }
1768
1769 #[test]
1770 fn test_skip_obsidian_comments_spanning_many_lines() {
1771 let content = r#"# Title
1773
1774%%
1775Line 1 of comment
1776Line 2 of comment
1777Line 3 of comment
1778Line 4 of comment
1779Line 5 of comment
1780%%
1781
1782After comment."#;
1783 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1784 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1785
1786 for i in 1..=5 {
1788 assert!(
1789 !lines
1790 .iter()
1791 .any(|l| l.content.contains(&format!("Line {i} of comment"))),
1792 "Should exclude line {i} of comment"
1793 );
1794 }
1795
1796 assert!(
1797 lines.iter().any(|l| l.content.contains("# Title")),
1798 "Should include title"
1799 );
1800 assert!(
1801 lines.iter().any(|l| l.content.contains("After comment")),
1802 "Should include content after comment"
1803 );
1804 }
1805
1806 #[test]
1807 fn test_obsidian_comment_line_info_field() {
1808 let content = "visible\n%%\nhidden\n%%\nvisible";
1810 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1811
1812 assert!(
1814 !ctx.lines[0].in_obsidian_comment,
1815 "Line 0 should not be marked as in_obsidian_comment"
1816 );
1817
1818 assert!(
1820 ctx.lines[2].in_obsidian_comment,
1821 "Line 2 (hidden) should be marked as in_obsidian_comment"
1822 );
1823
1824 assert!(
1826 !ctx.lines[4].in_obsidian_comment,
1827 "Line 4 should not be marked as in_obsidian_comment"
1828 );
1829 }
1830
1831 #[test]
1834 fn test_skip_pymdown_blocks_basic() {
1835 let content = r#"# Heading
1837
1838/// caption
1839Table caption here.
1840///
1841
1842Content after."#;
1843 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1844 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1845
1846 assert!(
1848 lines.iter().any(|l| l.content.contains("# Heading")),
1849 "Should include heading"
1850 );
1851 assert!(
1852 lines.iter().any(|l| l.content.contains("Content after")),
1853 "Should include content after block"
1854 );
1855
1856 assert!(
1858 !lines.iter().any(|l| l.content.contains("Table caption")),
1859 "Should exclude content inside block"
1860 );
1861 }
1862
1863 #[test]
1864 fn test_skip_pymdown_blocks_details() {
1865 let content = r#"# Heading
1867
1868/// details | Click to expand
1869 open: True
1870Hidden content here.
1871More hidden content.
1872///
1873
1874Visible content."#;
1875 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1876 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1877
1878 assert!(
1879 !lines.iter().any(|l| l.content.contains("Hidden content")),
1880 "Should exclude hidden content"
1881 );
1882 assert!(
1883 !lines.iter().any(|l| l.content.contains("open: True")),
1884 "Should exclude YAML options"
1885 );
1886 assert!(
1887 lines.iter().any(|l| l.content.contains("Visible content")),
1888 "Should include visible content"
1889 );
1890 }
1891
1892 #[test]
1893 fn test_skip_pymdown_blocks_nested() {
1894 let content = r#"# Title
1896
1897/// details | Outer
1898Outer content.
1899
1900 /// caption
1901 Inner caption.
1902 ///
1903
1904More outer content.
1905///
1906
1907After all blocks."#;
1908 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1909 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1910
1911 assert!(
1912 !lines.iter().any(|l| l.content.contains("Outer content")),
1913 "Should exclude outer block content"
1914 );
1915 assert!(
1916 !lines.iter().any(|l| l.content.contains("Inner caption")),
1917 "Should exclude inner block content"
1918 );
1919 assert!(
1920 lines.iter().any(|l| l.content.contains("After all blocks")),
1921 "Should include content after all blocks"
1922 );
1923 }
1924
1925 #[test]
1926 fn test_pymdown_block_line_info_field() {
1927 let content = "visible\n/// caption\nhidden\n///\nvisible";
1929 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1930
1931 assert!(
1933 !ctx.lines[0].in_pymdown_block,
1934 "Line 0 should not be marked as in_pymdown_block"
1935 );
1936
1937 assert!(
1939 ctx.lines[1].in_pymdown_block,
1940 "Line 1 (/// caption) should be marked as in_pymdown_block"
1941 );
1942
1943 assert!(
1945 ctx.lines[2].in_pymdown_block,
1946 "Line 2 (hidden) should be marked as in_pymdown_block"
1947 );
1948
1949 assert!(
1951 ctx.lines[3].in_pymdown_block,
1952 "Line 3 (closing ///) should be marked as in_pymdown_block"
1953 );
1954
1955 assert!(
1957 !ctx.lines[4].in_pymdown_block,
1958 "Line 4 should not be marked as in_pymdown_block"
1959 );
1960 }
1961
1962 #[test]
1963 fn test_pymdown_blocks_only_for_mkdocs_flavor() {
1964 let content = "/// caption\nCaption text\n///";
1966
1967 let ctx_mkdocs = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1969 assert!(
1970 ctx_mkdocs.lines[1].in_pymdown_block,
1971 "MkDocs flavor should detect pymdown blocks"
1972 );
1973
1974 let ctx_standard = LintContext::new(content, MarkdownFlavor::Standard, None);
1976 assert!(
1977 !ctx_standard.lines[1].in_pymdown_block,
1978 "Standard flavor should NOT detect pymdown blocks"
1979 );
1980 }
1981}