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}
99
100impl LineFilterConfig {
101 #[must_use]
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 #[must_use]
112 pub fn skip_front_matter(mut self) -> Self {
113 self.skip_front_matter = true;
114 self
115 }
116
117 #[must_use]
122 pub fn skip_code_blocks(mut self) -> Self {
123 self.skip_code_blocks = true;
124 self
125 }
126
127 #[must_use]
132 pub fn skip_html_blocks(mut self) -> Self {
133 self.skip_html_blocks = true;
134 self
135 }
136
137 #[must_use]
142 pub fn skip_html_comments(mut self) -> Self {
143 self.skip_html_comments = true;
144 self
145 }
146
147 #[must_use]
152 pub fn skip_mkdocstrings(mut self) -> Self {
153 self.skip_mkdocstrings = true;
154 self
155 }
156
157 #[must_use]
162 pub fn skip_esm_blocks(mut self) -> Self {
163 self.skip_esm_blocks = true;
164 self
165 }
166
167 #[must_use]
172 pub fn skip_math_blocks(mut self) -> Self {
173 self.skip_math_blocks = true;
174 self
175 }
176
177 #[must_use]
182 pub fn skip_quarto_divs(mut self) -> Self {
183 self.skip_quarto_divs = true;
184 self
185 }
186
187 #[must_use]
192 pub fn skip_jsx_expressions(mut self) -> Self {
193 self.skip_jsx_expressions = true;
194 self
195 }
196
197 #[must_use]
202 pub fn skip_mdx_comments(mut self) -> Self {
203 self.skip_mdx_comments = true;
204 self
205 }
206
207 #[must_use]
212 pub fn skip_admonitions(mut self) -> Self {
213 self.skip_admonitions = true;
214 self
215 }
216
217 #[must_use]
221 pub fn skip_content_tabs(mut self) -> Self {
222 self.skip_content_tabs = true;
223 self
224 }
225
226 #[must_use]
230 pub fn skip_mkdocs_html_markdown(mut self) -> Self {
231 self.skip_mkdocs_html_markdown = true;
232 self
233 }
234
235 #[must_use]
241 pub fn skip_mkdocs_containers(mut self) -> Self {
242 self.skip_admonitions = true;
243 self.skip_content_tabs = true;
244 self.skip_mkdocs_html_markdown = true;
245 self
246 }
247
248 #[must_use]
253 pub fn skip_definition_lists(mut self) -> Self {
254 self.skip_definition_lists = true;
255 self
256 }
257
258 #[must_use]
263 pub fn skip_obsidian_comments(mut self) -> Self {
264 self.skip_obsidian_comments = true;
265 self
266 }
267
268 #[must_use]
274 pub fn skip_pymdown_blocks(mut self) -> Self {
275 self.skip_pymdown_blocks = true;
276 self
277 }
278
279 fn should_filter(&self, line_info: &LineInfo) -> bool {
281 (self.skip_front_matter && line_info.in_front_matter)
282 || (self.skip_code_blocks && line_info.in_code_block)
283 || (self.skip_html_blocks && line_info.in_html_block)
284 || (self.skip_html_comments && line_info.in_html_comment)
285 || (self.skip_mkdocstrings && line_info.in_mkdocstrings)
286 || (self.skip_esm_blocks && line_info.in_esm_block)
287 || (self.skip_math_blocks && line_info.in_math_block)
288 || (self.skip_quarto_divs && line_info.in_quarto_div)
289 || (self.skip_jsx_expressions && line_info.in_jsx_expression)
290 || (self.skip_mdx_comments && line_info.in_mdx_comment)
291 || (self.skip_admonitions && line_info.in_admonition)
292 || (self.skip_content_tabs && line_info.in_content_tab)
293 || (self.skip_mkdocs_html_markdown && line_info.in_mkdocs_html_markdown)
294 || (self.skip_definition_lists && line_info.in_definition_list)
295 || (self.skip_obsidian_comments && line_info.in_obsidian_comment)
296 || (self.skip_pymdown_blocks && line_info.in_pymdown_block)
297 }
298}
299
300pub struct FilteredLinesIter<'a> {
302 ctx: &'a LintContext<'a>,
303 config: LineFilterConfig,
304 current_index: usize,
305}
306
307impl<'a> FilteredLinesIter<'a> {
308 fn new(ctx: &'a LintContext<'a>, config: LineFilterConfig) -> Self {
310 Self {
311 ctx,
312 config,
313 current_index: 0,
314 }
315 }
316}
317
318impl<'a> Iterator for FilteredLinesIter<'a> {
319 type Item = FilteredLine<'a>;
320
321 fn next(&mut self) -> Option<Self::Item> {
322 let lines = &self.ctx.lines;
323 let raw_lines = self.ctx.raw_lines();
324
325 while self.current_index < lines.len() {
326 let idx = self.current_index;
327 self.current_index += 1;
328
329 if self.config.should_filter(&lines[idx]) {
331 continue;
332 }
333
334 let line_content = raw_lines.get(idx).copied().unwrap_or("");
336
337 return Some(FilteredLine {
339 line_num: idx + 1, line_info: &lines[idx],
341 content: line_content,
342 });
343 }
344
345 None
346 }
347}
348
349pub trait FilteredLinesExt {
354 fn filtered_lines(&self) -> FilteredLinesBuilder<'_>;
373
374 fn content_lines(&self) -> FilteredLinesIter<'_>;
397}
398
399pub struct FilteredLinesBuilder<'a> {
401 ctx: &'a LintContext<'a>,
402 config: LineFilterConfig,
403}
404
405impl<'a> FilteredLinesBuilder<'a> {
406 fn new(ctx: &'a LintContext<'a>) -> Self {
407 Self {
408 ctx,
409 config: LineFilterConfig::new(),
410 }
411 }
412
413 #[must_use]
415 pub fn skip_front_matter(mut self) -> Self {
416 self.config = self.config.skip_front_matter();
417 self
418 }
419
420 #[must_use]
422 pub fn skip_code_blocks(mut self) -> Self {
423 self.config = self.config.skip_code_blocks();
424 self
425 }
426
427 #[must_use]
429 pub fn skip_html_blocks(mut self) -> Self {
430 self.config = self.config.skip_html_blocks();
431 self
432 }
433
434 #[must_use]
436 pub fn skip_html_comments(mut self) -> Self {
437 self.config = self.config.skip_html_comments();
438 self
439 }
440
441 #[must_use]
443 pub fn skip_mkdocstrings(mut self) -> Self {
444 self.config = self.config.skip_mkdocstrings();
445 self
446 }
447
448 #[must_use]
450 pub fn skip_esm_blocks(mut self) -> Self {
451 self.config = self.config.skip_esm_blocks();
452 self
453 }
454
455 #[must_use]
457 pub fn skip_math_blocks(mut self) -> Self {
458 self.config = self.config.skip_math_blocks();
459 self
460 }
461
462 #[must_use]
464 pub fn skip_quarto_divs(mut self) -> Self {
465 self.config = self.config.skip_quarto_divs();
466 self
467 }
468
469 #[must_use]
471 pub fn skip_jsx_expressions(mut self) -> Self {
472 self.config = self.config.skip_jsx_expressions();
473 self
474 }
475
476 #[must_use]
478 pub fn skip_mdx_comments(mut self) -> Self {
479 self.config = self.config.skip_mdx_comments();
480 self
481 }
482
483 #[must_use]
485 pub fn skip_admonitions(mut self) -> Self {
486 self.config = self.config.skip_admonitions();
487 self
488 }
489
490 #[must_use]
492 pub fn skip_content_tabs(mut self) -> Self {
493 self.config = self.config.skip_content_tabs();
494 self
495 }
496
497 #[must_use]
499 pub fn skip_mkdocs_html_markdown(mut self) -> Self {
500 self.config = self.config.skip_mkdocs_html_markdown();
501 self
502 }
503
504 #[must_use]
510 pub fn skip_mkdocs_containers(mut self) -> Self {
511 self.config = self.config.skip_mkdocs_containers();
512 self
513 }
514
515 #[must_use]
517 pub fn skip_definition_lists(mut self) -> Self {
518 self.config = self.config.skip_definition_lists();
519 self
520 }
521
522 #[must_use]
524 pub fn skip_obsidian_comments(mut self) -> Self {
525 self.config = self.config.skip_obsidian_comments();
526 self
527 }
528
529 #[must_use]
531 pub fn skip_pymdown_blocks(mut self) -> Self {
532 self.config = self.config.skip_pymdown_blocks();
533 self
534 }
535}
536
537impl<'a> IntoIterator for FilteredLinesBuilder<'a> {
538 type Item = FilteredLine<'a>;
539 type IntoIter = FilteredLinesIter<'a>;
540
541 fn into_iter(self) -> Self::IntoIter {
542 FilteredLinesIter::new(self.ctx, self.config)
543 }
544}
545
546impl<'a> FilteredLinesExt for LintContext<'a> {
547 fn filtered_lines(&self) -> FilteredLinesBuilder<'_> {
548 FilteredLinesBuilder::new(self)
549 }
550
551 fn content_lines(&self) -> FilteredLinesIter<'_> {
552 FilteredLinesIter::new(self, LineFilterConfig::new().skip_front_matter())
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::config::MarkdownFlavor;
560
561 #[test]
562 fn test_filtered_line_structure() {
563 let content = "# Title\n\nContent";
564 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
565
566 let line = ctx.content_lines().next().unwrap();
567 assert_eq!(line.line_num, 1);
568 assert_eq!(line.content, "# Title");
569 assert!(!line.line_info.in_front_matter);
570 }
571
572 #[test]
573 fn test_skip_front_matter_yaml() {
574 let content = "---\ntitle: Test\nurl: http://example.com\n---\n\n# Content\n\nMore content";
575 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
576
577 let lines: Vec<_> = ctx.content_lines().collect();
578 assert_eq!(lines.len(), 4);
580 assert_eq!(lines[0].line_num, 5); assert_eq!(lines[0].content, "");
582 assert_eq!(lines[1].line_num, 6);
583 assert_eq!(lines[1].content, "# Content");
584 assert_eq!(lines[2].line_num, 7);
585 assert_eq!(lines[2].content, "");
586 assert_eq!(lines[3].line_num, 8);
587 assert_eq!(lines[3].content, "More content");
588 }
589
590 #[test]
591 fn test_skip_front_matter_toml() {
592 let content = "+++\ntitle = \"Test\"\nurl = \"http://example.com\"\n+++\n\n# Content";
593 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
594
595 let lines: Vec<_> = ctx.content_lines().collect();
596 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
598 assert_eq!(lines[1].line_num, 6);
599 assert_eq!(lines[1].content, "# Content");
600 }
601
602 #[test]
603 fn test_skip_front_matter_json() {
604 let content = "{\n\"title\": \"Test\",\n\"url\": \"http://example.com\"\n}\n\n# Content";
605 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
606
607 let lines: Vec<_> = ctx.content_lines().collect();
608 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
610 assert_eq!(lines[1].line_num, 6);
611 assert_eq!(lines[1].content, "# Content");
612 }
613
614 #[test]
615 fn test_skip_code_blocks() {
616 let content = "# Title\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nContent";
617 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
618
619 let lines: Vec<_> = ctx.filtered_lines().skip_code_blocks().into_iter().collect();
620
621 assert!(lines.iter().any(|l| l.content == "# Title"));
626 assert!(lines.iter().any(|l| l.content == "Content"));
627 assert!(!lines.iter().any(|l| l.content == "let x = 1;"));
629 assert!(!lines.iter().any(|l| l.content == "let y = 2;"));
630 }
631
632 #[test]
633 fn test_no_filters() {
634 let content = "---\ntitle: Test\n---\n\n# Content";
635 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
636
637 let lines: Vec<_> = ctx.filtered_lines().into_iter().collect();
639 assert_eq!(lines.len(), ctx.lines.len());
640 }
641
642 #[test]
643 fn test_multiple_filters() {
644 let content = "---\ntitle: Test\n---\n\n# Title\n\n```rust\ncode\n```\n\nContent";
645 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
646
647 let lines: Vec<_> = ctx
648 .filtered_lines()
649 .skip_front_matter()
650 .skip_code_blocks()
651 .into_iter()
652 .collect();
653
654 assert!(lines.iter().any(|l| l.content == "# Title"));
656 assert!(lines.iter().any(|l| l.content == "Content"));
657 assert!(!lines.iter().any(|l| l.content == "title: Test"));
658 assert!(!lines.iter().any(|l| l.content == "code"));
659 }
660
661 #[test]
662 fn test_line_numbering_is_1_indexed() {
663 let content = "First\nSecond\nThird";
664 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
665
666 let lines: Vec<_> = ctx.content_lines().collect();
667 assert_eq!(lines[0].line_num, 1);
668 assert_eq!(lines[0].content, "First");
669 assert_eq!(lines[1].line_num, 2);
670 assert_eq!(lines[1].content, "Second");
671 assert_eq!(lines[2].line_num, 3);
672 assert_eq!(lines[2].content, "Third");
673 }
674
675 #[test]
676 fn test_content_lines_convenience_method() {
677 let content = "---\nfoo: bar\n---\n\nContent";
678 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
679
680 let lines: Vec<_> = ctx.content_lines().collect();
682 assert!(!lines.iter().any(|l| l.content.contains("foo")));
683 assert!(lines.iter().any(|l| l.content == "Content"));
684 }
685
686 #[test]
687 fn test_empty_document() {
688 let content = "";
689 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
690
691 let lines: Vec<_> = ctx.content_lines().collect();
692 assert_eq!(lines.len(), 0);
693 }
694
695 #[test]
696 fn test_only_front_matter() {
697 let content = "---\ntitle: Test\n---";
698 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
699
700 let lines: Vec<_> = ctx.content_lines().collect();
701 assert_eq!(
702 lines.len(),
703 0,
704 "Document with only front matter should have no content lines"
705 );
706 }
707
708 #[test]
709 fn test_builder_pattern_ergonomics() {
710 let content = "# Title\n\n```\ncode\n```\n\nContent";
711 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
712
713 let _lines: Vec<_> = ctx
715 .filtered_lines()
716 .skip_front_matter()
717 .skip_code_blocks()
718 .skip_html_blocks()
719 .into_iter()
720 .collect();
721
722 }
724
725 #[test]
726 fn test_filtered_line_access_to_line_info() {
727 let content = "# Title\n\nContent";
728 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
729
730 for line in ctx.content_lines() {
731 assert!(!line.line_info.in_front_matter);
733 assert!(!line.line_info.in_code_block);
734 }
735 }
736
737 #[test]
738 fn test_skip_mkdocstrings() {
739 let content = r#"# API Documentation
740
741::: mymodule.MyClass
742 options:
743 show_root_heading: true
744 show_source: false
745
746Some regular content here.
747
748::: mymodule.function
749 options:
750 show_signature: true
751
752More content."#;
753 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
754 let lines: Vec<_> = ctx.filtered_lines().skip_mkdocstrings().into_iter().collect();
755
756 assert!(
758 lines.iter().any(|l| l.content.contains("# API Documentation")),
759 "Should include lines outside mkdocstrings blocks"
760 );
761 assert!(
762 lines.iter().any(|l| l.content.contains("Some regular content")),
763 "Should include content between mkdocstrings blocks"
764 );
765 assert!(
766 lines.iter().any(|l| l.content.contains("More content")),
767 "Should include content after mkdocstrings blocks"
768 );
769
770 assert!(
772 !lines.iter().any(|l| l.content.contains("::: mymodule")),
773 "Should exclude mkdocstrings marker lines"
774 );
775 assert!(
776 !lines.iter().any(|l| l.content.contains("show_root_heading")),
777 "Should exclude mkdocstrings option lines"
778 );
779 assert!(
780 !lines.iter().any(|l| l.content.contains("show_signature")),
781 "Should exclude all mkdocstrings option lines"
782 );
783
784 assert_eq!(lines[0].line_num, 1, "First line should be line 1");
786 }
787
788 #[test]
789 fn test_skip_esm_blocks() {
790 let content = r#"import {Chart} from './components.js'
792import {Table} from './table.js'
793export const year = 2023
794
795# Last year's snowfall
796
797Content about snowfall data.
798
799import {Footer} from './footer.js'
800
801More content."#;
802 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
803 let lines: Vec<_> = ctx.filtered_lines().skip_esm_blocks().into_iter().collect();
804
805 assert!(
807 lines.iter().any(|l| l.content.contains("# Last year's snowfall")),
808 "Should include markdown headings"
809 );
810 assert!(
811 lines.iter().any(|l| l.content.contains("Content about snowfall")),
812 "Should include markdown content"
813 );
814 assert!(
815 lines.iter().any(|l| l.content.contains("More content")),
816 "Should include content after ESM blocks"
817 );
818
819 assert!(
821 !lines.iter().any(|l| l.content.contains("import {Chart}")),
822 "Should exclude import statements at top of file"
823 );
824 assert!(
825 !lines.iter().any(|l| l.content.contains("import {Table}")),
826 "Should exclude all import statements at top of file"
827 );
828 assert!(
829 !lines.iter().any(|l| l.content.contains("export const year")),
830 "Should exclude export statements at top of file"
831 );
832 assert!(
834 !lines.iter().any(|l| l.content.contains("import {Footer}")),
835 "Should exclude import statements even after markdown content (MDX 2.0+ ESM anywhere)"
836 );
837
838 let heading_line = lines
840 .iter()
841 .find(|l| l.content.contains("# Last year's snowfall"))
842 .unwrap();
843 assert_eq!(heading_line.line_num, 5, "Heading should be on line 5");
844 }
845
846 #[test]
847 fn test_all_filters_combined() {
848 let content = r#"---
849title: Test
850---
851
852# Title
853
854```
855code
856```
857
858<!-- HTML comment here -->
859
860::: mymodule.Class
861 options:
862 show_root_heading: true
863
864<div>
865HTML block
866</div>
867
868Content"#;
869 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
870
871 let lines: Vec<_> = ctx
872 .filtered_lines()
873 .skip_front_matter()
874 .skip_code_blocks()
875 .skip_html_blocks()
876 .skip_html_comments()
877 .skip_mkdocstrings()
878 .into_iter()
879 .collect();
880
881 assert!(
883 lines.iter().any(|l| l.content == "# Title"),
884 "Should include markdown headings"
885 );
886 assert!(
887 lines.iter().any(|l| l.content == "Content"),
888 "Should include markdown content"
889 );
890
891 assert!(
893 !lines.iter().any(|l| l.content == "title: Test"),
894 "Should exclude front matter"
895 );
896 assert!(
897 !lines.iter().any(|l| l.content == "code"),
898 "Should exclude code block content"
899 );
900 assert!(
901 !lines.iter().any(|l| l.content.contains("HTML comment")),
902 "Should exclude HTML comments"
903 );
904 assert!(
905 !lines.iter().any(|l| l.content.contains("::: mymodule")),
906 "Should exclude mkdocstrings blocks"
907 );
908 assert!(
909 !lines.iter().any(|l| l.content.contains("show_root_heading")),
910 "Should exclude mkdocstrings options"
911 );
912 assert!(
913 !lines.iter().any(|l| l.content.contains("HTML block")),
914 "Should exclude HTML blocks"
915 );
916 }
917
918 #[test]
919 fn test_skip_math_blocks() {
920 let content = r#"# Heading
921
922Some regular text.
923
924$$
925A = \left[
926\begin{array}{c}
9271 \\
928-D
929\end{array}
930\right]
931$$
932
933More content after math."#;
934 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
935 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
936
937 assert!(
939 lines.iter().any(|l| l.content.contains("# Heading")),
940 "Should include markdown headings"
941 );
942 assert!(
943 lines.iter().any(|l| l.content.contains("Some regular text")),
944 "Should include regular text before math block"
945 );
946 assert!(
947 lines.iter().any(|l| l.content.contains("More content after math")),
948 "Should include content after math block"
949 );
950
951 assert!(
953 !lines.iter().any(|l| l.content == "$$"),
954 "Should exclude math block delimiters"
955 );
956 assert!(
957 !lines.iter().any(|l| l.content.contains("\\left[")),
958 "Should exclude LaTeX content inside math block"
959 );
960 assert!(
961 !lines.iter().any(|l| l.content.contains("-D")),
962 "Should exclude content that looks like list items inside math block"
963 );
964 assert!(
965 !lines.iter().any(|l| l.content.contains("\\begin{array}")),
966 "Should exclude LaTeX array content"
967 );
968 }
969
970 #[test]
971 fn test_math_blocks_not_confused_with_code_blocks() {
972 let content = r#"# Title
973
974```python
975# This $$ is inside a code block
976x = 1
977```
978
979$$
980y = 2
981$$
982
983Regular text."#;
984 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
985
986 let lines: Vec<_> = ctx.filtered_lines().skip_math_blocks().into_iter().collect();
988
989 assert!(
992 lines.iter().any(|l| l.content.contains("# This $$")),
993 "Code block content with $$ should not be detected as math block"
994 );
995
996 assert!(
998 !lines.iter().any(|l| l.content == "y = 2"),
999 "Actual math block content should be excluded"
1000 );
1001 }
1002
1003 #[test]
1004 fn test_skip_quarto_divs() {
1005 let content = r#"# Heading
1006
1007::: {.callout-note}
1008This is a callout note.
1009With multiple lines.
1010:::
1011
1012Regular text outside.
1013
1014::: {.bordered}
1015Content inside bordered div.
1016:::
1017
1018More content."#;
1019 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1020 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1021
1022 assert!(
1024 lines.iter().any(|l| l.content.contains("# Heading")),
1025 "Should include markdown headings"
1026 );
1027 assert!(
1028 lines.iter().any(|l| l.content.contains("Regular text outside")),
1029 "Should include content between divs"
1030 );
1031 assert!(
1032 lines.iter().any(|l| l.content.contains("More content")),
1033 "Should include content after divs"
1034 );
1035
1036 assert!(
1038 !lines.iter().any(|l| l.content.contains("::: {.callout-note}")),
1039 "Should exclude callout div markers"
1040 );
1041 assert!(
1042 !lines.iter().any(|l| l.content.contains("This is a callout note")),
1043 "Should exclude callout content"
1044 );
1045 assert!(
1046 !lines.iter().any(|l| l.content.contains("Content inside bordered")),
1047 "Should exclude bordered div content"
1048 );
1049 }
1050
1051 #[test]
1052 fn test_skip_jsx_expressions() {
1053 let content = r#"# MDX Document
1054
1055Here is some content with {myVariable} inline.
1056
1057{items.map(item => (
1058 <Item key={item.id} />
1059))}
1060
1061Regular paragraph after expression.
1062
1063{/* This should NOT be skipped by jsx_expressions filter */}
1064{/* MDX comments have their own filter */}
1065
1066More content."#;
1067 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1068 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1069
1070 assert!(
1072 lines.iter().any(|l| l.content.contains("# MDX Document")),
1073 "Should include markdown headings"
1074 );
1075 assert!(
1076 lines.iter().any(|l| l.content.contains("Regular paragraph")),
1077 "Should include regular paragraphs"
1078 );
1079 assert!(
1080 lines.iter().any(|l| l.content.contains("More content")),
1081 "Should include content after expressions"
1082 );
1083
1084 assert!(
1086 !lines.iter().any(|l| l.content.contains("{myVariable}")),
1087 "Should exclude lines with inline JSX expressions"
1088 );
1089 assert!(
1090 !lines.iter().any(|l| l.content.contains("items.map")),
1091 "Should exclude multi-line JSX expression content"
1092 );
1093 assert!(
1094 !lines.iter().any(|l| l.content.contains("<Item key")),
1095 "Should exclude JSX inside expressions"
1096 );
1097 }
1098
1099 #[test]
1100 fn test_skip_quarto_divs_nested() {
1101 let content = r#"# Title
1102
1103::: {.outer}
1104Outer content.
1105
1106::: {.inner}
1107Inner content.
1108:::
1109
1110Back to outer.
1111:::
1112
1113Outside text."#;
1114 let ctx = LintContext::new(content, MarkdownFlavor::Quarto, None);
1115 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1116
1117 assert!(
1119 lines.iter().any(|l| l.content.contains("# Title")),
1120 "Should include heading"
1121 );
1122 assert!(
1123 lines.iter().any(|l| l.content.contains("Outside text")),
1124 "Should include text after divs"
1125 );
1126
1127 assert!(
1129 !lines.iter().any(|l| l.content.contains("Outer content")),
1130 "Should exclude outer div content"
1131 );
1132 assert!(
1133 !lines.iter().any(|l| l.content.contains("Inner content")),
1134 "Should exclude inner div content"
1135 );
1136 }
1137
1138 #[test]
1139 fn test_skip_quarto_divs_not_in_standard_flavor() {
1140 let content = r#"::: {.callout-note}
1141This should NOT be skipped in standard flavor.
1142:::"#;
1143 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1144 let lines: Vec<_> = ctx.filtered_lines().skip_quarto_divs().into_iter().collect();
1145
1146 assert!(
1148 lines.iter().any(|l| l.content.contains("This should NOT be skipped")),
1149 "Standard flavor should not detect Quarto divs"
1150 );
1151 }
1152
1153 #[test]
1154 fn test_skip_mdx_comments() {
1155 let content = r#"# MDX Document
1156
1157{/* This is an MDX comment */}
1158
1159Regular content here.
1160
1161{/*
1162 Multi-line
1163 MDX comment
1164*/}
1165
1166More content after comment."#;
1167 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1168 let lines: Vec<_> = ctx.filtered_lines().skip_mdx_comments().into_iter().collect();
1169
1170 assert!(
1172 lines.iter().any(|l| l.content.contains("# MDX Document")),
1173 "Should include markdown headings"
1174 );
1175 assert!(
1176 lines.iter().any(|l| l.content.contains("Regular content")),
1177 "Should include regular content"
1178 );
1179 assert!(
1180 lines.iter().any(|l| l.content.contains("More content")),
1181 "Should include content after comments"
1182 );
1183
1184 assert!(
1186 !lines.iter().any(|l| l.content.contains("{/* This is")),
1187 "Should exclude single-line MDX comments"
1188 );
1189 assert!(
1190 !lines.iter().any(|l| l.content.contains("Multi-line")),
1191 "Should exclude multi-line MDX comment content"
1192 );
1193 }
1194
1195 #[test]
1196 fn test_jsx_expressions_with_nested_braces() {
1197 let content = r#"# Document
1199
1200{props.style || {color: "red", background: "blue"}}
1201
1202Regular content."#;
1203 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1204 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1205
1206 assert!(
1208 !lines.iter().any(|l| l.content.contains("props.style")),
1209 "Should exclude JSX expression with nested braces"
1210 );
1211 assert!(
1212 lines.iter().any(|l| l.content.contains("Regular content")),
1213 "Should include content after nested expression"
1214 );
1215 }
1216
1217 #[test]
1218 fn test_jsx_and_mdx_comments_combined() {
1219 let content = r#"# Title
1221
1222{variable}
1223
1224{/* comment */}
1225
1226Content."#;
1227 let ctx = LintContext::new(content, MarkdownFlavor::MDX, None);
1228 let lines: Vec<_> = ctx
1229 .filtered_lines()
1230 .skip_jsx_expressions()
1231 .skip_mdx_comments()
1232 .into_iter()
1233 .collect();
1234
1235 assert!(
1236 lines.iter().any(|l| l.content.contains("# Title")),
1237 "Should include heading"
1238 );
1239 assert!(
1240 lines.iter().any(|l| l.content.contains("Content")),
1241 "Should include regular content"
1242 );
1243 assert!(
1244 !lines.iter().any(|l| l.content.contains("{variable}")),
1245 "Should exclude JSX expression"
1246 );
1247 assert!(
1248 !lines.iter().any(|l| l.content.contains("{/* comment */")),
1249 "Should exclude MDX comment"
1250 );
1251 }
1252
1253 #[test]
1254 fn test_jsx_expressions_not_detected_in_standard_flavor() {
1255 let content = r#"# Document
1257
1258{this is not JSX in standard markdown}
1259
1260Content."#;
1261 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1262 let lines: Vec<_> = ctx.filtered_lines().skip_jsx_expressions().into_iter().collect();
1263
1264 assert!(
1266 lines.iter().any(|l| l.content.contains("{this is not JSX")),
1267 "Should NOT exclude brace content in standard markdown"
1268 );
1269 }
1270
1271 #[test]
1274 fn test_skip_obsidian_comments_simple_inline() {
1275 let content = r#"# Heading
1277
1278This is visible %%this is hidden%% and visible again.
1279
1280More content."#;
1281 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1282 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1283
1284 assert!(
1286 lines.iter().any(|l| l.content.contains("# Heading")),
1287 "Should include heading"
1288 );
1289 assert!(
1290 lines.iter().any(|l| l.content.contains("This is visible")),
1291 "Should include line with inline comment"
1292 );
1293 assert!(
1294 lines.iter().any(|l| l.content.contains("More content")),
1295 "Should include content after comment"
1296 );
1297 }
1298
1299 #[test]
1300 fn test_skip_obsidian_comments_multiline_block() {
1301 let content = r#"# Heading
1303
1304%%
1305This is a multi-line
1306comment block
1307%%
1308
1309Content after."#;
1310 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1311 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1312
1313 assert!(
1315 lines.iter().any(|l| l.content.contains("# Heading")),
1316 "Should include heading"
1317 );
1318 assert!(
1319 lines.iter().any(|l| l.content.contains("Content after")),
1320 "Should include content after comment block"
1321 );
1322
1323 assert!(
1325 !lines.iter().any(|l| l.content.contains("This is a multi-line")),
1326 "Should exclude multi-line comment content"
1327 );
1328 assert!(
1329 !lines.iter().any(|l| l.content.contains("comment block")),
1330 "Should exclude multi-line comment content"
1331 );
1332 }
1333
1334 #[test]
1335 fn test_skip_obsidian_comments_in_code_block() {
1336 let content = r#"# Heading
1338
1339```
1340%% This is NOT a comment
1341It's inside a code block
1342%%
1343```
1344
1345Content."#;
1346 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1347 let lines: Vec<_> = ctx
1348 .filtered_lines()
1349 .skip_obsidian_comments()
1350 .skip_code_blocks()
1351 .into_iter()
1352 .collect();
1353
1354 assert!(
1356 lines.iter().any(|l| l.content.contains("# Heading")),
1357 "Should include heading"
1358 );
1359 assert!(
1360 lines.iter().any(|l| l.content.contains("Content")),
1361 "Should include content after code block"
1362 );
1363 }
1364
1365 #[test]
1366 fn test_skip_obsidian_comments_in_html_comment() {
1367 let content = r#"# Heading
1369
1370<!-- %% This is inside HTML comment %% -->
1371
1372Content."#;
1373 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1374 let lines: Vec<_> = ctx
1375 .filtered_lines()
1376 .skip_obsidian_comments()
1377 .skip_html_comments()
1378 .into_iter()
1379 .collect();
1380
1381 assert!(
1382 lines.iter().any(|l| l.content.contains("# Heading")),
1383 "Should include heading"
1384 );
1385 assert!(
1386 lines.iter().any(|l| l.content.contains("Content")),
1387 "Should include content"
1388 );
1389 }
1390
1391 #[test]
1392 fn test_skip_obsidian_comments_empty() {
1393 let content = r#"# Heading
1395
1396%%%% empty comment
1397
1398Content."#;
1399 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1400 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1401
1402 assert!(
1404 lines.iter().any(|l| l.content.contains("# Heading")),
1405 "Should include heading"
1406 );
1407 }
1408
1409 #[test]
1410 fn test_skip_obsidian_comments_unclosed() {
1411 let content = r#"# Heading
1413
1414%% starts but never ends
1415This should be hidden
1416Until end of document"#;
1417 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1418 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1419
1420 assert!(
1422 lines.iter().any(|l| l.content.contains("# Heading")),
1423 "Should include heading before unclosed comment"
1424 );
1425
1426 assert!(
1428 !lines.iter().any(|l| l.content.contains("This should be hidden")),
1429 "Should exclude content in unclosed comment"
1430 );
1431 assert!(
1432 !lines.iter().any(|l| l.content.contains("Until end of document")),
1433 "Should exclude content until end of document"
1434 );
1435 }
1436
1437 #[test]
1438 fn test_skip_obsidian_comments_multiple_on_same_line() {
1439 let content = r#"# Heading
1441
1442First %%hidden1%% middle %%hidden2%% last
1443
1444Content."#;
1445 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1446 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1447
1448 assert!(
1450 lines.iter().any(|l| l.content.contains("First")),
1451 "Should include line with multiple inline comments"
1452 );
1453 assert!(
1454 lines.iter().any(|l| l.content.contains("middle")),
1455 "Should include visible text between comments"
1456 );
1457 }
1458
1459 #[test]
1460 fn test_skip_obsidian_comments_at_start_of_line() {
1461 let content = r#"# Heading
1463
1464%%comment at start%%
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!(
1471 lines.iter().any(|l| l.content.contains("# Heading")),
1472 "Should include heading"
1473 );
1474 assert!(
1475 lines.iter().any(|l| l.content.contains("Content")),
1476 "Should include content"
1477 );
1478 }
1479
1480 #[test]
1481 fn test_skip_obsidian_comments_at_end_of_line() {
1482 let content = r#"# Heading
1484
1485Some text %%comment at end%%
1486
1487Content."#;
1488 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1489 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1490
1491 assert!(
1492 lines.iter().any(|l| l.content.contains("Some text")),
1493 "Should include text before comment"
1494 );
1495 }
1496
1497 #[test]
1498 fn test_skip_obsidian_comments_with_markdown_inside() {
1499 let content = r#"# Heading
1501
1502%%
1503# hidden heading
1504[hidden link](url)
1505**hidden bold**
1506%%
1507
1508Content."#;
1509 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1510 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1511
1512 assert!(
1513 !lines.iter().any(|l| l.content.contains("# hidden heading")),
1514 "Should exclude heading inside comment"
1515 );
1516 assert!(
1517 !lines.iter().any(|l| l.content.contains("[hidden link]")),
1518 "Should exclude link inside comment"
1519 );
1520 assert!(
1521 !lines.iter().any(|l| l.content.contains("**hidden bold**")),
1522 "Should exclude bold inside comment"
1523 );
1524 }
1525
1526 #[test]
1527 fn test_skip_obsidian_comments_with_unicode() {
1528 let content = r#"# Heading
1530
1531%%日本語コメント%%
1532
1533%%Комментарий%%
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!(
1541 lines.iter().any(|l| l.content.contains("# Heading")),
1542 "Should include heading"
1543 );
1544 assert!(
1545 lines.iter().any(|l| l.content.contains("Content")),
1546 "Should include content"
1547 );
1548 }
1549
1550 #[test]
1551 fn test_skip_obsidian_comments_triple_percent() {
1552 let content = r#"# Heading
1554
1555%%% odd percent
1556
1557Content."#;
1558 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1559 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1560
1561 assert!(
1563 lines.iter().any(|l| l.content.contains("# Heading")),
1564 "Should include heading"
1565 );
1566 }
1567
1568 #[test]
1569 fn test_skip_obsidian_comments_not_in_standard_flavor() {
1570 let content = r#"# Heading
1572
1573%%this is not hidden in standard%%
1574
1575Content."#;
1576 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1577 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1578
1579 assert!(
1581 lines.iter().any(|l| l.content.contains("%%this is not hidden")),
1582 "Should NOT hide %% content in Standard flavor"
1583 );
1584 }
1585
1586 #[test]
1587 fn test_skip_obsidian_comments_integration_with_other_filters() {
1588 let content = r#"---
1590title: Test
1591---
1592
1593# Heading
1594
1595```
1596code
1597```
1598
1599%%hidden comment%%
1600
1601Content."#;
1602 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1603 let lines: Vec<_> = ctx
1604 .filtered_lines()
1605 .skip_front_matter()
1606 .skip_code_blocks()
1607 .skip_obsidian_comments()
1608 .into_iter()
1609 .collect();
1610
1611 assert!(
1613 !lines.iter().any(|l| l.content.contains("title: Test")),
1614 "Should skip frontmatter"
1615 );
1616 assert!(
1617 !lines.iter().any(|l| l.content == "code"),
1618 "Should skip code block content"
1619 );
1620 assert!(
1621 lines.iter().any(|l| l.content.contains("# Heading")),
1622 "Should include heading"
1623 );
1624 assert!(
1625 lines.iter().any(|l| l.content.contains("Content")),
1626 "Should include content"
1627 );
1628 }
1629
1630 #[test]
1631 fn test_skip_obsidian_comments_whole_line_only() {
1632 let content = "start %%\nfully hidden\n%% end";
1634 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1635 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1636
1637 assert!(
1639 lines.iter().any(|l| l.content.contains("start")),
1640 "First line should be included (starts outside comment)"
1641 );
1642 assert!(
1644 !lines.iter().any(|l| l.content == "fully hidden"),
1645 "Middle line should be excluded (entirely within comment)"
1646 );
1647 assert!(
1649 lines.iter().any(|l| l.content.contains("end")),
1650 "Last line should be included (ends outside comment)"
1651 );
1652 }
1653
1654 #[test]
1655 fn test_skip_obsidian_comments_in_inline_code() {
1656 let content = r#"# Heading
1658
1659The syntax is `%%comment%%` in Obsidian.
1660
1661Content."#;
1662 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1663 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1664
1665 assert!(
1667 lines.iter().any(|l| l.content.contains("The syntax is")),
1668 "Should include line with %% in code span"
1669 );
1670 assert!(
1671 lines.iter().any(|l| l.content.contains("in Obsidian")),
1672 "Should include text after code span"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_skip_obsidian_comments_in_inline_code_multi_backtick() {
1678 let content = r#"# Heading
1680
1681The syntax is ``%%comment%%`` in Obsidian.
1682
1683Content."#;
1684 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1685 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1686
1687 assert!(
1688 lines.iter().any(|l| l.content.contains("The syntax is")),
1689 "Should include line with %% in multi-backtick code span"
1690 );
1691 assert!(
1692 lines.iter().any(|l| l.content.contains("Content")),
1693 "Should include content after code span"
1694 );
1695 }
1696
1697 #[test]
1698 fn test_skip_obsidian_comments_consecutive_blocks() {
1699 let content = r#"# Heading
1701
1702%%comment 1%%
1703
1704%%comment 2%%
1705
1706Content."#;
1707 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1708 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1709
1710 assert!(
1711 lines.iter().any(|l| l.content.contains("# Heading")),
1712 "Should include heading"
1713 );
1714 assert!(
1715 lines.iter().any(|l| l.content.contains("Content")),
1716 "Should include content after comments"
1717 );
1718 }
1719
1720 #[test]
1721 fn test_skip_obsidian_comments_spanning_many_lines() {
1722 let content = r#"# Title
1724
1725%%
1726Line 1 of comment
1727Line 2 of comment
1728Line 3 of comment
1729Line 4 of comment
1730Line 5 of comment
1731%%
1732
1733After comment."#;
1734 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1735 let lines: Vec<_> = ctx.filtered_lines().skip_obsidian_comments().into_iter().collect();
1736
1737 for i in 1..=5 {
1739 assert!(
1740 !lines
1741 .iter()
1742 .any(|l| l.content.contains(&format!("Line {i} of comment"))),
1743 "Should exclude line {i} of comment"
1744 );
1745 }
1746
1747 assert!(
1748 lines.iter().any(|l| l.content.contains("# Title")),
1749 "Should include title"
1750 );
1751 assert!(
1752 lines.iter().any(|l| l.content.contains("After comment")),
1753 "Should include content after comment"
1754 );
1755 }
1756
1757 #[test]
1758 fn test_obsidian_comment_line_info_field() {
1759 let content = "visible\n%%\nhidden\n%%\nvisible";
1761 let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None);
1762
1763 assert!(
1765 !ctx.lines[0].in_obsidian_comment,
1766 "Line 0 should not be marked as in_obsidian_comment"
1767 );
1768
1769 assert!(
1771 ctx.lines[2].in_obsidian_comment,
1772 "Line 2 (hidden) should be marked as in_obsidian_comment"
1773 );
1774
1775 assert!(
1777 !ctx.lines[4].in_obsidian_comment,
1778 "Line 4 should not be marked as in_obsidian_comment"
1779 );
1780 }
1781
1782 #[test]
1785 fn test_skip_pymdown_blocks_basic() {
1786 let content = r#"# Heading
1788
1789/// caption
1790Table caption here.
1791///
1792
1793Content after."#;
1794 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1795 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1796
1797 assert!(
1799 lines.iter().any(|l| l.content.contains("# Heading")),
1800 "Should include heading"
1801 );
1802 assert!(
1803 lines.iter().any(|l| l.content.contains("Content after")),
1804 "Should include content after block"
1805 );
1806
1807 assert!(
1809 !lines.iter().any(|l| l.content.contains("Table caption")),
1810 "Should exclude content inside block"
1811 );
1812 }
1813
1814 #[test]
1815 fn test_skip_pymdown_blocks_details() {
1816 let content = r#"# Heading
1818
1819/// details | Click to expand
1820 open: True
1821Hidden content here.
1822More hidden content.
1823///
1824
1825Visible content."#;
1826 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1827 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1828
1829 assert!(
1830 !lines.iter().any(|l| l.content.contains("Hidden content")),
1831 "Should exclude hidden content"
1832 );
1833 assert!(
1834 !lines.iter().any(|l| l.content.contains("open: True")),
1835 "Should exclude YAML options"
1836 );
1837 assert!(
1838 lines.iter().any(|l| l.content.contains("Visible content")),
1839 "Should include visible content"
1840 );
1841 }
1842
1843 #[test]
1844 fn test_skip_pymdown_blocks_nested() {
1845 let content = r#"# Title
1847
1848/// details | Outer
1849Outer content.
1850
1851 /// caption
1852 Inner caption.
1853 ///
1854
1855More outer content.
1856///
1857
1858After all blocks."#;
1859 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1860 let lines: Vec<_> = ctx.filtered_lines().skip_pymdown_blocks().into_iter().collect();
1861
1862 assert!(
1863 !lines.iter().any(|l| l.content.contains("Outer content")),
1864 "Should exclude outer block content"
1865 );
1866 assert!(
1867 !lines.iter().any(|l| l.content.contains("Inner caption")),
1868 "Should exclude inner block content"
1869 );
1870 assert!(
1871 lines.iter().any(|l| l.content.contains("After all blocks")),
1872 "Should include content after all blocks"
1873 );
1874 }
1875
1876 #[test]
1877 fn test_pymdown_block_line_info_field() {
1878 let content = "visible\n/// caption\nhidden\n///\nvisible";
1880 let ctx = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1881
1882 assert!(
1884 !ctx.lines[0].in_pymdown_block,
1885 "Line 0 should not be marked as in_pymdown_block"
1886 );
1887
1888 assert!(
1890 ctx.lines[1].in_pymdown_block,
1891 "Line 1 (/// caption) should be marked as in_pymdown_block"
1892 );
1893
1894 assert!(
1896 ctx.lines[2].in_pymdown_block,
1897 "Line 2 (hidden) should be marked as in_pymdown_block"
1898 );
1899
1900 assert!(
1902 ctx.lines[3].in_pymdown_block,
1903 "Line 3 (closing ///) should be marked as in_pymdown_block"
1904 );
1905
1906 assert!(
1908 !ctx.lines[4].in_pymdown_block,
1909 "Line 4 should not be marked as in_pymdown_block"
1910 );
1911 }
1912
1913 #[test]
1914 fn test_pymdown_blocks_only_for_mkdocs_flavor() {
1915 let content = "/// caption\nCaption text\n///";
1917
1918 let ctx_mkdocs = LintContext::new(content, MarkdownFlavor::MkDocs, None);
1920 assert!(
1921 ctx_mkdocs.lines[1].in_pymdown_block,
1922 "MkDocs flavor should detect pymdown blocks"
1923 );
1924
1925 let ctx_standard = LintContext::new(content, MarkdownFlavor::Standard, None);
1927 assert!(
1928 !ctx_standard.lines[1].in_pymdown_block,
1929 "Standard flavor should NOT detect pymdown blocks"
1930 );
1931 }
1932}