1use dyn_clone::DynClone;
6use serde::Serialize;
7use std::ops::Range;
8use thiserror::Error;
9
10use crate::lint_context::LintContext;
12use crate::utils::document_structure::DocumentStructure;
13
14pub use markdown::mdast::Node as MarkdownAst;
16
17#[macro_export]
19macro_rules! impl_rule_clone {
20 ($ty:ty) => {
21 impl $ty {
22 fn box_clone(&self) -> Box<dyn Rule> {
23 Box::new(self.clone())
24 }
25 }
26 };
27}
28
29#[derive(Debug, Error)]
30pub enum LintError {
31 #[error("Invalid input: {0}")]
32 InvalidInput(String),
33 #[error("Fix failed: {0}")]
34 FixFailed(String),
35 #[error("IO error: {0}")]
36 IoError(#[from] std::io::Error),
37 #[error("Parsing error: {0}")]
38 ParsingError(String),
39}
40
41pub type LintResult = Result<Vec<LintWarning>, LintError>;
42
43#[derive(Debug, PartialEq, Clone, Serialize)]
44pub struct LintWarning {
45 pub message: String,
46 pub line: usize, pub column: usize, pub end_line: usize, pub end_column: usize, pub severity: Severity,
51 pub fix: Option<Fix>,
52 pub rule_name: Option<&'static str>,
53}
54
55#[derive(Debug, PartialEq, Clone, Serialize)]
56pub struct Fix {
57 pub range: Range<usize>,
58 pub replacement: String,
59}
60
61#[derive(Debug, PartialEq, Clone, Copy, Serialize)]
62pub enum Severity {
63 Error,
64 Warning,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum RuleCategory {
70 Heading,
71 List,
72 CodeBlock,
73 Link,
74 Image,
75 Html,
76 Emphasis,
77 Whitespace,
78 Blockquote,
79 Table,
80 FrontMatter,
81 Other,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum FixCapability {
87 FullyFixable,
89 ConditionallyFixable,
91 Unfixable,
93}
94
95pub trait Rule: DynClone + Send + Sync {
97 fn name(&self) -> &'static str;
98 fn description(&self) -> &'static str;
99 fn check(&self, ctx: &LintContext) -> LintResult;
100 fn fix(&self, ctx: &LintContext) -> Result<String, LintError>;
101
102 fn check_with_structure(&self, ctx: &LintContext, _structure: &DocumentStructure) -> LintResult {
105 self.check(ctx)
106 }
107
108 fn check_with_ast(&self, ctx: &LintContext, _ast: &MarkdownAst) -> LintResult {
111 self.check(ctx)
112 }
113
114 fn check_with_structure_and_ast(
117 &self,
118 ctx: &LintContext,
119 _structure: &DocumentStructure,
120 _ast: &MarkdownAst,
121 ) -> LintResult {
122 self.check(ctx)
123 }
124
125 fn should_skip(&self, _ctx: &LintContext) -> bool {
127 false
128 }
129
130 fn category(&self) -> RuleCategory {
132 RuleCategory::Other }
134
135 fn uses_ast(&self) -> bool {
137 false
138 }
139
140 fn uses_document_structure(&self) -> bool {
142 false
143 }
144
145 fn as_any(&self) -> &dyn std::any::Any;
146
147 fn as_maybe_document_structure(&self) -> Option<&dyn MaybeDocumentStructure> {
148 None
149 }
150
151 fn as_maybe_ast(&self) -> Option<&dyn MaybeAst> {
152 None
153 }
154
155 fn default_config_section(&self) -> Option<(String, toml::Value)> {
159 None
160 }
161
162 fn fix_capability(&self) -> FixCapability {
164 FixCapability::FullyFixable }
166
167 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
169 where
170 Self: Sized,
171 {
172 panic!(
173 "from_config not implemented for rule: {}",
174 std::any::type_name::<Self>()
175 );
176 }
177}
178
179dyn_clone::clone_trait_object!(Rule);
181
182pub trait RuleExt {
184 fn downcast_ref<T: 'static>(&self) -> Option<&T>;
185}
186
187impl<R: Rule + 'static> RuleExt for Box<R> {
188 fn downcast_ref<T: 'static>(&self) -> Option<&T> {
189 if std::any::TypeId::of::<R>() == std::any::TypeId::of::<T>() {
190 unsafe { Some(&*(self.as_ref() as *const _ as *const T)) }
191 } else {
192 None
193 }
194 }
195}
196
197pub fn is_rule_disabled_at_line(content: &str, rule_name: &str, line_num: usize) -> bool {
199 let lines: Vec<&str> = content.lines().collect();
200 let mut is_disabled = false;
201
202 for (i, line) in lines.iter().enumerate() {
204 if i > line_num {
206 break;
207 }
208
209 if crate::rules::code_block_utils::CodeBlockUtils::is_in_code_block(content, i) {
211 continue;
212 }
213
214 let line = line.trim();
215
216 if let Some(rules) = parse_disable_comment(line)
218 && (rules.is_empty() || rules.contains(&rule_name))
219 {
220 is_disabled = true;
221 continue;
222 }
223
224 if let Some(rules) = parse_enable_comment(line)
226 && (rules.is_empty() || rules.contains(&rule_name))
227 {
228 is_disabled = false;
229 continue;
230 }
231 }
232
233 is_disabled
234}
235
236pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
238 if let Some(start) = line.find("<!-- rumdl-disable") {
240 let after_prefix = &line[start + "<!-- rumdl-disable".len()..];
241
242 if after_prefix.trim_start().starts_with("-->") {
244 return Some(Vec::new()); }
246
247 if let Some(end) = after_prefix.find("-->") {
249 let rules_str = after_prefix[..end].trim();
250 if !rules_str.is_empty() {
251 let rules: Vec<&str> = rules_str.split_whitespace().collect();
252 return Some(rules);
253 }
254 }
255 }
256
257 if let Some(start) = line.find("<!-- markdownlint-disable") {
259 let after_prefix = &line[start + "<!-- markdownlint-disable".len()..];
260
261 if after_prefix.trim_start().starts_with("-->") {
263 return Some(Vec::new()); }
265
266 if let Some(end) = after_prefix.find("-->") {
268 let rules_str = after_prefix[..end].trim();
269 if !rules_str.is_empty() {
270 let rules: Vec<&str> = rules_str.split_whitespace().collect();
271 return Some(rules);
272 }
273 }
274 }
275
276 None
277}
278
279pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
281 if let Some(start) = line.find("<!-- rumdl-enable") {
283 let after_prefix = &line[start + "<!-- rumdl-enable".len()..];
284
285 if after_prefix.trim_start().starts_with("-->") {
287 return Some(Vec::new()); }
289
290 if let Some(end) = after_prefix.find("-->") {
292 let rules_str = after_prefix[..end].trim();
293 if !rules_str.is_empty() {
294 let rules: Vec<&str> = rules_str.split_whitespace().collect();
295 return Some(rules);
296 }
297 }
298 }
299
300 if let Some(start) = line.find("<!-- markdownlint-enable") {
302 let after_prefix = &line[start + "<!-- markdownlint-enable".len()..];
303
304 if after_prefix.trim_start().starts_with("-->") {
306 return Some(Vec::new()); }
308
309 if let Some(end) = after_prefix.find("-->") {
311 let rules_str = after_prefix[..end].trim();
312 if !rules_str.is_empty() {
313 let rules: Vec<&str> = rules_str.split_whitespace().collect();
314 return Some(rules);
315 }
316 }
317 }
318
319 None
320}
321
322pub fn is_rule_disabled_by_comment(content: &str, rule_name: &str) -> bool {
324 let lines: Vec<&str> = content.lines().collect();
326 is_rule_disabled_at_line(content, rule_name, lines.len())
327}
328
329pub trait MaybeDocumentStructure {
331 fn check_with_structure_opt(
332 &self,
333 ctx: &LintContext,
334 structure: &crate::utils::document_structure::DocumentStructure,
335 ) -> Option<LintResult>;
336}
337
338impl<T> MaybeDocumentStructure for T
339where
340 T: Rule + crate::utils::document_structure::DocumentStructureExtensions + 'static,
341{
342 fn check_with_structure_opt(
343 &self,
344 ctx: &LintContext,
345 structure: &crate::utils::document_structure::DocumentStructure,
346 ) -> Option<LintResult> {
347 Some(self.check_with_structure(ctx, structure))
348 }
349}
350
351impl MaybeDocumentStructure for dyn Rule {
352 fn check_with_structure_opt(
353 &self,
354 _ctx: &LintContext,
355 _structure: &crate::utils::document_structure::DocumentStructure,
356 ) -> Option<LintResult> {
357 None
358 }
359}
360
361pub trait MaybeAst {
363 fn check_with_ast_opt(&self, ctx: &LintContext, ast: &MarkdownAst) -> Option<LintResult>;
364}
365
366impl<T> MaybeAst for T
367where
368 T: Rule + AstExtensions + 'static,
369{
370 fn check_with_ast_opt(&self, ctx: &LintContext, ast: &MarkdownAst) -> Option<LintResult> {
371 if self.has_relevant_ast_elements(ctx, ast) {
372 Some(self.check_with_ast(ctx, ast))
373 } else {
374 None
375 }
376 }
377}
378
379impl MaybeAst for dyn Rule {
380 fn check_with_ast_opt(&self, _ctx: &LintContext, _ast: &MarkdownAst) -> Option<LintResult> {
381 None
382 }
383}
384
385pub trait AstExtensions {
387 fn has_relevant_ast_elements(&self, ctx: &LintContext, ast: &MarkdownAst) -> bool;
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_parse_disable_comment() {
397 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
399
400 assert_eq!(
402 parse_disable_comment("<!-- rumdl-disable MD001 MD002 -->"),
403 Some(vec!["MD001", "MD002"])
404 );
405
406 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
408
409 assert_eq!(
411 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
412 Some(vec!["MD001", "MD002"])
413 );
414
415 assert_eq!(parse_disable_comment("<!-- some other comment -->"), None);
417
418 assert_eq!(
420 parse_disable_comment(" <!-- rumdl-disable MD013 --> "),
421 Some(vec!["MD013"])
422 );
423 }
424
425 #[test]
426 fn test_parse_enable_comment() {
427 assert_eq!(parse_enable_comment("<!-- rumdl-enable -->"), Some(vec![]));
429
430 assert_eq!(
432 parse_enable_comment("<!-- rumdl-enable MD001 MD002 -->"),
433 Some(vec!["MD001", "MD002"])
434 );
435
436 assert_eq!(parse_enable_comment("<!-- markdownlint-enable -->"), Some(vec![]));
438
439 assert_eq!(
441 parse_enable_comment("<!-- markdownlint-enable MD001 MD002 -->"),
442 Some(vec!["MD001", "MD002"])
443 );
444
445 assert_eq!(parse_enable_comment("<!-- some other comment -->"), None);
447 }
448
449 #[test]
450 fn test_is_rule_disabled_at_line() {
451 let content = r#"# Test
452<!-- rumdl-disable MD013 -->
453This is a long line
454<!-- rumdl-enable MD013 -->
455This is another line
456<!-- markdownlint-disable MD042 -->
457Empty link: []()
458<!-- markdownlint-enable MD042 -->
459Final line"#;
460
461 assert!(is_rule_disabled_at_line(content, "MD013", 2));
463
464 assert!(!is_rule_disabled_at_line(content, "MD013", 4));
466
467 assert!(is_rule_disabled_at_line(content, "MD042", 6));
469
470 assert!(!is_rule_disabled_at_line(content, "MD042", 8));
472
473 assert!(!is_rule_disabled_at_line(content, "MD001", 5));
475 }
476
477 #[test]
478 fn test_parse_disable_comment_edge_cases() {
479 assert_eq!(parse_disable_comment("<!-- rumdl-disable-->"), Some(vec![]));
481
482 assert_eq!(
484 parse_disable_comment("<!-- rumdl-disable MD001 MD002 -->"),
485 None
486 );
487
488 assert_eq!(
490 parse_disable_comment("<!-- rumdl-disable\tMD001\tMD002 -->"),
491 Some(vec!["MD001", "MD002"])
492 );
493
494 assert_eq!(
496 parse_disable_comment("Some text <!-- rumdl-disable MD001 --> more text"),
497 Some(vec!["MD001"])
498 );
499
500 assert_eq!(parse_disable_comment("<!-- rumdl-disable MD001"), None);
502
503 assert_eq!(parse_disable_comment("rumdl-disable MD001 -->"), None);
505
506 assert_eq!(parse_disable_comment("<!-- RUMDL-DISABLE -->"), None);
508 assert_eq!(parse_disable_comment("<!-- RuMdL-DiSaBlE -->"), None);
509
510 assert_eq!(
512 parse_disable_comment("<!-- rumdl-disable\nMD001 -->"),
513 Some(vec!["MD001"])
514 );
515
516 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
518
519 assert_eq!(
521 parse_disable_comment("<!-- rumdl-disable MD001 MD001 MD002 -->"),
522 Some(vec!["MD001", "MD001", "MD002"])
523 );
524 }
525
526 #[test]
527 fn test_parse_enable_comment_edge_cases() {
528 assert_eq!(parse_enable_comment("<!-- rumdl-enable-->"), Some(vec![]));
530
531 assert_eq!(parse_enable_comment("<!-- rumdl-enable MD001 MD002 -->"), None);
533
534 assert_eq!(
536 parse_enable_comment("<!-- rumdl-enable\tMD001\tMD002 -->"),
537 Some(vec!["MD001", "MD002"])
538 );
539
540 assert_eq!(
542 parse_enable_comment("Some text <!-- rumdl-enable MD001 --> more text"),
543 Some(vec!["MD001"])
544 );
545
546 assert_eq!(parse_enable_comment("<!-- rumdl-enable MD001"), None);
548
549 assert_eq!(parse_enable_comment("rumdl-enable MD001 -->"), None);
551
552 assert_eq!(parse_enable_comment("<!-- RUMDL-ENABLE -->"), None);
554 assert_eq!(parse_enable_comment("<!-- RuMdL-EnAbLe -->"), None);
555
556 assert_eq!(
558 parse_enable_comment("<!-- rumdl-enable\nMD001 -->"),
559 Some(vec!["MD001"])
560 );
561
562 assert_eq!(parse_enable_comment("<!-- rumdl-enable -->"), Some(vec![]));
564
565 assert_eq!(
567 parse_enable_comment("<!-- rumdl-enable MD001 MD001 MD002 -->"),
568 Some(vec!["MD001", "MD001", "MD002"])
569 );
570 }
571
572 #[test]
573 fn test_nested_disable_enable_comments() {
574 let content = r#"# Document
575<!-- rumdl-disable -->
576All rules disabled here
577<!-- rumdl-disable MD001 -->
578Still all disabled (redundant)
579<!-- rumdl-enable MD001 -->
580Only MD001 enabled, others still disabled
581<!-- rumdl-enable -->
582All rules enabled again"#;
583
584 assert!(is_rule_disabled_at_line(content, "MD001", 2));
586 assert!(is_rule_disabled_at_line(content, "MD002", 2));
587
588 assert!(is_rule_disabled_at_line(content, "MD001", 4));
590 assert!(is_rule_disabled_at_line(content, "MD002", 4));
591
592 assert!(!is_rule_disabled_at_line(content, "MD001", 6));
594 assert!(is_rule_disabled_at_line(content, "MD002", 6));
595
596 assert!(!is_rule_disabled_at_line(content, "MD001", 8));
598 assert!(!is_rule_disabled_at_line(content, "MD002", 8));
599 }
600
601 #[test]
602 fn test_mixed_comment_styles() {
603 let content = r#"# Document
604<!-- markdownlint-disable MD001 -->
605MD001 disabled via markdownlint
606<!-- rumdl-enable MD001 -->
607MD001 enabled via rumdl
608<!-- rumdl-disable -->
609All disabled via rumdl
610<!-- markdownlint-enable -->
611All enabled via markdownlint"#;
612
613 assert!(is_rule_disabled_at_line(content, "MD001", 2));
615 assert!(!is_rule_disabled_at_line(content, "MD002", 2));
616
617 assert!(!is_rule_disabled_at_line(content, "MD001", 4));
619 assert!(!is_rule_disabled_at_line(content, "MD002", 4));
620
621 assert!(is_rule_disabled_at_line(content, "MD001", 6));
623 assert!(is_rule_disabled_at_line(content, "MD002", 6));
624
625 assert!(!is_rule_disabled_at_line(content, "MD001", 8));
627 assert!(!is_rule_disabled_at_line(content, "MD002", 8));
628 }
629
630 #[test]
631 fn test_comments_in_code_blocks() {
632 let content = r#"# Document
633```markdown
634<!-- rumdl-disable MD001 -->
635This is in a code block, should not affect rules
636```
637MD001 should still be enabled here"#;
638
639 assert!(!is_rule_disabled_at_line(content, "MD001", 5));
641
642 let indented_content = r#"# Document
644
645 <!-- rumdl-disable MD001 -->
646 This is in an indented code block
647
648MD001 should still be enabled here"#;
649
650 assert!(!is_rule_disabled_at_line(indented_content, "MD001", 5));
651 }
652
653 #[test]
654 fn test_comments_with_unicode() {
655 assert_eq!(
657 parse_disable_comment("<!-- rumdl-disable MD001 --> 你好"),
658 Some(vec!["MD001"])
659 );
660
661 assert_eq!(
662 parse_disable_comment("🚀 <!-- rumdl-disable MD001 --> 🎉"),
663 Some(vec!["MD001"])
664 );
665 }
666
667 #[test]
668 fn test_rule_disabled_at_specific_lines() {
669 let content = r#"Line 0
670<!-- rumdl-disable MD001 MD002 -->
671Line 2
672Line 3
673<!-- rumdl-enable MD001 -->
674Line 5
675<!-- rumdl-disable -->
676Line 7
677<!-- rumdl-enable MD002 -->
678Line 9"#;
679
680 assert!(!is_rule_disabled_at_line(content, "MD001", 0));
682 assert!(!is_rule_disabled_at_line(content, "MD002", 0));
683
684 assert!(is_rule_disabled_at_line(content, "MD001", 2));
685 assert!(is_rule_disabled_at_line(content, "MD002", 2));
686
687 assert!(is_rule_disabled_at_line(content, "MD001", 3));
688 assert!(is_rule_disabled_at_line(content, "MD002", 3));
689
690 assert!(!is_rule_disabled_at_line(content, "MD001", 5));
691 assert!(is_rule_disabled_at_line(content, "MD002", 5));
692
693 assert!(is_rule_disabled_at_line(content, "MD001", 7));
694 assert!(is_rule_disabled_at_line(content, "MD002", 7));
695
696 assert!(is_rule_disabled_at_line(content, "MD001", 9));
697 assert!(!is_rule_disabled_at_line(content, "MD002", 9));
698 }
699
700 #[test]
701 fn test_is_rule_disabled_by_comment() {
702 let content = r#"# Document
703<!-- rumdl-disable MD001 -->
704Content here"#;
705
706 assert!(is_rule_disabled_by_comment(content, "MD001"));
707 assert!(!is_rule_disabled_by_comment(content, "MD002"));
708
709 let content2 = r#"# Document
710<!-- rumdl-disable -->
711Content here"#;
712
713 assert!(is_rule_disabled_by_comment(content2, "MD001"));
714 assert!(is_rule_disabled_by_comment(content2, "MD002"));
715 }
716
717 #[test]
718 fn test_comment_at_end_of_file() {
719 let content = "# Document\nContent\n<!-- rumdl-disable MD001 -->";
720
721 assert!(is_rule_disabled_by_comment(content, "MD001"));
723 assert!(!is_rule_disabled_at_line(content, "MD001", 1));
725 assert!(is_rule_disabled_at_line(content, "MD001", 2));
727 }
728
729 #[test]
730 fn test_multiple_comments_same_line() {
731 assert_eq!(
733 parse_disable_comment("<!-- rumdl-disable MD001 --> <!-- rumdl-disable MD002 -->"),
734 Some(vec!["MD001"])
735 );
736
737 assert_eq!(
738 parse_enable_comment("<!-- rumdl-enable MD001 --> <!-- rumdl-enable MD002 -->"),
739 Some(vec!["MD001"])
740 );
741 }
742
743 #[test]
744 fn test_severity_serialization() {
745 let warning = LintWarning {
746 message: "Test warning".to_string(),
747 line: 1,
748 column: 1,
749 end_line: 1,
750 end_column: 10,
751 severity: Severity::Warning,
752 fix: None,
753 rule_name: Some("MD001"),
754 };
755
756 let serialized = serde_json::to_string(&warning).unwrap();
757 assert!(serialized.contains("\"severity\":\"Warning\""));
758
759 let error = LintWarning {
760 severity: Severity::Error,
761 ..warning
762 };
763
764 let serialized = serde_json::to_string(&error).unwrap();
765 assert!(serialized.contains("\"severity\":\"Error\""));
766 }
767
768 #[test]
769 fn test_fix_serialization() {
770 let fix = Fix {
771 range: 0..10,
772 replacement: "fixed text".to_string(),
773 };
774
775 let warning = LintWarning {
776 message: "Test warning".to_string(),
777 line: 1,
778 column: 1,
779 end_line: 1,
780 end_column: 10,
781 severity: Severity::Warning,
782 fix: Some(fix),
783 rule_name: Some("MD001"),
784 };
785
786 let serialized = serde_json::to_string(&warning).unwrap();
787 assert!(serialized.contains("\"fix\""));
788 assert!(serialized.contains("\"replacement\":\"fixed text\""));
789 }
790
791 #[test]
792 fn test_rule_category_equality() {
793 assert_eq!(RuleCategory::Heading, RuleCategory::Heading);
794 assert_ne!(RuleCategory::Heading, RuleCategory::List);
795
796 let categories = [
798 RuleCategory::Heading,
799 RuleCategory::List,
800 RuleCategory::CodeBlock,
801 RuleCategory::Link,
802 RuleCategory::Image,
803 RuleCategory::Html,
804 RuleCategory::Emphasis,
805 RuleCategory::Whitespace,
806 RuleCategory::Blockquote,
807 RuleCategory::Table,
808 RuleCategory::FrontMatter,
809 RuleCategory::Other,
810 ];
811
812 for (i, cat1) in categories.iter().enumerate() {
813 for (j, cat2) in categories.iter().enumerate() {
814 if i == j {
815 assert_eq!(cat1, cat2);
816 } else {
817 assert_ne!(cat1, cat2);
818 }
819 }
820 }
821 }
822
823 #[test]
824 fn test_lint_error_conversions() {
825 use std::io;
826
827 let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
829 let lint_error: LintError = io_error.into();
830 match lint_error {
831 LintError::IoError(_) => {}
832 _ => panic!("Expected IoError variant"),
833 }
834
835 let invalid_input = LintError::InvalidInput("bad input".to_string());
837 assert_eq!(invalid_input.to_string(), "Invalid input: bad input");
838
839 let fix_failed = LintError::FixFailed("couldn't fix".to_string());
840 assert_eq!(fix_failed.to_string(), "Fix failed: couldn't fix");
841
842 let parsing_error = LintError::ParsingError("parse error".to_string());
843 assert_eq!(parsing_error.to_string(), "Parsing error: parse error");
844 }
845
846 #[test]
847 fn test_empty_content_edge_cases() {
848 assert!(!is_rule_disabled_at_line("", "MD001", 0));
849 assert!(!is_rule_disabled_by_comment("", "MD001"));
850
851 let single_comment = "<!-- rumdl-disable -->";
853 assert!(is_rule_disabled_at_line(single_comment, "MD001", 0));
854 assert!(is_rule_disabled_by_comment(single_comment, "MD001"));
855 }
856
857 #[test]
858 fn test_very_long_rule_list() {
859 let many_rules = (1..=100).map(|i| format!("MD{i:03}")).collect::<Vec<_>>().join(" ");
860 let comment = format!("<!-- rumdl-disable {many_rules} -->");
861
862 let parsed = parse_disable_comment(&comment);
863 assert!(parsed.is_some());
864 assert_eq!(parsed.unwrap().len(), 100);
865 }
866
867 #[test]
868 fn test_comment_with_special_characters() {
869 assert_eq!(
871 parse_disable_comment("<!-- rumdl-disable MD001-test -->"),
872 Some(vec!["MD001-test"])
873 );
874
875 assert_eq!(
876 parse_disable_comment("<!-- rumdl-disable MD_001 -->"),
877 Some(vec!["MD_001"])
878 );
879
880 assert_eq!(
881 parse_disable_comment("<!-- rumdl-disable MD.001 -->"),
882 Some(vec!["MD.001"])
883 );
884 }
885}