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}
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]
290 pub fn skip_kramdown_extension_blocks(mut self) -> Self {
291 self.skip_kramdown_extension_blocks = true;
292 self
293 }
294
295 #[must_use]
301 pub fn skip_div_markers(mut self) -> Self {
302 self.skip_div_markers = 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_quarto_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 }
329}
330
331pub struct FilteredLinesIter<'a> {
333 ctx: &'a LintContext<'a>,
334 config: LineFilterConfig,
335 current_index: usize,
336}
337
338impl<'a> FilteredLinesIter<'a> {
339 fn new(ctx: &'a LintContext<'a>, config: LineFilterConfig) -> Self {
341 Self {
342 ctx,
343 config,
344 current_index: 0,
345 }
346 }
347}
348
349impl<'a> Iterator for FilteredLinesIter<'a> {
350 type Item = FilteredLine<'a>;
351
352 fn next(&mut self) -> Option<Self::Item> {
353 let lines = &self.ctx.lines;
354 let raw_lines = self.ctx.raw_lines();
355
356 while self.current_index < lines.len() {
357 let idx = self.current_index;
358 self.current_index += 1;
359
360 if self.config.should_filter(&lines[idx]) {
362 continue;
363 }
364
365 let line_content = raw_lines.get(idx).copied().unwrap_or("");
367
368 return Some(FilteredLine {
370 line_num: idx + 1, line_info: &lines[idx],
372 content: line_content,
373 });
374 }
375
376 None
377 }
378}
379
380pub trait FilteredLinesExt {
385 fn filtered_lines(&self) -> FilteredLinesBuilder<'_>;
404
405 fn content_lines(&self) -> FilteredLinesIter<'_>;
428}
429
430pub struct FilteredLinesBuilder<'a> {
432 ctx: &'a LintContext<'a>,
433 config: LineFilterConfig,
434}
435
436impl<'a> FilteredLinesBuilder<'a> {
437 fn new(ctx: &'a LintContext<'a>) -> Self {
438 Self {
439 ctx,
440 config: LineFilterConfig::new(),
441 }
442 }
443
444 #[must_use]
446 pub fn skip_front_matter(mut self) -> Self {
447 self.config = self.config.skip_front_matter();
448 self
449 }
450
451 #[must_use]
453 pub fn skip_code_blocks(mut self) -> Self {
454 self.config = self.config.skip_code_blocks();
455 self
456 }
457
458 #[must_use]
460 pub fn skip_html_blocks(mut self) -> Self {
461 self.config = self.config.skip_html_blocks();
462 self
463 }
464
465 #[must_use]
467 pub fn skip_html_comments(mut self) -> Self {
468 self.config = self.config.skip_html_comments();
469 self
470 }
471
472 #[must_use]
474 pub fn skip_mkdocstrings(mut self) -> Self {
475 self.config = self.config.skip_mkdocstrings();
476 self
477 }
478
479 #[must_use]
481 pub fn skip_esm_blocks(mut self) -> Self {
482 self.config = self.config.skip_esm_blocks();
483 self
484 }
485
486 #[must_use]
488 pub fn skip_math_blocks(mut self) -> Self {
489 self.config = self.config.skip_math_blocks();
490 self
491 }
492
493 #[must_use]
495 pub fn skip_quarto_divs(mut self) -> Self {
496 self.config = self.config.skip_quarto_divs();
497 self
498 }
499
500 #[must_use]
502 pub fn skip_jsx_expressions(mut self) -> Self {
503 self.config = self.config.skip_jsx_expressions();
504 self
505 }
506
507 #[must_use]
509 pub fn skip_mdx_comments(mut self) -> Self {
510 self.config = self.config.skip_mdx_comments();
511 self
512 }
513
514 #[must_use]
516 pub fn skip_admonitions(mut self) -> Self {
517 self.config = self.config.skip_admonitions();
518 self
519 }
520
521 #[must_use]
523 pub fn skip_content_tabs(mut self) -> Self {
524 self.config = self.config.skip_content_tabs();
525 self
526 }
527
528 #[must_use]
530 pub fn skip_mkdocs_html_markdown(mut self) -> Self {
531 self.config = self.config.skip_mkdocs_html_markdown();
532 self
533 }
534
535 #[must_use]
541 pub fn skip_mkdocs_containers(mut self) -> Self {
542 self.config = self.config.skip_mkdocs_containers();
543 self
544 }
545
546 #[must_use]
548 pub fn skip_definition_lists(mut self) -> Self {
549 self.config = self.config.skip_definition_lists();
550 self
551 }
552
553 #[must_use]
555 pub fn skip_obsidian_comments(mut self) -> Self {
556 self.config = self.config.skip_obsidian_comments();
557 self
558 }
559
560 #[must_use]
562 pub fn skip_pymdown_blocks(mut self) -> Self {
563 self.config = self.config.skip_pymdown_blocks();
564 self
565 }
566
567 #[must_use]
569 pub fn skip_kramdown_extension_blocks(mut self) -> Self {
570 self.config = self.config.skip_kramdown_extension_blocks();
571 self
572 }
573
574 #[must_use]
579 pub fn skip_div_markers(mut self) -> Self {
580 self.config = self.config.skip_div_markers();
581 self
582 }
583}
584
585impl<'a> IntoIterator for FilteredLinesBuilder<'a> {
586 type Item = FilteredLine<'a>;
587 type IntoIter = FilteredLinesIter<'a>;
588
589 fn into_iter(self) -> Self::IntoIter {
590 FilteredLinesIter::new(self.ctx, self.config)
591 }
592}
593
594impl<'a> FilteredLinesExt for LintContext<'a> {
595 fn filtered_lines(&self) -> FilteredLinesBuilder<'_> {
596 FilteredLinesBuilder::new(self)
597 }
598
599 fn content_lines(&self) -> FilteredLinesIter<'_> {
600 FilteredLinesIter::new(self, LineFilterConfig::new().skip_front_matter())
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::config::MarkdownFlavor;
608
609 #[test]
610 fn test_filtered_line_structure() {
611 let content = "# Title\n\nContent";
612 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
613
614 let line = ctx.content_lines().next().unwrap();
615 assert_eq!(line.line_num, 1);
616 assert_eq!(line.content, "# Title");
617 assert!(!line.line_info.in_front_matter);
618 }
619
620 #[test]
621 fn test_skip_front_matter_yaml() {
622 let content = "---\ntitle: Test\nurl: http://example.com\n---\n\n# Content\n\nMore content";
623 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
624
625 let lines: Vec<_> = ctx.content_lines().collect();
626 assert_eq!(lines.len(), 4);
628 assert_eq!(lines[0].line_num, 5); assert_eq!(lines[0].content, "");
630 assert_eq!(lines[1].line_num, 6);
631 assert_eq!(lines[1].content, "# Content");
632 assert_eq!(lines[2].line_num, 7);
633 assert_eq!(lines[2].content, "");
634 assert_eq!(lines[3].line_num, 8);
635 assert_eq!(lines[3].content, "More content");
636 }
637
638 #[test]
639 fn test_skip_front_matter_toml() {
640 let content = "+++\ntitle = \"Test\"\nurl = \"http://example.com\"\n+++\n\n# Content";
641 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
642
643 let lines: Vec<_> = ctx.content_lines().collect();
644 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
646 assert_eq!(lines[1].line_num, 6);
647 assert_eq!(lines[1].content, "# Content");
648 }
649
650 #[test]
651 fn test_skip_front_matter_json() {
652 let content = "{\n\"title\": \"Test\",\n\"url\": \"http://example.com\"\n}\n\n# Content";
653 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
654
655 let lines: Vec<_> = ctx.content_lines().collect();
656 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
658 assert_eq!(lines[1].line_num, 6);
659 assert_eq!(lines[1].content, "# Content");
660 }
661
662 #[test]
663 fn test_skip_code_blocks() {
664 let content = "# Title\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nContent";
665 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
666
667 let lines: Vec<_> = ctx.filtered_lines().skip_code_blocks().into_iter().collect();
668
669 assert!(lines.iter().any(|l| l.content == "# Title"));
674 assert!(lines.iter().any(|l| l.content == "Content"));
675 assert!(!lines.iter().any(|l| l.content == "let x = 1;"));
677 assert!(!lines.iter().any(|l| l.content == "let y = 2;"));
678 }
679
680 #[test]
681 fn test_no_filters() {
682 let content = "---\ntitle: Test\n---\n\n# Content";
683 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
684
685 let lines: Vec<_> = ctx.filtered_lines().into_iter().collect();
687 assert_eq!(lines.len(), ctx.lines.len());
688 }
689
690 #[test]
691 fn test_multiple_filters() {
692 let content = "---\ntitle: Test\n---\n\n# Title\n\n```rust\ncode\n```\n\nContent";
693 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
694
695 let lines: Vec<_> = ctx
696 .filtered_lines()
697 .skip_front_matter()
698 .skip_code_blocks()
699 .into_iter()
700 .collect();
701
702 assert!(lines.iter().any(|l| l.content == "# Title"));
704 assert!(lines.iter().any(|l| l.content == "Content"));
705 assert!(!lines.iter().any(|l| l.content == "title: Test"));
706 assert!(!lines.iter().any(|l| l.content == "code"));
707 }
708
709 #[test]
710 fn test_line_numbering_is_1_indexed() {
711 let content = "First\nSecond\nThird";
712 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
713
714 let lines: Vec<_> = ctx.content_lines().collect();
715 assert_eq!(lines[0].line_num, 1);
716 assert_eq!(lines[0].content, "First");
717 assert_eq!(lines[1].line_num, 2);
718 assert_eq!(lines[1].content, "Second");
719 assert_eq!(lines[2].line_num, 3);
720 assert_eq!(lines[2].content, "Third");
721 }
722
723 #[test]
724 fn test_content_lines_convenience_method() {
725 let content = "---\nfoo: bar\n---\n\nContent";
726 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
727
728 let lines: Vec<_> = ctx.content_lines().collect();
730 assert!(!lines.iter().any(|l| l.content.contains("foo")));
731 assert!(lines.iter().any(|l| l.content == "Content"));
732 }
733
734 #[test]
735 fn test_empty_document() {
736 let content = "";
737 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
738
739 let lines: Vec<_> = ctx.content_lines().collect();
740 assert_eq!(lines.len(), 0);
741 }
742
743 #[test]
744 fn test_only_front_matter() {
745 let content = "---\ntitle: Test\n---";
746 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
747
748 let lines: Vec<_> = ctx.content_lines().collect();
749 assert_eq!(
750 lines.len(),
751 0,
752 "Document with only front matter should have no content lines"
753 );
754 }
755
756 #[test]
757 fn test_builder_pattern_ergonomics() {
758 let content = "# Title\n\n```\ncode\n```\n\nContent";
759 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
760
761 let _lines: Vec<_> = ctx
763 .filtered_lines()
764 .skip_front_matter()
765 .skip_code_blocks()
766 .skip_html_blocks()
767 .into_iter()
768 .collect();
769
770 }
772
773 #[test]
774 fn test_filtered_line_access_to_line_info() {
775 let content = "# Title\n\nContent";
776 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
777
778 for line in ctx.content_lines() {
779 assert!(!line.line_info.in_front_matter);
781 assert!(!line.line_info.in_code_block);
782 }
783 }
784
785 #[test]
786 fn test_skip_mkdocstrings() {
787 let content = r#"# API Documentation
788
789::: mymodule.MyClass
790 options:
791 show_root_heading: true
792 show_source: false
793
794Some regular content here.
795
796::: mymodule.function
797 options:
798 show_signature: true
799
800More content."#;
801 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
802 let lines: Vec<_> = ctx.filtered_lines().skip_mkdocstrings().into_iter().collect();
803
804 assert!(
806 lines.iter().any(|l| l.content.contains("# API Documentation")),
807 "Should include lines outside mkdocstrings blocks"
808 );
809 assert!(
810 lines.iter().any(|l| l.content.contains("Some regular content")),
811 "Should include content between mkdocstrings blocks"
812 );
813 assert!(
814 lines.iter().any(|l| l.content.contains("More content")),
815 "Should include content after mkdocstrings blocks"
816 );
817
818 assert!(
820 !lines.iter().any(|l| l.content.contains("::: mymodule")),
821 "Should exclude mkdocstrings marker lines"
822 );
823 assert!(
824 !lines.iter().any(|l| l.content.contains("show_root_heading")),
825 "Should exclude mkdocstrings option lines"
826 );
827 assert!(
828 !lines.iter().any(|l| l.content.contains("show_signature")),
829 "Should exclude all mkdocstrings option lines"
830 );
831
832 assert_eq!(lines[0].line_num, 1, "First line should be line 1");
834 }
835
836 #[test]
837 fn test_skip_esm_blocks() {
838 let content = r#"import {Chart} from './components.js'
840import {Table} from './table.js'
841export const year = 2023
842
843# Last year's snowfall
844
845Content about snowfall data.
846
847import {Footer} from './footer.js'
848
849More content."#;
850 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
851 let lines: Vec<_> = ctx.filtered_lines().skip_esm_blocks().into_iter().collect();
852
853 assert!(
855 lines.iter().any(|l| l.content.contains("# Last year's snowfall")),
856 "Should include markdown headings"
857 );
858 assert!(
859 lines.iter().any(|l| l.content.contains("Content about snowfall")),
860 "Should include markdown content"
861 );
862 assert!(
863 lines.iter().any(|l| l.content.contains("More content")),
864 "Should include content after ESM blocks"
865 );
866
867 assert!(
869 !lines.iter().any(|l| l.content.contains("import {Chart}")),
870 "Should exclude import statements at top of file"
871 );
872 assert!(
873 !lines.iter().any(|l| l.content.contains("import {Table}")),
874 "Should exclude all import statements at top of file"
875 );
876 assert!(
877 !lines.iter().any(|l| l.content.contains("export const year")),
878 "Should exclude export statements at top of file"
879 );
880 assert!(
882 !lines.iter().any(|l| l.content.contains("import {Footer}")),
883 "Should exclude import statements even after markdown content (MDX 2.0+ ESM anywhere)"
884 );
885
886 let heading_line = lines
888 .iter()
889 .find(|l| l.content.contains("# Last year's snowfall"))
890 .unwrap();
891 assert_eq!(heading_line.line_num, 5, "Heading should be on line 5");
892 }
893
894 #[test]
895 fn test_all_filters_combined() {
896 let content = r#"---
897title: Test
898---
899
900# Title
901
902```
903code
904```
905
906<!-- HTML comment here -->
907
908::: mymodule.Class
909 options:
910 show_root_heading: true
911
912<div>
913HTML block
914</div>
915
916Content"#;
917 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
918
919 let lines: Vec<_> = ctx
920 .filtered_lines()
921 .skip_front_matter()
922 .skip_code_blocks()
923 .skip_html_blocks()
924 .skip_html_comments()
925 .skip_mkdocstrings()
926 .into_iter()
927 .collect();
928
929 assert!(
931 lines.iter().any(|l| l.content == "# Title"),
932 "Should include markdown headings"
933 );
934 assert!(
935 lines.iter().any(|l| l.content == "Content"),
936 "Should include markdown content"
937 );
938
939 assert!(
941 !lines.iter().any(|l| l.content == "title: Test"),
942 "Should exclude front matter"
943 );
944 assert!(
945 !lines.iter().any(|l| l.content == "code"),
946 "Should exclude code block content"
947 );
948 assert!(
949 !lines.iter().any(|l| l.content.contains("HTML comment")),
950 "Should exclude HTML comments"
951 );
952 assert!(
953 !lines.iter().any(|l| l.content.contains("::: mymodule")),
954 "Should exclude mkdocstrings blocks"
955 );
956 assert!(
957 !lines.iter().any(|l| l.content.contains("show_root_heading")),
958 "Should exclude mkdocstrings options"
959 );
960 assert!(
961 !lines.iter().any(|l| l.content.contains("HTML block")),
962 "Should exclude HTML blocks"
963 );
964 }
965
966 #[test]
967 fn test_skip_math_blocks() {
968 let content = r#"# Heading
969
970Some regular text.
971
972$$
973A = \left[
974\begin{array}{c}
9751 \\
976-D
977\end{array}
978\right]
979$$
980
981More content after math."#;
982 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
983 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
984
985 assert!(
987 lines.iter().any(|l| l.content.contains("# Heading")),
988 "Should include markdown headings"
989 );
990 assert!(
991 lines.iter().any(|l| l.content.contains("Some regular text")),
992 "Should include regular text before math block"
993 );
994 assert!(
995 lines.iter().any(|l| l.content.contains("More content after math")),
996 "Should include content after math block"
997 );
998
999 assert!(
1001 !lines.iter().any(|l| l.content == "$$"),
1002 "Should exclude math block delimiters"
1003 );
1004 assert!(
1005 !lines.iter().any(|l| l.content.contains("\\left[")),
1006 "Should exclude LaTeX content inside math block"
1007 );
1008 assert!(
1009 !lines.iter().any(|l| l.content.contains("-D")),
1010 "Should exclude content that looks like list items inside math block"
1011 );
1012 assert!(
1013 !lines.iter().any(|l| l.content.contains("\\begin{array}")),
1014 "Should exclude LaTeX array content"
1015 );
1016 }
1017
1018 #[test]
1019 fn test_math_blocks_not_confused_with_code_blocks() {
1020 let content = r#"# Title
1021
1022```python
1023# This $$ is inside a code block
1024x = 1
1025```
1026
1027$$
1028y = 2
1029$$
1030
1031Regular text."#;
1032 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1033
1034 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
1036
1037 assert!(
1040 lines.iter().any(|l| l.content.contains("# This $$")),
1041 "Code block content with $$ should not be detected as math block"
1042 );
1043
1044 assert!(
1046 !lines.iter().any(|l| l.content == "y = 2"),
1047 "Actual math block content should be excluded"
1048 );
1049 }
1050
1051 #[test]
1052 fn test_skip_quarto_divs() {
1053 let content = r#"# Heading
1054
1055::: {.callout-note}
1056This is a callout note.
1057With multiple lines.
1058:::
1059
1060Regular text outside.
1061
1062::: {.bordered}
1063Content inside bordered div.
1064:::
1065
1066More content."#;
1067 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1068 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1069
1070 assert!(
1072 lines.iter().any(|l| l.content.contains("# Heading")),
1073 "Should include markdown headings"
1074 );
1075 assert!(
1076 lines.iter().any(|l| l.content.contains("Regular text outside")),
1077 "Should include content between divs"
1078 );
1079 assert!(
1080 lines.iter().any(|l| l.content.contains("More content")),
1081 "Should include content after divs"
1082 );
1083
1084 assert!(
1086 !lines.iter().any(|l| l.content.contains("::: {.callout-note}")),
1087 "Should exclude callout div markers"
1088 );
1089 assert!(
1090 !lines.iter().any(|l| l.content.contains("This is a callout note")),
1091 "Should exclude callout content"
1092 );
1093 assert!(
1094 !lines.iter().any(|l| l.content.contains("Content inside bordered")),
1095 "Should exclude bordered div content"
1096 );
1097 }
1098
1099 #[test]
1100 fn test_skip_jsx_expressions() {
1101 let content = r#"# MDX Document
1102
1103Here is some content with {myVariable} inline.
1104
1105{items.map(item => (
1106 <Item key={item.id} />
1107))}
1108
1109Regular paragraph after expression.
1110
1111{/* This should NOT be skipped by jsx_expressions filter */}
1112{/* MDX comments have their own filter */}
1113
1114More content."#;
1115 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1116 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1117
1118 assert!(
1120 lines.iter().any(|l| l.content.contains("# MDX Document")),
1121 "Should include markdown headings"
1122 );
1123 assert!(
1124 lines.iter().any(|l| l.content.contains("Regular paragraph")),
1125 "Should include regular paragraphs"
1126 );
1127 assert!(
1128 lines.iter().any(|l| l.content.contains("More content")),
1129 "Should include content after expressions"
1130 );
1131
1132 assert!(
1134 !lines.iter().any(|l| l.content.contains("{myVariable}")),
1135 "Should exclude lines with inline JSX expressions"
1136 );
1137 assert!(
1138 !lines.iter().any(|l| l.content.contains("items.map")),
1139 "Should exclude multi-line JSX expression content"
1140 );
1141 assert!(
1142 !lines.iter().any(|l| l.content.contains("<Item key")),
1143 "Should exclude JSX inside expressions"
1144 );
1145 }
1146
1147 #[test]
1148 fn test_skip_quarto_divs_nested() {
1149 let content = r#"# Title
1150
1151::: {.outer}
1152Outer content.
1153
1154::: {.inner}
1155Inner content.
1156:::
1157
1158Back to outer.
1159:::
1160
1161Outside text."#;
1162 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1163 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1164
1165 assert!(
1167 lines.iter().any(|l| l.content.contains("# Title")),
1168 "Should include heading"
1169 );
1170 assert!(
1171 lines.iter().any(|l| l.content.contains("Outside text")),
1172 "Should include text after divs"
1173 );
1174
1175 assert!(
1177 !lines.iter().any(|l| l.content.contains("Outer content")),
1178 "Should exclude outer div content"
1179 );
1180 assert!(
1181 !lines.iter().any(|l| l.content.contains("Inner content")),
1182 "Should exclude inner div content"
1183 );
1184 }
1185
1186 #[test]
1187 fn test_skip_quarto_divs_not_in_standard_flavor() {
1188 let content = r#"::: {.callout-note}
1189This should NOT be skipped in standard flavor.
1190:::"#;
1191 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1192 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1193
1194 assert!(
1196 lines.iter().any(|l| l.content.contains("This should NOT be skipped")),
1197 "Standard flavor should not detect Quarto divs"
1198 );
1199 }
1200
1201 #[test]
1202 fn test_skip_mdx_comments() {
1203 let content = r#"# MDX Document
1204
1205{/* This is an MDX comment */}
1206
1207Regular content here.
1208
1209{/*
1210 Multi-line
1211 MDX comment
1212*/}
1213
1214More content after comment."#;
1215 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1216 let lines: Vec<_> = ctx.filtered_lines().skip_mdx_comments().into_iter().collect();
1217
1218 assert!(
1220 lines.iter().any(|l| l.content.contains("# MDX Document")),
1221 "Should include markdown headings"
1222 );
1223 assert!(
1224 lines.iter().any(|l| l.content.contains("Regular content")),
1225 "Should include regular content"
1226 );
1227 assert!(
1228 lines.iter().any(|l| l.content.contains("More content")),
1229 "Should include content after comments"
1230 );
1231
1232 assert!(
1234 !lines.iter().any(|l| l.content.contains("{/* This is")),
1235 "Should exclude single-line MDX comments"
1236 );
1237 assert!(
1238 !lines.iter().any(|l| l.content.contains("Multi-line")),
1239 "Should exclude multi-line MDX comment content"
1240 );
1241 }
1242
1243 #[test]
1244 fn test_jsx_expressions_with_nested_braces() {
1245 let content = r#"# Document
1247
1248{props.style || {color: "red", background: "blue"}}
1249
1250Regular content."#;
1251 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1252 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1253
1254 assert!(
1256 !lines.iter().any(|l| l.content.contains("props.style")),
1257 "Should exclude JSX expression with nested braces"
1258 );
1259 assert!(
1260 lines.iter().any(|l| l.content.contains("Regular content")),
1261 "Should include content after nested expression"
1262 );
1263 }
1264
1265 #[test]
1266 fn test_jsx_and_mdx_comments_combined() {
1267 let content = r#"# Title
1269
1270{variable}
1271
1272{/* comment */}
1273
1274Content."#;
1275 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1276 let lines: Vec<_> = ctx
1277 .filtered_lines()
1278 .skip_jsx_expressions()
1279 .skip_mdx_comments()
1280 .into_iter()
1281 .collect();
1282
1283 assert!(
1284 lines.iter().any(|l| l.content.contains("# Title")),
1285 "Should include heading"
1286 );
1287 assert!(
1288 lines.iter().any(|l| l.content.contains("Content")),
1289 "Should include regular content"
1290 );
1291 assert!(
1292 !lines.iter().any(|l| l.content.contains("{variable}")),
1293 "Should exclude JSX expression"
1294 );
1295 assert!(
1296 !lines.iter().any(|l| l.content.contains("{/* comment */")),
1297 "Should exclude MDX comment"
1298 );
1299 }
1300
1301 #[test]
1302 fn test_jsx_expressions_not_detected_in_standard_flavor() {
1303 let content = r#"# Document
1305
1306{this is not JSX in standard markdown}
1307
1308Content."#;
1309 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1310 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1311
1312 assert!(
1314 lines.iter().any(|l| l.content.contains("{this is not JSX")),
1315 "Should NOT exclude brace content in standard markdown"
1316 );
1317 }
1318
1319 #[test]
1322 fn test_skip_obsidian_comments_simple_inline() {
1323 let content = r#"# Heading
1325
1326This is visible %%this is hidden%% and visible again.
1327
1328More content."#;
1329 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1330 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1331
1332 assert!(
1334 lines.iter().any(|l| l.content.contains("# Heading")),
1335 "Should include heading"
1336 );
1337 assert!(
1338 lines.iter().any(|l| l.content.contains("This is visible")),
1339 "Should include line with inline comment"
1340 );
1341 assert!(
1342 lines.iter().any(|l| l.content.contains("More content")),
1343 "Should include content after comment"
1344 );
1345 }
1346
1347 #[test]
1348 fn test_skip_obsidian_comments_multiline_block() {
1349 let content = r#"# Heading
1351
1352%%
1353This is a multi-line
1354comment block
1355%%
1356
1357Content after."#;
1358 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1359 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1360
1361 assert!(
1363 lines.iter().any(|l| l.content.contains("# Heading")),
1364 "Should include heading"
1365 );
1366 assert!(
1367 lines.iter().any(|l| l.content.contains("Content after")),
1368 "Should include content after comment block"
1369 );
1370
1371 assert!(
1373 !lines.iter().any(|l| l.content.contains("This is a multi-line")),
1374 "Should exclude multi-line comment content"
1375 );
1376 assert!(
1377 !lines.iter().any(|l| l.content.contains("comment block")),
1378 "Should exclude multi-line comment content"
1379 );
1380 }
1381
1382 #[test]
1383 fn test_skip_obsidian_comments_in_code_block() {
1384 let content = r#"# Heading
1386
1387```
1388%% This is NOT a comment
1389It's inside a code block
1390%%
1391```
1392
1393Content."#;
1394 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1395 let lines: Vec<_> = ctx
1396 .filtered_lines()
1397 .skip_obsidian_comments()
1398 .skip_code_blocks()
1399 .into_iter()
1400 .collect();
1401
1402 assert!(
1404 lines.iter().any(|l| l.content.contains("# Heading")),
1405 "Should include heading"
1406 );
1407 assert!(
1408 lines.iter().any(|l| l.content.contains("Content")),
1409 "Should include content after code block"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_skip_obsidian_comments_in_html_comment() {
1415 let content = r#"# Heading
1417
1418<!-- %% This is inside HTML comment %% -->
1419
1420Content."#;
1421 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1422 let lines: Vec<_> = ctx
1423 .filtered_lines()
1424 .skip_obsidian_comments()
1425 .skip_html_comments()
1426 .into_iter()
1427 .collect();
1428
1429 assert!(
1430 lines.iter().any(|l| l.content.contains("# Heading")),
1431 "Should include heading"
1432 );
1433 assert!(
1434 lines.iter().any(|l| l.content.contains("Content")),
1435 "Should include content"
1436 );
1437 }
1438
1439 #[test]
1440 fn test_skip_obsidian_comments_empty() {
1441 let content = r#"# Heading
1443
1444%%%% empty comment
1445
1446Content."#;
1447 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1448 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1449
1450 assert!(
1452 lines.iter().any(|l| l.content.contains("# Heading")),
1453 "Should include heading"
1454 );
1455 }
1456
1457 #[test]
1458 fn test_skip_obsidian_comments_unclosed() {
1459 let content = r#"# Heading
1461
1462%% starts but never ends
1463This should be hidden
1464Until end of document"#;
1465 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1466 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1467
1468 assert!(
1470 lines.iter().any(|l| l.content.contains("# Heading")),
1471 "Should include heading before unclosed comment"
1472 );
1473
1474 assert!(
1476 !lines.iter().any(|l| l.content.contains("This should be hidden")),
1477 "Should exclude content in unclosed comment"
1478 );
1479 assert!(
1480 !lines.iter().any(|l| l.content.contains("Until end of document")),
1481 "Should exclude content until end of document"
1482 );
1483 }
1484
1485 #[test]
1486 fn test_skip_obsidian_comments_multiple_on_same_line() {
1487 let content = r#"# Heading
1489
1490First %%hidden1%% middle %%hidden2%% last
1491
1492Content."#;
1493 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1494 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1495
1496 assert!(
1498 lines.iter().any(|l| l.content.contains("First")),
1499 "Should include line with multiple inline comments"
1500 );
1501 assert!(
1502 lines.iter().any(|l| l.content.contains("middle")),
1503 "Should include visible text between comments"
1504 );
1505 }
1506
1507 #[test]
1508 fn test_skip_obsidian_comments_at_start_of_line() {
1509 let content = r#"# Heading
1511
1512%%comment at start%%
1513
1514Content."#;
1515 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1516 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1517
1518 assert!(
1519 lines.iter().any(|l| l.content.contains("# Heading")),
1520 "Should include heading"
1521 );
1522 assert!(
1523 lines.iter().any(|l| l.content.contains("Content")),
1524 "Should include content"
1525 );
1526 }
1527
1528 #[test]
1529 fn test_skip_obsidian_comments_at_end_of_line() {
1530 let content = r#"# Heading
1532
1533Some text %%comment at end%%
1534
1535Content."#;
1536 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1537 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1538
1539 assert!(
1540 lines.iter().any(|l| l.content.contains("Some text")),
1541 "Should include text before comment"
1542 );
1543 }
1544
1545 #[test]
1546 fn test_skip_obsidian_comments_with_markdown_inside() {
1547 let content = r#"# Heading
1549
1550%%
1551# hidden heading
1552[hidden link](url)
1553**hidden bold**
1554%%
1555
1556Content."#;
1557 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1558 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1559
1560 assert!(
1561 !lines.iter().any(|l| l.content.contains("# hidden heading")),
1562 "Should exclude heading inside comment"
1563 );
1564 assert!(
1565 !lines.iter().any(|l| l.content.contains("[hidden link]")),
1566 "Should exclude link inside comment"
1567 );
1568 assert!(
1569 !lines.iter().any(|l| l.content.contains("**hidden bold**")),
1570 "Should exclude bold inside comment"
1571 );
1572 }
1573
1574 #[test]
1575 fn test_skip_obsidian_comments_with_unicode() {
1576 let content = r#"# Heading
1578
1579%%日本語コメント%%
1580
1581%%Комментарий%%
1582
1583Content."#;
1584 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1585 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1586
1587 assert!(
1589 lines.iter().any(|l| l.content.contains("# Heading")),
1590 "Should include heading"
1591 );
1592 assert!(
1593 lines.iter().any(|l| l.content.contains("Content")),
1594 "Should include content"
1595 );
1596 }
1597
1598 #[test]
1599 fn test_skip_obsidian_comments_triple_percent() {
1600 let content = r#"# Heading
1602
1603%%% odd percent
1604
1605Content."#;
1606 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1607 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1608
1609 assert!(
1611 lines.iter().any(|l| l.content.contains("# Heading")),
1612 "Should include heading"
1613 );
1614 }
1615
1616 #[test]
1617 fn test_skip_obsidian_comments_not_in_standard_flavor() {
1618 let content = r#"# Heading
1620
1621%%this is not hidden in standard%%
1622
1623Content."#;
1624 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1625 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1626
1627 assert!(
1629 lines.iter().any(|l| l.content.contains("%%this is not hidden")),
1630 "Should NOT hide %% content in Standard flavor"
1631 );
1632 }
1633
1634 #[test]
1635 fn test_skip_obsidian_comments_integration_with_other_filters() {
1636 let content = r#"---
1638title: Test
1639---
1640
1641# Heading
1642
1643```
1644code
1645```
1646
1647%%hidden comment%%
1648
1649Content."#;
1650 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1651 let lines: Vec<_> = ctx
1652 .filtered_lines()
1653 .skip_front_matter()
1654 .skip_code_blocks()
1655 .skip_obsidian_comments()
1656 .into_iter()
1657 .collect();
1658
1659 assert!(
1661 !lines.iter().any(|l| l.content.contains("title: Test")),
1662 "Should skip frontmatter"
1663 );
1664 assert!(
1665 !lines.iter().any(|l| l.content == "code"),
1666 "Should skip code block content"
1667 );
1668 assert!(
1669 lines.iter().any(|l| l.content.contains("# Heading")),
1670 "Should include heading"
1671 );
1672 assert!(
1673 lines.iter().any(|l| l.content.contains("Content")),
1674 "Should include content"
1675 );
1676 }
1677
1678 #[test]
1679 fn test_skip_obsidian_comments_whole_line_only() {
1680 let content = "start %%\nfully hidden\n%% end";
1682 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1683 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1684
1685 assert!(
1687 lines.iter().any(|l| l.content.contains("start")),
1688 "First line should be included (starts outside comment)"
1689 );
1690 assert!(
1692 !lines.iter().any(|l| l.content == "fully hidden"),
1693 "Middle line should be excluded (entirely within comment)"
1694 );
1695 assert!(
1697 lines.iter().any(|l| l.content.contains("end")),
1698 "Last line should be included (ends outside comment)"
1699 );
1700 }
1701
1702 #[test]
1703 fn test_skip_obsidian_comments_in_inline_code() {
1704 let content = r#"# Heading
1706
1707The syntax is `%%comment%%` in Obsidian.
1708
1709Content."#;
1710 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1711 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1712
1713 assert!(
1715 lines.iter().any(|l| l.content.contains("The syntax is")),
1716 "Should include line with %% in code span"
1717 );
1718 assert!(
1719 lines.iter().any(|l| l.content.contains("in Obsidian")),
1720 "Should include text after code span"
1721 );
1722 }
1723
1724 #[test]
1725 fn test_skip_obsidian_comments_in_inline_code_multi_backtick() {
1726 let content = r#"# Heading
1728
1729The syntax is ``%%comment%%`` in Obsidian.
1730
1731Content."#;
1732 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1733 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1734
1735 assert!(
1736 lines.iter().any(|l| l.content.contains("The syntax is")),
1737 "Should include line with %% in multi-backtick code span"
1738 );
1739 assert!(
1740 lines.iter().any(|l| l.content.contains("Content")),
1741 "Should include content after code span"
1742 );
1743 }
1744
1745 #[test]
1746 fn test_skip_obsidian_comments_consecutive_blocks() {
1747 let content = r#"# Heading
1749
1750%%comment 1%%
1751
1752%%comment 2%%
1753
1754Content."#;
1755 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1756 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1757
1758 assert!(
1759 lines.iter().any(|l| l.content.contains("# Heading")),
1760 "Should include heading"
1761 );
1762 assert!(
1763 lines.iter().any(|l| l.content.contains("Content")),
1764 "Should include content after comments"
1765 );
1766 }
1767
1768 #[test]
1769 fn test_skip_obsidian_comments_spanning_many_lines() {
1770 let content = r#"# Title
1772
1773%%
1774Line 1 of comment
1775Line 2 of comment
1776Line 3 of comment
1777Line 4 of comment
1778Line 5 of comment
1779%%
1780
1781After comment."#;
1782 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1783 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1784
1785 for i in 1..=5 {
1787 assert!(
1788 !lines
1789 .iter()
1790 .any(|l| l.content.contains(&format!("Line {i} of comment"))),
1791 "Should exclude line {i} of comment"
1792 );
1793 }
1794
1795 assert!(
1796 lines.iter().any(|l| l.content.contains("# Title")),
1797 "Should include title"
1798 );
1799 assert!(
1800 lines.iter().any(|l| l.content.contains("After comment")),
1801 "Should include content after comment"
1802 );
1803 }
1804
1805 #[test]
1806 fn test_obsidian_comment_line_info_field() {
1807 let content = "visible\n%%\nhidden\n%%\nvisible";
1809 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1810
1811 assert!(
1813 !ctx.lines[0].in_obsidian_comment,
1814 "Line 0 should not be marked as in_obsidian_comment"
1815 );
1816
1817 assert!(
1819 ctx.lines[2].in_obsidian_comment,
1820 "Line 2 (hidden) should be marked as in_obsidian_comment"
1821 );
1822
1823 assert!(
1825 !ctx.lines[4].in_obsidian_comment,
1826 "Line 4 should not be marked as in_obsidian_comment"
1827 );
1828 }
1829
1830 #[test]
1833 fn test_skip_pymdown_blocks_basic() {
1834 let content = r#"# Heading
1836
1837/// caption
1838Table caption here.
1839///
1840
1841Content after."#;
1842 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1843 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1844
1845 assert!(
1847 lines.iter().any(|l| l.content.contains("# Heading")),
1848 "Should include heading"
1849 );
1850 assert!(
1851 lines.iter().any(|l| l.content.contains("Content after")),
1852 "Should include content after block"
1853 );
1854
1855 assert!(
1857 !lines.iter().any(|l| l.content.contains("Table caption")),
1858 "Should exclude content inside block"
1859 );
1860 }
1861
1862 #[test]
1863 fn test_skip_pymdown_blocks_details() {
1864 let content = r#"# Heading
1866
1867/// details | Click to expand
1868 open: True
1869Hidden content here.
1870More hidden content.
1871///
1872
1873Visible content."#;
1874 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1875 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1876
1877 assert!(
1878 !lines.iter().any(|l| l.content.contains("Hidden content")),
1879 "Should exclude hidden content"
1880 );
1881 assert!(
1882 !lines.iter().any(|l| l.content.contains("open: True")),
1883 "Should exclude YAML options"
1884 );
1885 assert!(
1886 lines.iter().any(|l| l.content.contains("Visible content")),
1887 "Should include visible content"
1888 );
1889 }
1890
1891 #[test]
1892 fn test_skip_pymdown_blocks_nested() {
1893 let content = r#"# Title
1895
1896/// details | Outer
1897Outer content.
1898
1899 /// caption
1900 Inner caption.
1901 ///
1902
1903More outer content.
1904///
1905
1906After all blocks."#;
1907 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1908 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1909
1910 assert!(
1911 !lines.iter().any(|l| l.content.contains("Outer content")),
1912 "Should exclude outer block content"
1913 );
1914 assert!(
1915 !lines.iter().any(|l| l.content.contains("Inner caption")),
1916 "Should exclude inner block content"
1917 );
1918 assert!(
1919 lines.iter().any(|l| l.content.contains("After all blocks")),
1920 "Should include content after all blocks"
1921 );
1922 }
1923
1924 #[test]
1925 fn test_pymdown_block_line_info_field() {
1926 let content = "visible\n/// caption\nhidden\n///\nvisible";
1928 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1929
1930 assert!(
1932 !ctx.lines[0].in_pymdown_block,
1933 "Line 0 should not be marked as in_pymdown_block"
1934 );
1935
1936 assert!(
1938 ctx.lines[1].in_pymdown_block,
1939 "Line 1 (/// caption) should be marked as in_pymdown_block"
1940 );
1941
1942 assert!(
1944 ctx.lines[2].in_pymdown_block,
1945 "Line 2 (hidden) should be marked as in_pymdown_block"
1946 );
1947
1948 assert!(
1950 ctx.lines[3].in_pymdown_block,
1951 "Line 3 (closing ///) should be marked as in_pymdown_block"
1952 );
1953
1954 assert!(
1956 !ctx.lines[4].in_pymdown_block,
1957 "Line 4 should not be marked as in_pymdown_block"
1958 );
1959 }
1960
1961 #[test]
1962 fn test_pymdown_blocks_only_for_mkdocs_flavor() {
1963 let content = "/// caption\nCaption text\n///";
1965
1966 let ctx_mkdocs = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1968 assert!(
1969 ctx_mkdocs.lines[1].in_pymdown_block,
1970 "MkDocs flavor should detect pymdown blocks"
1971 );
1972
1973 let ctx_standard = LintContext::new(content, MarkdownFlavor::Standard, None);
1975 assert!(
1976 !ctx_standard.lines[1].in_pymdown_block,
1977 "Standard flavor should NOT detect pymdown blocks"
1978 );
1979 }
1980}