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