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_pandoc_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 #[must_use]
599 pub fn skip_jsx_blocks(mut self) -> Self {
600 self.config = self.config.skip_jsx_blocks();
601 self
602 }
603}
604
605impl<'a> IntoIterator for FilteredLinesBuilder<'a> {
606 type Item = FilteredLine<'a>;
607 type IntoIter = FilteredLinesIter<'a>;
608
609 fn into_iter(self) -> Self::IntoIter {
610 FilteredLinesIter::new(self.ctx, self.config)
611 }
612}
613
614impl<'a> FilteredLinesExt for LintContext<'a> {
615 fn filtered_lines(&self) -> FilteredLinesBuilder<'_> {
616 FilteredLinesBuilder::new(self)
617 }
618
619 fn content_lines(&self) -> FilteredLinesIter<'_> {
620 FilteredLinesIter::new(self, LineFilterConfig::new().skip_front_matter())
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::config::MarkdownFlavor;
628
629 #[test]
630 fn test_filtered_line_structure() {
631 let content = "# Title\n\nContent";
632 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
633
634 let line = ctx.content_lines().next().unwrap();
635 assert_eq!(line.line_num, 1);
636 assert_eq!(line.content, "# Title");
637 assert!(!line.line_info.in_front_matter);
638 }
639
640 #[test]
641 fn test_skip_front_matter_yaml() {
642 let content = "---\ntitle: Test\nurl: http://example.com\n---\n\n# Content\n\nMore content";
643 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
644
645 let lines: Vec<_> = ctx.content_lines().collect();
646 assert_eq!(lines.len(), 4);
648 assert_eq!(lines[0].line_num, 5); assert_eq!(lines[0].content, "");
650 assert_eq!(lines[1].line_num, 6);
651 assert_eq!(lines[1].content, "# Content");
652 assert_eq!(lines[2].line_num, 7);
653 assert_eq!(lines[2].content, "");
654 assert_eq!(lines[3].line_num, 8);
655 assert_eq!(lines[3].content, "More content");
656 }
657
658 #[test]
659 fn test_skip_front_matter_toml() {
660 let content = "+++\ntitle = \"Test\"\nurl = \"http://example.com\"\n+++\n\n# Content";
661 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
662
663 let lines: Vec<_> = ctx.content_lines().collect();
664 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
666 assert_eq!(lines[1].line_num, 6);
667 assert_eq!(lines[1].content, "# Content");
668 }
669
670 #[test]
671 fn test_skip_front_matter_json() {
672 let content = "{\n\"title\": \"Test\",\n\"url\": \"http://example.com\"\n}\n\n# Content";
673 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
674
675 let lines: Vec<_> = ctx.content_lines().collect();
676 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
678 assert_eq!(lines[1].line_num, 6);
679 assert_eq!(lines[1].content, "# Content");
680 }
681
682 #[test]
683 fn test_skip_code_blocks() {
684 let content = "# Title\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nContent";
685 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
686
687 let lines: Vec<_> = ctx.filtered_lines().skip_code_blocks().into_iter().collect();
688
689 assert!(lines.iter().any(|l| l.content == "# Title"));
694 assert!(lines.iter().any(|l| l.content == "Content"));
695 assert!(!lines.iter().any(|l| l.content == "let x = 1;"));
697 assert!(!lines.iter().any(|l| l.content == "let y = 2;"));
698 }
699
700 #[test]
701 fn test_no_filters() {
702 let content = "---\ntitle: Test\n---\n\n# Content";
703 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
704
705 let lines: Vec<_> = ctx.filtered_lines().into_iter().collect();
707 assert_eq!(lines.len(), ctx.lines.len());
708 }
709
710 #[test]
711 fn test_multiple_filters() {
712 let content = "---\ntitle: Test\n---\n\n# Title\n\n```rust\ncode\n```\n\nContent";
713 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
714
715 let lines: Vec<_> = ctx
716 .filtered_lines()
717 .skip_front_matter()
718 .skip_code_blocks()
719 .into_iter()
720 .collect();
721
722 assert!(lines.iter().any(|l| l.content == "# Title"));
724 assert!(lines.iter().any(|l| l.content == "Content"));
725 assert!(!lines.iter().any(|l| l.content == "title: Test"));
726 assert!(!lines.iter().any(|l| l.content == "code"));
727 }
728
729 #[test]
730 fn test_line_numbering_is_1_indexed() {
731 let content = "First\nSecond\nThird";
732 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
733
734 let lines: Vec<_> = ctx.content_lines().collect();
735 assert_eq!(lines[0].line_num, 1);
736 assert_eq!(lines[0].content, "First");
737 assert_eq!(lines[1].line_num, 2);
738 assert_eq!(lines[1].content, "Second");
739 assert_eq!(lines[2].line_num, 3);
740 assert_eq!(lines[2].content, "Third");
741 }
742
743 #[test]
744 fn test_content_lines_convenience_method() {
745 let content = "---\nfoo: bar\n---\n\nContent";
746 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
747
748 let lines: Vec<_> = ctx.content_lines().collect();
750 assert!(!lines.iter().any(|l| l.content.contains("foo")));
751 assert!(lines.iter().any(|l| l.content == "Content"));
752 }
753
754 #[test]
755 fn test_empty_document() {
756 let content = "";
757 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
758
759 let lines: Vec<_> = ctx.content_lines().collect();
760 assert_eq!(lines.len(), 0);
761 }
762
763 #[test]
764 fn test_only_front_matter() {
765 let content = "---\ntitle: Test\n---";
766 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
767
768 let lines: Vec<_> = ctx.content_lines().collect();
769 assert_eq!(
770 lines.len(),
771 0,
772 "Document with only front matter should have no content lines"
773 );
774 }
775
776 #[test]
777 fn test_builder_pattern_ergonomics() {
778 let content = "# Title\n\n```\ncode\n```\n\nContent";
779 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
780
781 let _lines: Vec<_> = ctx
783 .filtered_lines()
784 .skip_front_matter()
785 .skip_code_blocks()
786 .skip_html_blocks()
787 .into_iter()
788 .collect();
789
790 }
792
793 #[test]
794 fn test_filtered_line_access_to_line_info() {
795 let content = "# Title\n\nContent";
796 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
797
798 for line in ctx.content_lines() {
799 assert!(!line.line_info.in_front_matter);
801 assert!(!line.line_info.in_code_block);
802 }
803 }
804
805 #[test]
806 fn test_skip_mkdocstrings() {
807 let content = r#"# API Documentation
808
809::: mymodule.MyClass
810 options:
811 show_root_heading: true
812 show_source: false
813
814Some regular content here.
815
816::: mymodule.function
817 options:
818 show_signature: true
819
820More content."#;
821 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
822 let lines: Vec<_> = ctx.filtered_lines().skip_mkdocstrings().into_iter().collect();
823
824 assert!(
826 lines.iter().any(|l| l.content.contains("# API Documentation")),
827 "Should include lines outside mkdocstrings blocks"
828 );
829 assert!(
830 lines.iter().any(|l| l.content.contains("Some regular content")),
831 "Should include content between mkdocstrings blocks"
832 );
833 assert!(
834 lines.iter().any(|l| l.content.contains("More content")),
835 "Should include content after mkdocstrings blocks"
836 );
837
838 assert!(
840 !lines.iter().any(|l| l.content.contains("::: mymodule")),
841 "Should exclude mkdocstrings marker lines"
842 );
843 assert!(
844 !lines.iter().any(|l| l.content.contains("show_root_heading")),
845 "Should exclude mkdocstrings option lines"
846 );
847 assert!(
848 !lines.iter().any(|l| l.content.contains("show_signature")),
849 "Should exclude all mkdocstrings option lines"
850 );
851
852 assert_eq!(lines[0].line_num, 1, "First line should be line 1");
854 }
855
856 #[test]
857 fn test_skip_esm_blocks() {
858 let content = r#"import {Chart} from './components.js'
860import {Table} from './table.js'
861export const year = 2023
862
863# Last year's snowfall
864
865Content about snowfall data.
866
867import {Footer} from './footer.js'
868
869More content."#;
870 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
871 let lines: Vec<_> = ctx.filtered_lines().skip_esm_blocks().into_iter().collect();
872
873 assert!(
875 lines.iter().any(|l| l.content.contains("# Last year's snowfall")),
876 "Should include markdown headings"
877 );
878 assert!(
879 lines.iter().any(|l| l.content.contains("Content about snowfall")),
880 "Should include markdown content"
881 );
882 assert!(
883 lines.iter().any(|l| l.content.contains("More content")),
884 "Should include content after ESM blocks"
885 );
886
887 assert!(
889 !lines.iter().any(|l| l.content.contains("import {Chart}")),
890 "Should exclude import statements at top of file"
891 );
892 assert!(
893 !lines.iter().any(|l| l.content.contains("import {Table}")),
894 "Should exclude all import statements at top of file"
895 );
896 assert!(
897 !lines.iter().any(|l| l.content.contains("export const year")),
898 "Should exclude export statements at top of file"
899 );
900 assert!(
902 !lines.iter().any(|l| l.content.contains("import {Footer}")),
903 "Should exclude import statements even after markdown content (MDX 2.0+ ESM anywhere)"
904 );
905
906 let heading_line = lines
908 .iter()
909 .find(|l| l.content.contains("# Last year's snowfall"))
910 .unwrap();
911 assert_eq!(heading_line.line_num, 5, "Heading should be on line 5");
912 }
913
914 #[test]
915 fn test_all_filters_combined() {
916 let content = r#"---
917title: Test
918---
919
920# Title
921
922```
923code
924```
925
926<!-- HTML comment here -->
927
928::: mymodule.Class
929 options:
930 show_root_heading: true
931
932<div>
933HTML block
934</div>
935
936Content"#;
937 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
938
939 let lines: Vec<_> = ctx
940 .filtered_lines()
941 .skip_front_matter()
942 .skip_code_blocks()
943 .skip_html_blocks()
944 .skip_html_comments()
945 .skip_mkdocstrings()
946 .into_iter()
947 .collect();
948
949 assert!(
951 lines.iter().any(|l| l.content == "# Title"),
952 "Should include markdown headings"
953 );
954 assert!(
955 lines.iter().any(|l| l.content == "Content"),
956 "Should include markdown content"
957 );
958
959 assert!(
961 !lines.iter().any(|l| l.content == "title: Test"),
962 "Should exclude front matter"
963 );
964 assert!(
965 !lines.iter().any(|l| l.content == "code"),
966 "Should exclude code block content"
967 );
968 assert!(
969 !lines.iter().any(|l| l.content.contains("HTML comment")),
970 "Should exclude HTML comments"
971 );
972 assert!(
973 !lines.iter().any(|l| l.content.contains("::: mymodule")),
974 "Should exclude mkdocstrings blocks"
975 );
976 assert!(
977 !lines.iter().any(|l| l.content.contains("show_root_heading")),
978 "Should exclude mkdocstrings options"
979 );
980 assert!(
981 !lines.iter().any(|l| l.content.contains("HTML block")),
982 "Should exclude HTML blocks"
983 );
984 }
985
986 #[test]
987 fn test_skip_math_blocks() {
988 let content = r#"# Heading
989
990Some regular text.
991
992$$
993A = \left[
994\begin{array}{c}
9951 \\
996-D
997\end{array}
998\right]
999$$
1000
1001More content after math."#;
1002 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1003 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
1004
1005 assert!(
1007 lines.iter().any(|l| l.content.contains("# Heading")),
1008 "Should include markdown headings"
1009 );
1010 assert!(
1011 lines.iter().any(|l| l.content.contains("Some regular text")),
1012 "Should include regular text before math block"
1013 );
1014 assert!(
1015 lines.iter().any(|l| l.content.contains("More content after math")),
1016 "Should include content after math block"
1017 );
1018
1019 assert!(
1021 !lines.iter().any(|l| l.content == "$$"),
1022 "Should exclude math block delimiters"
1023 );
1024 assert!(
1025 !lines.iter().any(|l| l.content.contains("\\left[")),
1026 "Should exclude LaTeX content inside math block"
1027 );
1028 assert!(
1029 !lines.iter().any(|l| l.content.contains("-D")),
1030 "Should exclude content that looks like list items inside math block"
1031 );
1032 assert!(
1033 !lines.iter().any(|l| l.content.contains("\\begin{array}")),
1034 "Should exclude LaTeX array content"
1035 );
1036 }
1037
1038 #[test]
1039 fn test_math_blocks_not_confused_with_code_blocks() {
1040 let content = r#"# Title
1041
1042```python
1043# This $$ is inside a code block
1044x = 1
1045```
1046
1047$$
1048y = 2
1049$$
1050
1051Regular text."#;
1052 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1053
1054 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
1056
1057 assert!(
1060 lines.iter().any(|l| l.content.contains("# This $$")),
1061 "Code block content with $$ should not be detected as math block"
1062 );
1063
1064 assert!(
1066 !lines.iter().any(|l| l.content == "y = 2"),
1067 "Actual math block content should be excluded"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_skip_quarto_divs() {
1073 let content = r#"# Heading
1074
1075::: {.callout-note}
1076This is a callout note.
1077With multiple lines.
1078:::
1079
1080Regular text outside.
1081
1082::: {.bordered}
1083Content inside bordered div.
1084:::
1085
1086More content."#;
1087 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1088 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1089
1090 assert!(
1092 lines.iter().any(|l| l.content.contains("# Heading")),
1093 "Should include markdown headings"
1094 );
1095 assert!(
1096 lines.iter().any(|l| l.content.contains("Regular text outside")),
1097 "Should include content between divs"
1098 );
1099 assert!(
1100 lines.iter().any(|l| l.content.contains("More content")),
1101 "Should include content after divs"
1102 );
1103
1104 assert!(
1106 !lines.iter().any(|l| l.content.contains("::: {.callout-note}")),
1107 "Should exclude callout div markers"
1108 );
1109 assert!(
1110 !lines.iter().any(|l| l.content.contains("This is a callout note")),
1111 "Should exclude callout content"
1112 );
1113 assert!(
1114 !lines.iter().any(|l| l.content.contains("Content inside bordered")),
1115 "Should exclude bordered div content"
1116 );
1117 }
1118
1119 #[test]
1120 fn test_skip_jsx_expressions() {
1121 let content = r#"# MDX Document
1122
1123Here is some content with {myVariable} inline.
1124
1125{items.map(item => (
1126 <Item key={item.id} />
1127))}
1128
1129Regular paragraph after expression.
1130
1131{/* This should NOT be skipped by jsx_expressions filter */}
1132{/* MDX comments have their own filter */}
1133
1134More content."#;
1135 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1136 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1137
1138 assert!(
1140 lines.iter().any(|l| l.content.contains("# MDX Document")),
1141 "Should include markdown headings"
1142 );
1143 assert!(
1144 lines.iter().any(|l| l.content.contains("Regular paragraph")),
1145 "Should include regular paragraphs"
1146 );
1147 assert!(
1148 lines.iter().any(|l| l.content.contains("More content")),
1149 "Should include content after expressions"
1150 );
1151
1152 assert!(
1154 !lines.iter().any(|l| l.content.contains("{myVariable}")),
1155 "Should exclude lines with inline JSX expressions"
1156 );
1157 assert!(
1158 !lines.iter().any(|l| l.content.contains("items.map")),
1159 "Should exclude multi-line JSX expression content"
1160 );
1161 assert!(
1162 !lines.iter().any(|l| l.content.contains("<Item key")),
1163 "Should exclude JSX inside expressions"
1164 );
1165 }
1166
1167 #[test]
1168 fn test_skip_quarto_divs_nested() {
1169 let content = r#"# Title
1170
1171::: {.outer}
1172Outer content.
1173
1174::: {.inner}
1175Inner content.
1176:::
1177
1178Back to outer.
1179:::
1180
1181Outside text."#;
1182 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1183 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1184
1185 assert!(
1187 lines.iter().any(|l| l.content.contains("# Title")),
1188 "Should include heading"
1189 );
1190 assert!(
1191 lines.iter().any(|l| l.content.contains("Outside text")),
1192 "Should include text after divs"
1193 );
1194
1195 assert!(
1197 !lines.iter().any(|l| l.content.contains("Outer content")),
1198 "Should exclude outer div content"
1199 );
1200 assert!(
1201 !lines.iter().any(|l| l.content.contains("Inner content")),
1202 "Should exclude inner div content"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_skip_quarto_divs_not_in_standard_flavor() {
1208 let content = r#"::: {.callout-note}
1209This should NOT be skipped in standard flavor.
1210:::"#;
1211 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1212 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1213
1214 assert!(
1216 lines.iter().any(|l| l.content.contains("This should NOT be skipped")),
1217 "Standard flavor should not detect Quarto divs"
1218 );
1219 }
1220
1221 #[test]
1222 fn test_skip_mdx_comments() {
1223 let content = r#"# MDX Document
1224
1225{/* This is an MDX comment */}
1226
1227Regular content here.
1228
1229{/*
1230 Multi-line
1231 MDX comment
1232*/}
1233
1234More content after comment."#;
1235 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1236 let lines: Vec<_> = ctx.filtered_lines().skip_mdx_comments().into_iter().collect();
1237
1238 assert!(
1240 lines.iter().any(|l| l.content.contains("# MDX Document")),
1241 "Should include markdown headings"
1242 );
1243 assert!(
1244 lines.iter().any(|l| l.content.contains("Regular content")),
1245 "Should include regular content"
1246 );
1247 assert!(
1248 lines.iter().any(|l| l.content.contains("More content")),
1249 "Should include content after comments"
1250 );
1251
1252 assert!(
1254 !lines.iter().any(|l| l.content.contains("{/* This is")),
1255 "Should exclude single-line MDX comments"
1256 );
1257 assert!(
1258 !lines.iter().any(|l| l.content.contains("Multi-line")),
1259 "Should exclude multi-line MDX comment content"
1260 );
1261 }
1262
1263 #[test]
1264 fn test_jsx_expressions_with_nested_braces() {
1265 let content = r#"# Document
1267
1268{props.style || {color: "red", background: "blue"}}
1269
1270Regular content."#;
1271 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1272 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1273
1274 assert!(
1276 !lines.iter().any(|l| l.content.contains("props.style")),
1277 "Should exclude JSX expression with nested braces"
1278 );
1279 assert!(
1280 lines.iter().any(|l| l.content.contains("Regular content")),
1281 "Should include content after nested expression"
1282 );
1283 }
1284
1285 #[test]
1286 fn test_jsx_and_mdx_comments_combined() {
1287 let content = r#"# Title
1289
1290{variable}
1291
1292{/* comment */}
1293
1294Content."#;
1295 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1296 let lines: Vec<_> = ctx
1297 .filtered_lines()
1298 .skip_jsx_expressions()
1299 .skip_mdx_comments()
1300 .into_iter()
1301 .collect();
1302
1303 assert!(
1304 lines.iter().any(|l| l.content.contains("# Title")),
1305 "Should include heading"
1306 );
1307 assert!(
1308 lines.iter().any(|l| l.content.contains("Content")),
1309 "Should include regular content"
1310 );
1311 assert!(
1312 !lines.iter().any(|l| l.content.contains("{variable}")),
1313 "Should exclude JSX expression"
1314 );
1315 assert!(
1316 !lines.iter().any(|l| l.content.contains("{/* comment */")),
1317 "Should exclude MDX comment"
1318 );
1319 }
1320
1321 #[test]
1322 fn test_jsx_expressions_not_detected_in_standard_flavor() {
1323 let content = r#"# Document
1325
1326{this is not JSX in standard markdown}
1327
1328Content."#;
1329 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1330 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1331
1332 assert!(
1334 lines.iter().any(|l| l.content.contains("{this is not JSX")),
1335 "Should NOT exclude brace content in standard markdown"
1336 );
1337 }
1338
1339 #[test]
1342 fn test_skip_obsidian_comments_simple_inline() {
1343 let content = r#"# Heading
1345
1346This is visible %%this is hidden%% and visible again.
1347
1348More content."#;
1349 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1350 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1351
1352 assert!(
1354 lines.iter().any(|l| l.content.contains("# Heading")),
1355 "Should include heading"
1356 );
1357 assert!(
1358 lines.iter().any(|l| l.content.contains("This is visible")),
1359 "Should include line with inline comment"
1360 );
1361 assert!(
1362 lines.iter().any(|l| l.content.contains("More content")),
1363 "Should include content after comment"
1364 );
1365 }
1366
1367 #[test]
1368 fn test_skip_obsidian_comments_multiline_block() {
1369 let content = r#"# Heading
1371
1372%%
1373This is a multi-line
1374comment block
1375%%
1376
1377Content after."#;
1378 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1379 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1380
1381 assert!(
1383 lines.iter().any(|l| l.content.contains("# Heading")),
1384 "Should include heading"
1385 );
1386 assert!(
1387 lines.iter().any(|l| l.content.contains("Content after")),
1388 "Should include content after comment block"
1389 );
1390
1391 assert!(
1393 !lines.iter().any(|l| l.content.contains("This is a multi-line")),
1394 "Should exclude multi-line comment content"
1395 );
1396 assert!(
1397 !lines.iter().any(|l| l.content.contains("comment block")),
1398 "Should exclude multi-line comment content"
1399 );
1400 }
1401
1402 #[test]
1403 fn test_skip_obsidian_comments_in_code_block() {
1404 let content = r#"# Heading
1406
1407```
1408%% This is NOT a comment
1409It's inside a code block
1410%%
1411```
1412
1413Content."#;
1414 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1415 let lines: Vec<_> = ctx
1416 .filtered_lines()
1417 .skip_obsidian_comments()
1418 .skip_code_blocks()
1419 .into_iter()
1420 .collect();
1421
1422 assert!(
1424 lines.iter().any(|l| l.content.contains("# Heading")),
1425 "Should include heading"
1426 );
1427 assert!(
1428 lines.iter().any(|l| l.content.contains("Content")),
1429 "Should include content after code block"
1430 );
1431 }
1432
1433 #[test]
1434 fn test_skip_obsidian_comments_in_html_comment() {
1435 let content = r#"# Heading
1437
1438<!-- %% This is inside HTML comment %% -->
1439
1440Content."#;
1441 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1442 let lines: Vec<_> = ctx
1443 .filtered_lines()
1444 .skip_obsidian_comments()
1445 .skip_html_comments()
1446 .into_iter()
1447 .collect();
1448
1449 assert!(
1450 lines.iter().any(|l| l.content.contains("# Heading")),
1451 "Should include heading"
1452 );
1453 assert!(
1454 lines.iter().any(|l| l.content.contains("Content")),
1455 "Should include content"
1456 );
1457 }
1458
1459 #[test]
1460 fn test_skip_obsidian_comments_empty() {
1461 let content = r#"# Heading
1463
1464%%%% empty comment
1465
1466Content."#;
1467 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1468 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1469
1470 assert!(
1472 lines.iter().any(|l| l.content.contains("# Heading")),
1473 "Should include heading"
1474 );
1475 }
1476
1477 #[test]
1478 fn test_skip_obsidian_comments_unclosed() {
1479 let content = r#"# Heading
1481
1482%% starts but never ends
1483This should be hidden
1484Until end of document"#;
1485 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1486 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1487
1488 assert!(
1490 lines.iter().any(|l| l.content.contains("# Heading")),
1491 "Should include heading before unclosed comment"
1492 );
1493
1494 assert!(
1496 !lines.iter().any(|l| l.content.contains("This should be hidden")),
1497 "Should exclude content in unclosed comment"
1498 );
1499 assert!(
1500 !lines.iter().any(|l| l.content.contains("Until end of document")),
1501 "Should exclude content until end of document"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_skip_obsidian_comments_multiple_on_same_line() {
1507 let content = r#"# Heading
1509
1510First %%hidden1%% middle %%hidden2%% last
1511
1512Content."#;
1513 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1514 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1515
1516 assert!(
1518 lines.iter().any(|l| l.content.contains("First")),
1519 "Should include line with multiple inline comments"
1520 );
1521 assert!(
1522 lines.iter().any(|l| l.content.contains("middle")),
1523 "Should include visible text between comments"
1524 );
1525 }
1526
1527 #[test]
1528 fn test_skip_obsidian_comments_at_start_of_line() {
1529 let content = r#"# Heading
1531
1532%%comment at start%%
1533
1534Content."#;
1535 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1536 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1537
1538 assert!(
1539 lines.iter().any(|l| l.content.contains("# Heading")),
1540 "Should include heading"
1541 );
1542 assert!(
1543 lines.iter().any(|l| l.content.contains("Content")),
1544 "Should include content"
1545 );
1546 }
1547
1548 #[test]
1549 fn test_skip_obsidian_comments_at_end_of_line() {
1550 let content = r#"# Heading
1552
1553Some text %%comment at end%%
1554
1555Content."#;
1556 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1557 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1558
1559 assert!(
1560 lines.iter().any(|l| l.content.contains("Some text")),
1561 "Should include text before comment"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_skip_obsidian_comments_with_markdown_inside() {
1567 let content = r#"# Heading
1569
1570%%
1571# hidden heading
1572[hidden link](url)
1573**hidden bold**
1574%%
1575
1576Content."#;
1577 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1578 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1579
1580 assert!(
1581 !lines.iter().any(|l| l.content.contains("# hidden heading")),
1582 "Should exclude heading inside comment"
1583 );
1584 assert!(
1585 !lines.iter().any(|l| l.content.contains("[hidden link]")),
1586 "Should exclude link inside comment"
1587 );
1588 assert!(
1589 !lines.iter().any(|l| l.content.contains("**hidden bold**")),
1590 "Should exclude bold inside comment"
1591 );
1592 }
1593
1594 #[test]
1595 fn test_skip_obsidian_comments_with_unicode() {
1596 let content = r#"# Heading
1598
1599%%日本語コメント%%
1600
1601%%Комментарий%%
1602
1603Content."#;
1604 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1605 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1606
1607 assert!(
1609 lines.iter().any(|l| l.content.contains("# Heading")),
1610 "Should include heading"
1611 );
1612 assert!(
1613 lines.iter().any(|l| l.content.contains("Content")),
1614 "Should include content"
1615 );
1616 }
1617
1618 #[test]
1619 fn test_skip_obsidian_comments_triple_percent() {
1620 let content = r#"# Heading
1622
1623%%% odd percent
1624
1625Content."#;
1626 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1627 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1628
1629 assert!(
1631 lines.iter().any(|l| l.content.contains("# Heading")),
1632 "Should include heading"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_skip_obsidian_comments_not_in_standard_flavor() {
1638 let content = r#"# Heading
1640
1641%%this is not hidden in standard%%
1642
1643Content."#;
1644 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1645 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1646
1647 assert!(
1649 lines.iter().any(|l| l.content.contains("%%this is not hidden")),
1650 "Should NOT hide %% content in Standard flavor"
1651 );
1652 }
1653
1654 #[test]
1655 fn test_skip_obsidian_comments_integration_with_other_filters() {
1656 let content = r#"---
1658title: Test
1659---
1660
1661# Heading
1662
1663```
1664code
1665```
1666
1667%%hidden comment%%
1668
1669Content."#;
1670 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1671 let lines: Vec<_> = ctx
1672 .filtered_lines()
1673 .skip_front_matter()
1674 .skip_code_blocks()
1675 .skip_obsidian_comments()
1676 .into_iter()
1677 .collect();
1678
1679 assert!(
1681 !lines.iter().any(|l| l.content.contains("title: Test")),
1682 "Should skip frontmatter"
1683 );
1684 assert!(
1685 !lines.iter().any(|l| l.content == "code"),
1686 "Should skip code block content"
1687 );
1688 assert!(
1689 lines.iter().any(|l| l.content.contains("# Heading")),
1690 "Should include heading"
1691 );
1692 assert!(
1693 lines.iter().any(|l| l.content.contains("Content")),
1694 "Should include content"
1695 );
1696 }
1697
1698 #[test]
1699 fn test_skip_obsidian_comments_whole_line_only() {
1700 let content = "start %%\nfully hidden\n%% end";
1702 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1703 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1704
1705 assert!(
1707 lines.iter().any(|l| l.content.contains("start")),
1708 "First line should be included (starts outside comment)"
1709 );
1710 assert!(
1712 !lines.iter().any(|l| l.content == "fully hidden"),
1713 "Middle line should be excluded (entirely within comment)"
1714 );
1715 assert!(
1717 lines.iter().any(|l| l.content.contains("end")),
1718 "Last line should be included (ends outside comment)"
1719 );
1720 }
1721
1722 #[test]
1723 fn test_skip_obsidian_comments_in_inline_code() {
1724 let content = r#"# Heading
1726
1727The syntax is `%%comment%%` in Obsidian.
1728
1729Content."#;
1730 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1731 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1732
1733 assert!(
1735 lines.iter().any(|l| l.content.contains("The syntax is")),
1736 "Should include line with %% in code span"
1737 );
1738 assert!(
1739 lines.iter().any(|l| l.content.contains("in Obsidian")),
1740 "Should include text after code span"
1741 );
1742 }
1743
1744 #[test]
1745 fn test_skip_obsidian_comments_in_inline_code_multi_backtick() {
1746 let content = r#"# Heading
1748
1749The syntax is ``%%comment%%`` in Obsidian.
1750
1751Content."#;
1752 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1753 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1754
1755 assert!(
1756 lines.iter().any(|l| l.content.contains("The syntax is")),
1757 "Should include line with %% in multi-backtick code span"
1758 );
1759 assert!(
1760 lines.iter().any(|l| l.content.contains("Content")),
1761 "Should include content after code span"
1762 );
1763 }
1764
1765 #[test]
1766 fn test_skip_obsidian_comments_consecutive_blocks() {
1767 let content = r#"# Heading
1769
1770%%comment 1%%
1771
1772%%comment 2%%
1773
1774Content."#;
1775 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1776 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1777
1778 assert!(
1779 lines.iter().any(|l| l.content.contains("# Heading")),
1780 "Should include heading"
1781 );
1782 assert!(
1783 lines.iter().any(|l| l.content.contains("Content")),
1784 "Should include content after comments"
1785 );
1786 }
1787
1788 #[test]
1789 fn test_skip_obsidian_comments_spanning_many_lines() {
1790 let content = r#"# Title
1792
1793%%
1794Line 1 of comment
1795Line 2 of comment
1796Line 3 of comment
1797Line 4 of comment
1798Line 5 of comment
1799%%
1800
1801After comment."#;
1802 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1803 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1804
1805 for i in 1..=5 {
1807 assert!(
1808 !lines
1809 .iter()
1810 .any(|l| l.content.contains(&format!("Line {i} of comment"))),
1811 "Should exclude line {i} of comment"
1812 );
1813 }
1814
1815 assert!(
1816 lines.iter().any(|l| l.content.contains("# Title")),
1817 "Should include title"
1818 );
1819 assert!(
1820 lines.iter().any(|l| l.content.contains("After comment")),
1821 "Should include content after comment"
1822 );
1823 }
1824
1825 #[test]
1826 fn test_obsidian_comment_line_info_field() {
1827 let content = "visible\n%%\nhidden\n%%\nvisible";
1829 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1830
1831 assert!(
1833 !ctx.lines[0].in_obsidian_comment,
1834 "Line 0 should not be marked as in_obsidian_comment"
1835 );
1836
1837 assert!(
1839 ctx.lines[2].in_obsidian_comment,
1840 "Line 2 (hidden) should be marked as in_obsidian_comment"
1841 );
1842
1843 assert!(
1845 !ctx.lines[4].in_obsidian_comment,
1846 "Line 4 should not be marked as in_obsidian_comment"
1847 );
1848 }
1849
1850 #[test]
1853 fn test_skip_pymdown_blocks_basic() {
1854 let content = r#"# Heading
1856
1857/// caption
1858Table caption here.
1859///
1860
1861Content after."#;
1862 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1863 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1864
1865 assert!(
1867 lines.iter().any(|l| l.content.contains("# Heading")),
1868 "Should include heading"
1869 );
1870 assert!(
1871 lines.iter().any(|l| l.content.contains("Content after")),
1872 "Should include content after block"
1873 );
1874
1875 assert!(
1877 !lines.iter().any(|l| l.content.contains("Table caption")),
1878 "Should exclude content inside block"
1879 );
1880 }
1881
1882 #[test]
1883 fn test_skip_pymdown_blocks_details() {
1884 let content = r#"# Heading
1886
1887/// details | Click to expand
1888 open: True
1889Hidden content here.
1890More hidden content.
1891///
1892
1893Visible content."#;
1894 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1895 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1896
1897 assert!(
1898 !lines.iter().any(|l| l.content.contains("Hidden content")),
1899 "Should exclude hidden content"
1900 );
1901 assert!(
1902 !lines.iter().any(|l| l.content.contains("open: True")),
1903 "Should exclude YAML options"
1904 );
1905 assert!(
1906 lines.iter().any(|l| l.content.contains("Visible content")),
1907 "Should include visible content"
1908 );
1909 }
1910
1911 #[test]
1912 fn test_skip_pymdown_blocks_nested() {
1913 let content = r#"# Title
1915
1916/// details | Outer
1917Outer content.
1918
1919 /// caption
1920 Inner caption.
1921 ///
1922
1923More outer content.
1924///
1925
1926After all blocks."#;
1927 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1928 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1929
1930 assert!(
1931 !lines.iter().any(|l| l.content.contains("Outer content")),
1932 "Should exclude outer block content"
1933 );
1934 assert!(
1935 !lines.iter().any(|l| l.content.contains("Inner caption")),
1936 "Should exclude inner block content"
1937 );
1938 assert!(
1939 lines.iter().any(|l| l.content.contains("After all blocks")),
1940 "Should include content after all blocks"
1941 );
1942 }
1943
1944 #[test]
1945 fn test_pymdown_block_line_info_field() {
1946 let content = "visible\n/// caption\nhidden\n///\nvisible";
1948 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1949
1950 assert!(
1952 !ctx.lines[0].in_pymdown_block,
1953 "Line 0 should not be marked as in_pymdown_block"
1954 );
1955
1956 assert!(
1958 ctx.lines[1].in_pymdown_block,
1959 "Line 1 (/// caption) should be marked as in_pymdown_block"
1960 );
1961
1962 assert!(
1964 ctx.lines[2].in_pymdown_block,
1965 "Line 2 (hidden) should be marked as in_pymdown_block"
1966 );
1967
1968 assert!(
1970 ctx.lines[3].in_pymdown_block,
1971 "Line 3 (closing ///) should be marked as in_pymdown_block"
1972 );
1973
1974 assert!(
1976 !ctx.lines[4].in_pymdown_block,
1977 "Line 4 should not be marked as in_pymdown_block"
1978 );
1979 }
1980
1981 #[test]
1982 fn test_pymdown_blocks_only_for_mkdocs_flavor() {
1983 let content = "/// caption\nCaption text\n///";
1985
1986 let ctx_mkdocs = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1988 assert!(
1989 ctx_mkdocs.lines[1].in_pymdown_block,
1990 "MkDocs flavor should detect pymdown blocks"
1991 );
1992
1993 let ctx_standard = LintContext::new(content, MarkdownFlavor::Standard, None);
1995 assert!(
1996 !ctx_standard.lines[1].in_pymdown_block,
1997 "Standard flavor should NOT detect pymdown blocks"
1998 );
1999 }
2000}