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 config_aliases(&self) -> Option<std::collections::HashMap<String, String>> {
165 None
166 }
167
168 fn fix_capability(&self) -> FixCapability {
170 FixCapability::FullyFixable }
172
173 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
175 where
176 Self: Sized,
177 {
178 panic!(
179 "from_config not implemented for rule: {}",
180 std::any::type_name::<Self>()
181 );
182 }
183}
184
185dyn_clone::clone_trait_object!(Rule);
187
188pub trait RuleExt {
190 fn downcast_ref<T: 'static>(&self) -> Option<&T>;
191}
192
193impl<R: Rule + 'static> RuleExt for Box<R> {
194 fn downcast_ref<T: 'static>(&self) -> Option<&T> {
195 if std::any::TypeId::of::<R>() == std::any::TypeId::of::<T>() {
196 unsafe { Some(&*(self.as_ref() as *const _ as *const T)) }
197 } else {
198 None
199 }
200 }
201}
202
203pub fn is_rule_disabled_at_line(content: &str, rule_name: &str, line_num: usize) -> bool {
205 let lines: Vec<&str> = content.lines().collect();
206 let mut is_disabled = false;
207
208 for (i, line) in lines.iter().enumerate() {
210 if i > line_num {
212 break;
213 }
214
215 if crate::rules::code_block_utils::CodeBlockUtils::is_in_code_block(content, i) {
217 continue;
218 }
219
220 let line = line.trim();
221
222 if let Some(rules) = parse_disable_comment(line)
224 && (rules.is_empty() || rules.contains(&rule_name))
225 {
226 is_disabled = true;
227 continue;
228 }
229
230 if let Some(rules) = parse_enable_comment(line)
232 && (rules.is_empty() || rules.contains(&rule_name))
233 {
234 is_disabled = false;
235 continue;
236 }
237 }
238
239 is_disabled
240}
241
242pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
244 if let Some(start) = line.find("<!-- rumdl-disable") {
246 let after_prefix = &line[start + "<!-- rumdl-disable".len()..];
247
248 if after_prefix.trim_start().starts_with("-->") {
250 return Some(Vec::new()); }
252
253 if let Some(end) = after_prefix.find("-->") {
255 let rules_str = after_prefix[..end].trim();
256 if !rules_str.is_empty() {
257 let rules: Vec<&str> = rules_str.split_whitespace().collect();
258 return Some(rules);
259 }
260 }
261 }
262
263 if let Some(start) = line.find("<!-- markdownlint-disable") {
265 let after_prefix = &line[start + "<!-- markdownlint-disable".len()..];
266
267 if after_prefix.trim_start().starts_with("-->") {
269 return Some(Vec::new()); }
271
272 if let Some(end) = after_prefix.find("-->") {
274 let rules_str = after_prefix[..end].trim();
275 if !rules_str.is_empty() {
276 let rules: Vec<&str> = rules_str.split_whitespace().collect();
277 return Some(rules);
278 }
279 }
280 }
281
282 None
283}
284
285pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
287 if let Some(start) = line.find("<!-- rumdl-enable") {
289 let after_prefix = &line[start + "<!-- rumdl-enable".len()..];
290
291 if after_prefix.trim_start().starts_with("-->") {
293 return Some(Vec::new()); }
295
296 if let Some(end) = after_prefix.find("-->") {
298 let rules_str = after_prefix[..end].trim();
299 if !rules_str.is_empty() {
300 let rules: Vec<&str> = rules_str.split_whitespace().collect();
301 return Some(rules);
302 }
303 }
304 }
305
306 if let Some(start) = line.find("<!-- markdownlint-enable") {
308 let after_prefix = &line[start + "<!-- markdownlint-enable".len()..];
309
310 if after_prefix.trim_start().starts_with("-->") {
312 return Some(Vec::new()); }
314
315 if let Some(end) = after_prefix.find("-->") {
317 let rules_str = after_prefix[..end].trim();
318 if !rules_str.is_empty() {
319 let rules: Vec<&str> = rules_str.split_whitespace().collect();
320 return Some(rules);
321 }
322 }
323 }
324
325 None
326}
327
328pub fn is_rule_disabled_by_comment(content: &str, rule_name: &str) -> bool {
330 let lines: Vec<&str> = content.lines().collect();
332 is_rule_disabled_at_line(content, rule_name, lines.len())
333}
334
335pub trait MaybeDocumentStructure {
337 fn check_with_structure_opt(
338 &self,
339 ctx: &LintContext,
340 structure: &crate::utils::document_structure::DocumentStructure,
341 ) -> Option<LintResult>;
342}
343
344impl<T> MaybeDocumentStructure for T
345where
346 T: Rule + crate::utils::document_structure::DocumentStructureExtensions + 'static,
347{
348 fn check_with_structure_opt(
349 &self,
350 ctx: &LintContext,
351 structure: &crate::utils::document_structure::DocumentStructure,
352 ) -> Option<LintResult> {
353 Some(self.check_with_structure(ctx, structure))
354 }
355}
356
357impl MaybeDocumentStructure for dyn Rule {
358 fn check_with_structure_opt(
359 &self,
360 _ctx: &LintContext,
361 _structure: &crate::utils::document_structure::DocumentStructure,
362 ) -> Option<LintResult> {
363 None
364 }
365}
366
367pub trait MaybeAst {
369 fn check_with_ast_opt(&self, ctx: &LintContext, ast: &MarkdownAst) -> Option<LintResult>;
370}
371
372impl<T> MaybeAst for T
373where
374 T: Rule + AstExtensions + 'static,
375{
376 fn check_with_ast_opt(&self, ctx: &LintContext, ast: &MarkdownAst) -> Option<LintResult> {
377 if self.has_relevant_ast_elements(ctx, ast) {
378 Some(self.check_with_ast(ctx, ast))
379 } else {
380 None
381 }
382 }
383}
384
385impl MaybeAst for dyn Rule {
386 fn check_with_ast_opt(&self, _ctx: &LintContext, _ast: &MarkdownAst) -> Option<LintResult> {
387 None
388 }
389}
390
391pub trait AstExtensions {
393 fn has_relevant_ast_elements(&self, ctx: &LintContext, ast: &MarkdownAst) -> bool;
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_parse_disable_comment() {
403 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
405
406 assert_eq!(
408 parse_disable_comment("<!-- rumdl-disable MD001 MD002 -->"),
409 Some(vec!["MD001", "MD002"])
410 );
411
412 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
414
415 assert_eq!(
417 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
418 Some(vec!["MD001", "MD002"])
419 );
420
421 assert_eq!(parse_disable_comment("<!-- some other comment -->"), None);
423
424 assert_eq!(
426 parse_disable_comment(" <!-- rumdl-disable MD013 --> "),
427 Some(vec!["MD013"])
428 );
429 }
430
431 #[test]
432 fn test_parse_enable_comment() {
433 assert_eq!(parse_enable_comment("<!-- rumdl-enable -->"), Some(vec![]));
435
436 assert_eq!(
438 parse_enable_comment("<!-- rumdl-enable MD001 MD002 -->"),
439 Some(vec!["MD001", "MD002"])
440 );
441
442 assert_eq!(parse_enable_comment("<!-- markdownlint-enable -->"), Some(vec![]));
444
445 assert_eq!(
447 parse_enable_comment("<!-- markdownlint-enable MD001 MD002 -->"),
448 Some(vec!["MD001", "MD002"])
449 );
450
451 assert_eq!(parse_enable_comment("<!-- some other comment -->"), None);
453 }
454
455 #[test]
456 fn test_is_rule_disabled_at_line() {
457 let content = r#"# Test
458<!-- rumdl-disable MD013 -->
459This is a long line
460<!-- rumdl-enable MD013 -->
461This is another line
462<!-- markdownlint-disable MD042 -->
463Empty link: []()
464<!-- markdownlint-enable MD042 -->
465Final line"#;
466
467 assert!(is_rule_disabled_at_line(content, "MD013", 2));
469
470 assert!(!is_rule_disabled_at_line(content, "MD013", 4));
472
473 assert!(is_rule_disabled_at_line(content, "MD042", 6));
475
476 assert!(!is_rule_disabled_at_line(content, "MD042", 8));
478
479 assert!(!is_rule_disabled_at_line(content, "MD001", 5));
481 }
482
483 #[test]
484 fn test_parse_disable_comment_edge_cases() {
485 assert_eq!(parse_disable_comment("<!-- rumdl-disable-->"), Some(vec![]));
487
488 assert_eq!(
490 parse_disable_comment("<!-- rumdl-disable MD001 MD002 -->"),
491 None
492 );
493
494 assert_eq!(
496 parse_disable_comment("<!-- rumdl-disable\tMD001\tMD002 -->"),
497 Some(vec!["MD001", "MD002"])
498 );
499
500 assert_eq!(
502 parse_disable_comment("Some text <!-- rumdl-disable MD001 --> more text"),
503 Some(vec!["MD001"])
504 );
505
506 assert_eq!(parse_disable_comment("<!-- rumdl-disable MD001"), None);
508
509 assert_eq!(parse_disable_comment("rumdl-disable MD001 -->"), None);
511
512 assert_eq!(parse_disable_comment("<!-- RUMDL-DISABLE -->"), None);
514 assert_eq!(parse_disable_comment("<!-- RuMdL-DiSaBlE -->"), None);
515
516 assert_eq!(
518 parse_disable_comment("<!-- rumdl-disable\nMD001 -->"),
519 Some(vec!["MD001"])
520 );
521
522 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
524
525 assert_eq!(
527 parse_disable_comment("<!-- rumdl-disable MD001 MD001 MD002 -->"),
528 Some(vec!["MD001", "MD001", "MD002"])
529 );
530 }
531
532 #[test]
533 fn test_parse_enable_comment_edge_cases() {
534 assert_eq!(parse_enable_comment("<!-- rumdl-enable-->"), Some(vec![]));
536
537 assert_eq!(parse_enable_comment("<!-- rumdl-enable MD001 MD002 -->"), None);
539
540 assert_eq!(
542 parse_enable_comment("<!-- rumdl-enable\tMD001\tMD002 -->"),
543 Some(vec!["MD001", "MD002"])
544 );
545
546 assert_eq!(
548 parse_enable_comment("Some text <!-- rumdl-enable MD001 --> more text"),
549 Some(vec!["MD001"])
550 );
551
552 assert_eq!(parse_enable_comment("<!-- rumdl-enable MD001"), None);
554
555 assert_eq!(parse_enable_comment("rumdl-enable MD001 -->"), None);
557
558 assert_eq!(parse_enable_comment("<!-- RUMDL-ENABLE -->"), None);
560 assert_eq!(parse_enable_comment("<!-- RuMdL-EnAbLe -->"), None);
561
562 assert_eq!(
564 parse_enable_comment("<!-- rumdl-enable\nMD001 -->"),
565 Some(vec!["MD001"])
566 );
567
568 assert_eq!(parse_enable_comment("<!-- rumdl-enable -->"), Some(vec![]));
570
571 assert_eq!(
573 parse_enable_comment("<!-- rumdl-enable MD001 MD001 MD002 -->"),
574 Some(vec!["MD001", "MD001", "MD002"])
575 );
576 }
577
578 #[test]
579 fn test_nested_disable_enable_comments() {
580 let content = r#"# Document
581<!-- rumdl-disable -->
582All rules disabled here
583<!-- rumdl-disable MD001 -->
584Still all disabled (redundant)
585<!-- rumdl-enable MD001 -->
586Only MD001 enabled, others still disabled
587<!-- rumdl-enable -->
588All rules enabled again"#;
589
590 assert!(is_rule_disabled_at_line(content, "MD001", 2));
592 assert!(is_rule_disabled_at_line(content, "MD002", 2));
593
594 assert!(is_rule_disabled_at_line(content, "MD001", 4));
596 assert!(is_rule_disabled_at_line(content, "MD002", 4));
597
598 assert!(!is_rule_disabled_at_line(content, "MD001", 6));
600 assert!(is_rule_disabled_at_line(content, "MD002", 6));
601
602 assert!(!is_rule_disabled_at_line(content, "MD001", 8));
604 assert!(!is_rule_disabled_at_line(content, "MD002", 8));
605 }
606
607 #[test]
608 fn test_mixed_comment_styles() {
609 let content = r#"# Document
610<!-- markdownlint-disable MD001 -->
611MD001 disabled via markdownlint
612<!-- rumdl-enable MD001 -->
613MD001 enabled via rumdl
614<!-- rumdl-disable -->
615All disabled via rumdl
616<!-- markdownlint-enable -->
617All enabled via markdownlint"#;
618
619 assert!(is_rule_disabled_at_line(content, "MD001", 2));
621 assert!(!is_rule_disabled_at_line(content, "MD002", 2));
622
623 assert!(!is_rule_disabled_at_line(content, "MD001", 4));
625 assert!(!is_rule_disabled_at_line(content, "MD002", 4));
626
627 assert!(is_rule_disabled_at_line(content, "MD001", 6));
629 assert!(is_rule_disabled_at_line(content, "MD002", 6));
630
631 assert!(!is_rule_disabled_at_line(content, "MD001", 8));
633 assert!(!is_rule_disabled_at_line(content, "MD002", 8));
634 }
635
636 #[test]
637 fn test_comments_in_code_blocks() {
638 let content = r#"# Document
639```markdown
640<!-- rumdl-disable MD001 -->
641This is in a code block, should not affect rules
642```
643MD001 should still be enabled here"#;
644
645 assert!(!is_rule_disabled_at_line(content, "MD001", 5));
647
648 let indented_content = r#"# Document
650
651 <!-- rumdl-disable MD001 -->
652 This is in an indented code block
653
654MD001 should still be enabled here"#;
655
656 assert!(!is_rule_disabled_at_line(indented_content, "MD001", 5));
657 }
658
659 #[test]
660 fn test_comments_with_unicode() {
661 assert_eq!(
663 parse_disable_comment("<!-- rumdl-disable MD001 --> 你好"),
664 Some(vec!["MD001"])
665 );
666
667 assert_eq!(
668 parse_disable_comment("🚀 <!-- rumdl-disable MD001 --> 🎉"),
669 Some(vec!["MD001"])
670 );
671 }
672
673 #[test]
674 fn test_rule_disabled_at_specific_lines() {
675 let content = r#"Line 0
676<!-- rumdl-disable MD001 MD002 -->
677Line 2
678Line 3
679<!-- rumdl-enable MD001 -->
680Line 5
681<!-- rumdl-disable -->
682Line 7
683<!-- rumdl-enable MD002 -->
684Line 9"#;
685
686 assert!(!is_rule_disabled_at_line(content, "MD001", 0));
688 assert!(!is_rule_disabled_at_line(content, "MD002", 0));
689
690 assert!(is_rule_disabled_at_line(content, "MD001", 2));
691 assert!(is_rule_disabled_at_line(content, "MD002", 2));
692
693 assert!(is_rule_disabled_at_line(content, "MD001", 3));
694 assert!(is_rule_disabled_at_line(content, "MD002", 3));
695
696 assert!(!is_rule_disabled_at_line(content, "MD001", 5));
697 assert!(is_rule_disabled_at_line(content, "MD002", 5));
698
699 assert!(is_rule_disabled_at_line(content, "MD001", 7));
700 assert!(is_rule_disabled_at_line(content, "MD002", 7));
701
702 assert!(is_rule_disabled_at_line(content, "MD001", 9));
703 assert!(!is_rule_disabled_at_line(content, "MD002", 9));
704 }
705
706 #[test]
707 fn test_is_rule_disabled_by_comment() {
708 let content = r#"# Document
709<!-- rumdl-disable MD001 -->
710Content here"#;
711
712 assert!(is_rule_disabled_by_comment(content, "MD001"));
713 assert!(!is_rule_disabled_by_comment(content, "MD002"));
714
715 let content2 = r#"# Document
716<!-- rumdl-disable -->
717Content here"#;
718
719 assert!(is_rule_disabled_by_comment(content2, "MD001"));
720 assert!(is_rule_disabled_by_comment(content2, "MD002"));
721 }
722
723 #[test]
724 fn test_comment_at_end_of_file() {
725 let content = "# Document\nContent\n<!-- rumdl-disable MD001 -->";
726
727 assert!(is_rule_disabled_by_comment(content, "MD001"));
729 assert!(!is_rule_disabled_at_line(content, "MD001", 1));
731 assert!(is_rule_disabled_at_line(content, "MD001", 2));
733 }
734
735 #[test]
736 fn test_multiple_comments_same_line() {
737 assert_eq!(
739 parse_disable_comment("<!-- rumdl-disable MD001 --> <!-- rumdl-disable MD002 -->"),
740 Some(vec!["MD001"])
741 );
742
743 assert_eq!(
744 parse_enable_comment("<!-- rumdl-enable MD001 --> <!-- rumdl-enable MD002 -->"),
745 Some(vec!["MD001"])
746 );
747 }
748
749 #[test]
750 fn test_severity_serialization() {
751 let warning = LintWarning {
752 message: "Test warning".to_string(),
753 line: 1,
754 column: 1,
755 end_line: 1,
756 end_column: 10,
757 severity: Severity::Warning,
758 fix: None,
759 rule_name: Some("MD001"),
760 };
761
762 let serialized = serde_json::to_string(&warning).unwrap();
763 assert!(serialized.contains("\"severity\":\"Warning\""));
764
765 let error = LintWarning {
766 severity: Severity::Error,
767 ..warning
768 };
769
770 let serialized = serde_json::to_string(&error).unwrap();
771 assert!(serialized.contains("\"severity\":\"Error\""));
772 }
773
774 #[test]
775 fn test_fix_serialization() {
776 let fix = Fix {
777 range: 0..10,
778 replacement: "fixed text".to_string(),
779 };
780
781 let warning = LintWarning {
782 message: "Test warning".to_string(),
783 line: 1,
784 column: 1,
785 end_line: 1,
786 end_column: 10,
787 severity: Severity::Warning,
788 fix: Some(fix),
789 rule_name: Some("MD001"),
790 };
791
792 let serialized = serde_json::to_string(&warning).unwrap();
793 assert!(serialized.contains("\"fix\""));
794 assert!(serialized.contains("\"replacement\":\"fixed text\""));
795 }
796
797 #[test]
798 fn test_rule_category_equality() {
799 assert_eq!(RuleCategory::Heading, RuleCategory::Heading);
800 assert_ne!(RuleCategory::Heading, RuleCategory::List);
801
802 let categories = [
804 RuleCategory::Heading,
805 RuleCategory::List,
806 RuleCategory::CodeBlock,
807 RuleCategory::Link,
808 RuleCategory::Image,
809 RuleCategory::Html,
810 RuleCategory::Emphasis,
811 RuleCategory::Whitespace,
812 RuleCategory::Blockquote,
813 RuleCategory::Table,
814 RuleCategory::FrontMatter,
815 RuleCategory::Other,
816 ];
817
818 for (i, cat1) in categories.iter().enumerate() {
819 for (j, cat2) in categories.iter().enumerate() {
820 if i == j {
821 assert_eq!(cat1, cat2);
822 } else {
823 assert_ne!(cat1, cat2);
824 }
825 }
826 }
827 }
828
829 #[test]
830 fn test_lint_error_conversions() {
831 use std::io;
832
833 let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
835 let lint_error: LintError = io_error.into();
836 match lint_error {
837 LintError::IoError(_) => {}
838 _ => panic!("Expected IoError variant"),
839 }
840
841 let invalid_input = LintError::InvalidInput("bad input".to_string());
843 assert_eq!(invalid_input.to_string(), "Invalid input: bad input");
844
845 let fix_failed = LintError::FixFailed("couldn't fix".to_string());
846 assert_eq!(fix_failed.to_string(), "Fix failed: couldn't fix");
847
848 let parsing_error = LintError::ParsingError("parse error".to_string());
849 assert_eq!(parsing_error.to_string(), "Parsing error: parse error");
850 }
851
852 #[test]
853 fn test_empty_content_edge_cases() {
854 assert!(!is_rule_disabled_at_line("", "MD001", 0));
855 assert!(!is_rule_disabled_by_comment("", "MD001"));
856
857 let single_comment = "<!-- rumdl-disable -->";
859 assert!(is_rule_disabled_at_line(single_comment, "MD001", 0));
860 assert!(is_rule_disabled_by_comment(single_comment, "MD001"));
861 }
862
863 #[test]
864 fn test_very_long_rule_list() {
865 let many_rules = (1..=100).map(|i| format!("MD{i:03}")).collect::<Vec<_>>().join(" ");
866 let comment = format!("<!-- rumdl-disable {many_rules} -->");
867
868 let parsed = parse_disable_comment(&comment);
869 assert!(parsed.is_some());
870 assert_eq!(parsed.unwrap().len(), 100);
871 }
872
873 #[test]
874 fn test_comment_with_special_characters() {
875 assert_eq!(
877 parse_disable_comment("<!-- rumdl-disable MD001-test -->"),
878 Some(vec!["MD001-test"])
879 );
880
881 assert_eq!(
882 parse_disable_comment("<!-- rumdl-disable MD_001 -->"),
883 Some(vec!["MD_001"])
884 );
885
886 assert_eq!(
887 parse_disable_comment("<!-- rumdl-disable MD.001 -->"),
888 Some(vec!["MD.001"])
889 );
890 }
891}