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