1use crate::markdownlint_config::markdownlint_to_rumdl_rule_key;
22use crate::utils::code_block_utils::CodeBlockUtils;
23use serde_json::Value as JsonValue;
24use std::collections::{HashMap, HashSet};
25
26fn normalize_rule_name(rule: &str) -> String {
29 markdownlint_to_rumdl_rule_key(rule)
30 .map(|s| s.to_string())
31 .unwrap_or_else(|| rule.to_uppercase())
32}
33
34fn has_inline_config_markers(content: &str) -> bool {
35 if !content.contains("<!--") {
36 return false;
37 }
38 content.contains("markdownlint") || content.contains("rumdl") || content.contains("prettier-ignore")
39}
40
41#[derive(Debug, Clone)]
42pub struct InlineConfig {
43 disabled_at_line: HashMap<usize, HashSet<String>>,
45 enabled_at_line: HashMap<usize, HashSet<String>>,
48 line_disabled_rules: HashMap<usize, HashSet<String>>,
50 file_disabled_rules: HashSet<String>,
52 file_enabled_rules: HashSet<String>,
54 file_rule_config: HashMap<String, JsonValue>,
57}
58
59impl Default for InlineConfig {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl InlineConfig {
66 pub fn new() -> Self {
67 Self {
68 disabled_at_line: HashMap::new(),
69 enabled_at_line: HashMap::new(),
70 line_disabled_rules: HashMap::new(),
71 file_disabled_rules: HashSet::new(),
72 file_enabled_rules: HashSet::new(),
73 file_rule_config: HashMap::new(),
74 }
75 }
76
77 pub fn from_content(content: &str) -> Self {
79 if !has_inline_config_markers(content) {
80 return Self::new();
81 }
82
83 let code_blocks = CodeBlockUtils::detect_code_blocks(content);
84 Self::from_content_with_code_blocks_internal(content, &code_blocks)
85 }
86
87 pub fn from_content_with_code_blocks(content: &str, code_blocks: &[(usize, usize)]) -> Self {
89 if !has_inline_config_markers(content) {
90 return Self::new();
91 }
92
93 Self::from_content_with_code_blocks_internal(content, code_blocks)
94 }
95
96 fn from_content_with_code_blocks_internal(content: &str, code_blocks: &[(usize, usize)]) -> Self {
97 let mut config = Self::new();
98 let lines: Vec<&str> = content.lines().collect();
99
100 let mut line_positions = Vec::with_capacity(lines.len());
102 let mut pos = 0;
103 for line in &lines {
104 line_positions.push(pos);
105 pos += line.len() + 1; }
107
108 let mut currently_disabled = HashSet::new();
110 let mut currently_enabled = HashSet::new(); let mut capture_stack: Vec<(HashSet<String>, HashSet<String>)> = Vec::new();
112
113 for (idx, line) in lines.iter().enumerate() {
114 let line_num = idx + 1; config.disabled_at_line.insert(line_num, currently_disabled.clone());
119 config.enabled_at_line.insert(line_num, currently_enabled.clone());
120
121 let line_start = line_positions[idx];
123 let line_end = line_start + line.len();
124 let in_code_block = code_blocks
125 .iter()
126 .any(|&(block_start, block_end)| line_start >= block_start && line_end <= block_end);
127
128 if in_code_block {
129 continue;
130 }
131
132 if let Some(rules) = parse_disable_file_comment(line) {
135 if rules.is_empty() {
136 config.file_disabled_rules.clear();
138 config.file_disabled_rules.insert("*".to_string());
139 } else {
140 if config.file_disabled_rules.contains("*") {
142 for rule in rules {
144 config.file_enabled_rules.remove(&normalize_rule_name(rule));
145 }
146 } else {
147 for rule in rules {
149 config.file_disabled_rules.insert(normalize_rule_name(rule));
150 }
151 }
152 }
153 }
154
155 if let Some(rules) = parse_enable_file_comment(line) {
157 if rules.is_empty() {
158 config.file_disabled_rules.clear();
160 config.file_enabled_rules.clear();
161 } else {
162 if config.file_disabled_rules.contains("*") {
164 for rule in rules {
166 config.file_enabled_rules.insert(normalize_rule_name(rule));
167 }
168 } else {
169 for rule in rules {
171 config.file_disabled_rules.remove(&normalize_rule_name(rule));
172 }
173 }
174 }
175 }
176
177 if let Some(json_config) = parse_configure_file_comment(line) {
179 if let Some(obj) = json_config.as_object() {
181 for (rule_name, rule_config) in obj {
182 config.file_rule_config.insert(rule_name.clone(), rule_config.clone());
183 }
184 }
185 }
186
187 if let Some(rules) = parse_disable_next_line_comment(line) {
192 let next_line = line_num + 1;
193 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
194 if rules.is_empty() {
195 line_rules.insert("*".to_string());
197 } else {
198 for rule in rules {
199 line_rules.insert(normalize_rule_name(rule));
200 }
201 }
202 }
203
204 if line.contains("<!-- prettier-ignore -->") {
206 let next_line = line_num + 1;
207 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
208 line_rules.insert("*".to_string());
209 }
210
211 if let Some(rules) = parse_disable_line_comment(line) {
213 let line_rules = config.line_disabled_rules.entry(line_num).or_default();
214 if rules.is_empty() {
215 line_rules.insert("*".to_string());
217 } else {
218 for rule in rules {
219 line_rules.insert(normalize_rule_name(rule));
220 }
221 }
222 }
223
224 let mut processed_capture = false;
227 let mut processed_restore = false;
228
229 let mut comment_positions = Vec::new();
231
232 if let Some(pos) = line.find("<!-- markdownlint-disable")
233 && !line[pos..].contains("<!-- markdownlint-disable-line")
234 && !line[pos..].contains("<!-- markdownlint-disable-next-line")
235 {
236 comment_positions.push((pos, "disable"));
237 }
238 if let Some(pos) = line.find("<!-- rumdl-disable")
239 && !line[pos..].contains("<!-- rumdl-disable-line")
240 && !line[pos..].contains("<!-- rumdl-disable-next-line")
241 {
242 comment_positions.push((pos, "disable"));
243 }
244
245 if let Some(pos) = line.find("<!-- markdownlint-enable") {
246 comment_positions.push((pos, "enable"));
247 }
248 if let Some(pos) = line.find("<!-- rumdl-enable") {
249 comment_positions.push((pos, "enable"));
250 }
251
252 if let Some(pos) = line.find("<!-- markdownlint-capture") {
253 comment_positions.push((pos, "capture"));
254 }
255 if let Some(pos) = line.find("<!-- rumdl-capture") {
256 comment_positions.push((pos, "capture"));
257 }
258
259 if let Some(pos) = line.find("<!-- markdownlint-restore") {
260 comment_positions.push((pos, "restore"));
261 }
262 if let Some(pos) = line.find("<!-- rumdl-restore") {
263 comment_positions.push((pos, "restore"));
264 }
265
266 comment_positions.sort_by_key(|&(pos, _)| pos);
268
269 for (_, comment_type) in comment_positions {
271 match comment_type {
272 "disable" => {
273 if let Some(rules) = parse_disable_comment(line) {
274 if rules.is_empty() {
275 currently_disabled.clear();
277 currently_disabled.insert("*".to_string());
278 currently_enabled.clear(); } else {
280 if currently_disabled.contains("*") {
282 for rule in rules {
284 currently_enabled.remove(&normalize_rule_name(rule));
285 }
286 } else {
287 for rule in rules {
289 currently_disabled.insert(normalize_rule_name(rule));
290 }
291 }
292 }
293 }
294 }
295 "enable" => {
296 if let Some(rules) = parse_enable_comment(line) {
297 if rules.is_empty() {
298 currently_disabled.clear();
300 currently_enabled.clear();
301 } else {
302 if currently_disabled.contains("*") {
304 for rule in rules {
306 currently_enabled.insert(normalize_rule_name(rule));
307 }
308 } else {
309 for rule in rules {
311 currently_disabled.remove(&normalize_rule_name(rule));
312 }
313 }
314 }
315 }
316 }
317 "capture" => {
318 if !processed_capture && is_capture_comment(line) {
319 capture_stack.push((currently_disabled.clone(), currently_enabled.clone()));
320 processed_capture = true;
321 }
322 }
323 "restore" => {
324 if !processed_restore && is_restore_comment(line) {
325 if let Some((disabled, enabled)) = capture_stack.pop() {
326 currently_disabled = disabled;
327 currently_enabled = enabled;
328 }
329 processed_restore = true;
330 }
331 }
332 _ => {}
333 }
334 }
335 }
336
337 config
338 }
339
340 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
342 if self.file_disabled_rules.contains("*") {
344 return !self.file_enabled_rules.contains(rule_name);
346 } else if self.file_disabled_rules.contains(rule_name) {
347 return true;
348 }
349
350 if let Some(line_rules) = self.line_disabled_rules.get(&line_number)
352 && (line_rules.contains("*") || line_rules.contains(rule_name))
353 {
354 return true;
355 }
356
357 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
359 if disabled_set.contains("*") {
360 if let Some(enabled_set) = self.enabled_at_line.get(&line_number) {
362 return !enabled_set.contains(rule_name);
363 }
364 return true; } else {
366 return disabled_set.contains(rule_name);
367 }
368 }
369
370 false
371 }
372
373 pub fn get_disabled_rules(&self, line_number: usize) -> HashSet<String> {
375 let mut disabled = HashSet::new();
376
377 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
379 if disabled_set.contains("*") {
380 disabled.insert("*".to_string());
382 } else {
385 for rule in disabled_set {
386 disabled.insert(rule.clone());
387 }
388 }
389 }
390
391 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
393 for rule in line_rules {
394 disabled.insert(rule.clone());
395 }
396 }
397
398 disabled
399 }
400
401 pub fn get_rule_config(&self, rule_name: &str) -> Option<&JsonValue> {
403 self.file_rule_config.get(rule_name)
404 }
405
406 pub fn get_all_rule_configs(&self) -> &HashMap<String, JsonValue> {
408 &self.file_rule_config
409 }
410
411 pub fn export_for_file_index(&self) -> (HashSet<String>, HashMap<usize, HashSet<String>>) {
416 let file_disabled = self.file_disabled_rules.clone();
417
418 let mut line_disabled: HashMap<usize, HashSet<String>> = HashMap::new();
420
421 for (line, rules) in &self.disabled_at_line {
422 line_disabled.entry(*line).or_default().extend(rules.clone());
423 }
424 for (line, rules) in &self.line_disabled_rules {
425 line_disabled.entry(*line).or_default().extend(rules.clone());
426 }
427
428 (file_disabled, line_disabled)
429 }
430}
431
432pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
434 for prefix in &["<!-- rumdl-disable", "<!-- markdownlint-disable"] {
436 if let Some(start) = line.find(prefix) {
437 let after_prefix = &line[start + prefix.len()..];
438
439 if after_prefix.starts_with('-') {
441 continue;
442 }
443
444 if after_prefix.trim_start().starts_with("-->") {
446 return Some(Vec::new()); }
448
449 if let Some(end) = after_prefix.find("-->") {
451 let rules_str = after_prefix[..end].trim();
452 if !rules_str.is_empty() {
453 let rules: Vec<&str> = rules_str.split_whitespace().collect();
454 return Some(rules);
455 }
456 }
457 }
458 }
459
460 None
461}
462
463pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
465 for prefix in &["<!-- rumdl-enable", "<!-- markdownlint-enable"] {
467 if let Some(start) = line.find(prefix) {
468 let after_prefix = &line[start + prefix.len()..];
469
470 if after_prefix.starts_with('-') {
472 continue;
473 }
474
475 if after_prefix.trim_start().starts_with("-->") {
477 return Some(Vec::new()); }
479
480 if let Some(end) = after_prefix.find("-->") {
482 let rules_str = after_prefix[..end].trim();
483 if !rules_str.is_empty() {
484 let rules: Vec<&str> = rules_str.split_whitespace().collect();
485 return Some(rules);
486 }
487 }
488 }
489 }
490
491 None
492}
493
494pub fn parse_disable_line_comment(line: &str) -> Option<Vec<&str>> {
496 for prefix in &["<!-- rumdl-disable-line", "<!-- markdownlint-disable-line"] {
498 if let Some(start) = line.find(prefix) {
499 let after_prefix = &line[start + prefix.len()..];
500
501 if after_prefix.trim_start().starts_with("-->") {
503 return Some(Vec::new()); }
505
506 if let Some(end) = after_prefix.find("-->") {
508 let rules_str = after_prefix[..end].trim();
509 if !rules_str.is_empty() {
510 let rules: Vec<&str> = rules_str.split_whitespace().collect();
511 return Some(rules);
512 }
513 }
514 }
515 }
516
517 None
518}
519
520pub fn parse_disable_next_line_comment(line: &str) -> Option<Vec<&str>> {
522 for prefix in &["<!-- rumdl-disable-next-line", "<!-- markdownlint-disable-next-line"] {
524 if let Some(start) = line.find(prefix) {
525 let after_prefix = &line[start + prefix.len()..];
526
527 if after_prefix.trim_start().starts_with("-->") {
529 return Some(Vec::new()); }
531
532 if let Some(end) = after_prefix.find("-->") {
534 let rules_str = after_prefix[..end].trim();
535 if !rules_str.is_empty() {
536 let rules: Vec<&str> = rules_str.split_whitespace().collect();
537 return Some(rules);
538 }
539 }
540 }
541 }
542
543 None
544}
545
546pub fn is_capture_comment(line: &str) -> bool {
548 line.contains("<!-- markdownlint-capture -->") || line.contains("<!-- rumdl-capture -->")
549}
550
551pub fn is_restore_comment(line: &str) -> bool {
553 line.contains("<!-- markdownlint-restore -->") || line.contains("<!-- rumdl-restore -->")
554}
555
556pub fn parse_disable_file_comment(line: &str) -> Option<Vec<&str>> {
558 for prefix in &["<!-- rumdl-disable-file", "<!-- markdownlint-disable-file"] {
560 if let Some(start) = line.find(prefix) {
561 let after_prefix = &line[start + prefix.len()..];
562
563 if after_prefix.trim_start().starts_with("-->") {
565 return Some(Vec::new()); }
567
568 if let Some(end) = after_prefix.find("-->") {
570 let rules_str = after_prefix[..end].trim();
571 if !rules_str.is_empty() {
572 let rules: Vec<&str> = rules_str.split_whitespace().collect();
573 return Some(rules);
574 }
575 }
576 }
577 }
578
579 None
580}
581
582pub fn parse_enable_file_comment(line: &str) -> Option<Vec<&str>> {
584 for prefix in &["<!-- rumdl-enable-file", "<!-- markdownlint-enable-file"] {
586 if let Some(start) = line.find(prefix) {
587 let after_prefix = &line[start + prefix.len()..];
588
589 if after_prefix.trim_start().starts_with("-->") {
591 return Some(Vec::new()); }
593
594 if let Some(end) = after_prefix.find("-->") {
596 let rules_str = after_prefix[..end].trim();
597 if !rules_str.is_empty() {
598 let rules: Vec<&str> = rules_str.split_whitespace().collect();
599 return Some(rules);
600 }
601 }
602 }
603 }
604
605 None
606}
607
608pub fn parse_configure_file_comment(line: &str) -> Option<JsonValue> {
610 for prefix in &["<!-- rumdl-configure-file", "<!-- markdownlint-configure-file"] {
612 if let Some(start) = line.find(prefix) {
613 let after_prefix = &line[start + prefix.len()..];
614
615 if let Some(end) = after_prefix.find("-->") {
617 let json_str = after_prefix[..end].trim();
618 if !json_str.is_empty() {
619 if let Ok(value) = serde_json::from_str(json_str) {
621 return Some(value);
622 }
623 }
624 }
625 }
626 }
627
628 None
629}
630
631#[derive(Debug, Clone, PartialEq, Eq)]
633pub struct InlineConfigWarning {
634 pub line_number: usize,
636 pub rule_name: String,
638 pub comment_type: String,
640 pub suggestion: Option<String>,
642}
643
644impl InlineConfigWarning {
645 pub fn format_message(&self) -> String {
647 if let Some(ref suggestion) = self.suggestion {
648 format!(
649 "Unknown rule in inline {} comment: {} (did you mean: {}?)",
650 self.comment_type, self.rule_name, suggestion
651 )
652 } else {
653 format!(
654 "Unknown rule in inline {} comment: {}",
655 self.comment_type, self.rule_name
656 )
657 }
658 }
659
660 pub fn print_warning(&self, file_path: &str) {
662 eprintln!(
663 "\x1b[33m[inline config warning]\x1b[0m {}:{}: {}",
664 file_path,
665 self.line_number,
666 self.format_message()
667 );
668 }
669}
670
671pub fn validate_inline_config_rules(content: &str) -> Vec<InlineConfigWarning> {
677 use crate::config::{RULE_ALIAS_MAP, is_valid_rule_name, suggest_similar_key};
678
679 let mut warnings = Vec::new();
680 let all_rule_names: Vec<String> = RULE_ALIAS_MAP.keys().map(|s| s.to_string()).collect();
681
682 for (idx, line) in content.lines().enumerate() {
683 let line_num = idx + 1;
684
685 let mut rule_entries: Vec<(&str, &str)> = Vec::new();
687
688 if let Some(rules) = parse_disable_comment(line) {
690 for rule in rules {
691 rule_entries.push((rule, "disable"));
692 }
693 }
694 if let Some(rules) = parse_enable_comment(line) {
695 for rule in rules {
696 rule_entries.push((rule, "enable"));
697 }
698 }
699 if let Some(rules) = parse_disable_line_comment(line) {
700 for rule in rules {
701 rule_entries.push((rule, "disable-line"));
702 }
703 }
704 if let Some(rules) = parse_disable_next_line_comment(line) {
705 for rule in rules {
706 rule_entries.push((rule, "disable-next-line"));
707 }
708 }
709 if let Some(rules) = parse_disable_file_comment(line) {
710 for rule in rules {
711 rule_entries.push((rule, "disable-file"));
712 }
713 }
714 if let Some(rules) = parse_enable_file_comment(line) {
715 for rule in rules {
716 rule_entries.push((rule, "enable-file"));
717 }
718 }
719
720 if let Some(json_config) = parse_configure_file_comment(line)
722 && let Some(obj) = json_config.as_object()
723 {
724 for rule_name in obj.keys() {
725 if !is_valid_rule_name(rule_name) {
726 let suggestion = suggest_similar_key(rule_name, &all_rule_names)
727 .map(|s| if s.starts_with("MD") { s } else { s.to_lowercase() });
728 warnings.push(InlineConfigWarning {
729 line_number: line_num,
730 rule_name: rule_name.to_string(),
731 comment_type: "configure-file".to_string(),
732 suggestion,
733 });
734 }
735 }
736 }
737
738 for (rule_name, comment_type) in rule_entries {
740 if !is_valid_rule_name(rule_name) {
741 let suggestion = suggest_similar_key(rule_name, &all_rule_names)
742 .map(|s| if s.starts_with("MD") { s } else { s.to_lowercase() });
743 warnings.push(InlineConfigWarning {
744 line_number: line_num,
745 rule_name: rule_name.to_string(),
746 comment_type: comment_type.to_string(),
747 suggestion,
748 });
749 }
750 }
751 }
752
753 warnings
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn test_parse_disable_comment() {
762 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
764 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
765
766 assert_eq!(
768 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
769 Some(vec!["MD001", "MD002"])
770 );
771
772 assert_eq!(parse_disable_comment("Some regular text"), None);
774 }
775
776 #[test]
777 fn test_parse_disable_line_comment() {
778 assert_eq!(
780 parse_disable_line_comment("<!-- markdownlint-disable-line -->"),
781 Some(vec![])
782 );
783
784 assert_eq!(
786 parse_disable_line_comment("<!-- markdownlint-disable-line MD013 -->"),
787 Some(vec!["MD013"])
788 );
789
790 assert_eq!(parse_disable_line_comment("Some regular text"), None);
792 }
793
794 #[test]
795 fn test_inline_config_from_content() {
796 let content = r#"# Test Document
797
798<!-- markdownlint-disable MD013 -->
799This is a very long line that would normally trigger MD013 but it's disabled
800
801<!-- markdownlint-enable MD013 -->
802This line will be checked again
803
804<!-- markdownlint-disable-next-line MD001 -->
805# This heading will not be checked for MD001
806## But this one will
807
808Some text <!-- markdownlint-disable-line MD013 -->
809
810<!-- markdownlint-capture -->
811<!-- markdownlint-disable MD001 MD002 -->
812# Heading with MD001 disabled
813<!-- markdownlint-restore -->
814# Heading with MD001 enabled again
815"#;
816
817 let config = InlineConfig::from_content(content);
818
819 assert!(config.is_rule_disabled("MD013", 4));
821
822 assert!(!config.is_rule_disabled("MD013", 7));
824
825 assert!(config.is_rule_disabled("MD001", 10));
827
828 assert!(!config.is_rule_disabled("MD001", 11));
830
831 assert!(config.is_rule_disabled("MD013", 13));
833
834 assert!(!config.is_rule_disabled("MD001", 19));
836 }
837
838 #[test]
839 fn test_capture_restore() {
840 let content = r#"<!-- markdownlint-disable MD001 -->
841<!-- markdownlint-capture -->
842<!-- markdownlint-disable MD002 MD003 -->
843<!-- markdownlint-restore -->
844Some content after restore
845"#;
846
847 let config = InlineConfig::from_content(content);
848
849 assert!(config.is_rule_disabled("MD001", 5));
851 assert!(!config.is_rule_disabled("MD002", 5));
852 assert!(!config.is_rule_disabled("MD003", 5));
853 }
854
855 #[test]
856 fn test_validate_inline_config_rules_unknown_rule() {
857 let content = "<!-- rumdl-disable abc -->\nSome content";
858 let warnings = validate_inline_config_rules(content);
859 assert_eq!(warnings.len(), 1);
860 assert_eq!(warnings[0].line_number, 1);
861 assert_eq!(warnings[0].rule_name, "abc");
862 assert_eq!(warnings[0].comment_type, "disable");
863 }
864
865 #[test]
866 fn test_validate_inline_config_rules_valid_rule() {
867 let content = "<!-- rumdl-disable MD001 -->\nSome content";
868 let warnings = validate_inline_config_rules(content);
869 assert!(
870 warnings.is_empty(),
871 "MD001 is a valid rule, should not produce warnings"
872 );
873 }
874
875 #[test]
876 fn test_validate_inline_config_rules_alias() {
877 let content = "<!-- rumdl-disable heading-increment -->\nSome content";
878 let warnings = validate_inline_config_rules(content);
879 assert!(warnings.is_empty(), "heading-increment is a valid alias for MD001");
880 }
881
882 #[test]
883 fn test_validate_inline_config_rules_multiple_unknown() {
884 let content = r#"<!-- rumdl-disable abc xyz -->
885<!-- rumdl-disable-line foo -->
886<!-- markdownlint-disable-next-line bar -->
887"#;
888 let warnings = validate_inline_config_rules(content);
889 assert_eq!(warnings.len(), 4);
890 assert_eq!(warnings[0].rule_name, "abc");
891 assert_eq!(warnings[1].rule_name, "xyz");
892 assert_eq!(warnings[2].rule_name, "foo");
893 assert_eq!(warnings[3].rule_name, "bar");
894 }
895
896 #[test]
897 fn test_validate_inline_config_rules_suggestion() {
898 let content = "<!-- rumdl-disable MD00 -->\n";
900 let warnings = validate_inline_config_rules(content);
901 assert_eq!(warnings.len(), 1);
902 assert!(warnings[0].suggestion.is_some());
904 }
905
906 #[test]
907 fn test_validate_inline_config_rules_file_comments() {
908 let content = "<!-- rumdl-disable-file nonexistent -->\n<!-- markdownlint-enable-file another_fake -->";
909 let warnings = validate_inline_config_rules(content);
910 assert_eq!(warnings.len(), 2);
911 assert_eq!(warnings[0].comment_type, "disable-file");
912 assert_eq!(warnings[1].comment_type, "enable-file");
913 }
914
915 #[test]
916 fn test_validate_inline_config_rules_global_disable() {
917 let content = "<!-- rumdl-disable -->\n<!-- markdownlint-enable -->";
919 let warnings = validate_inline_config_rules(content);
920 assert!(warnings.is_empty(), "Global disable/enable should not produce warnings");
921 }
922
923 #[test]
924 fn test_validate_inline_config_rules_mixed_valid_invalid() {
925 let content = "<!-- rumdl-disable MD001 abc MD003 xyz -->";
927 let warnings = validate_inline_config_rules(content);
928 assert_eq!(warnings.len(), 2);
929 assert_eq!(warnings[0].rule_name, "abc");
930 assert_eq!(warnings[1].rule_name, "xyz");
931 }
932
933 #[test]
934 fn test_validate_inline_config_rules_configure_file() {
935 let content =
937 r#"<!-- rumdl-configure-file { "MD013": { "line_length": 120 }, "nonexistent": { "foo": true } } -->"#;
938 let warnings = validate_inline_config_rules(content);
939 assert_eq!(warnings.len(), 1);
940 assert_eq!(warnings[0].rule_name, "nonexistent");
941 assert_eq!(warnings[0].comment_type, "configure-file");
942 }
943
944 #[test]
945 fn test_validate_inline_config_rules_markdownlint_variants() {
946 let content = r#"<!-- markdownlint-disable unknown_rule -->
948<!-- markdownlint-enable another_fake -->
949<!-- markdownlint-disable-line bad_rule -->
950<!-- markdownlint-disable-next-line fake_rule -->
951<!-- markdownlint-disable-file missing_rule -->
952<!-- markdownlint-enable-file nonexistent -->
953"#;
954 let warnings = validate_inline_config_rules(content);
955 assert_eq!(warnings.len(), 6);
956 assert_eq!(warnings[0].rule_name, "unknown_rule");
957 assert_eq!(warnings[1].rule_name, "another_fake");
958 assert_eq!(warnings[2].rule_name, "bad_rule");
959 assert_eq!(warnings[3].rule_name, "fake_rule");
960 assert_eq!(warnings[4].rule_name, "missing_rule");
961 assert_eq!(warnings[5].rule_name, "nonexistent");
962 }
963
964 #[test]
965 fn test_validate_inline_config_rules_markdownlint_configure_file() {
966 let content = r#"<!-- markdownlint-configure-file { "fake_rule": {} } -->"#;
967 let warnings = validate_inline_config_rules(content);
968 assert_eq!(warnings.len(), 1);
969 assert_eq!(warnings[0].rule_name, "fake_rule");
970 assert_eq!(warnings[0].comment_type, "configure-file");
971 }
972
973 #[test]
974 fn test_get_rule_config_from_configure_file() {
975 let content = r#"<!-- markdownlint-configure-file {"MD013": {"line_length": 50}} -->
976
977This is a test line."#;
978
979 let inline_config = InlineConfig::from_content(content);
980 let config_override = inline_config.get_rule_config("MD013");
981
982 assert!(config_override.is_some(), "MD013 config should be found");
983 let json = config_override.unwrap();
984 assert!(json.is_object(), "Config should be an object");
985 let obj = json.as_object().unwrap();
986 assert!(obj.contains_key("line_length"), "Should have line_length key");
987 assert_eq!(obj.get("line_length").unwrap().as_u64().unwrap(), 50);
988 }
989
990 #[test]
991 fn test_get_rule_config_tables_false() {
992 let content = r#"<!-- markdownlint-configure-file {"MD013": {"tables": false}} -->"#;
994
995 let inline_config = InlineConfig::from_content(content);
996 let config_override = inline_config.get_rule_config("MD013");
997
998 assert!(config_override.is_some(), "MD013 config should be found");
999 let json = config_override.unwrap();
1000 let obj = json.as_object().unwrap();
1001 assert!(obj.contains_key("tables"), "Should have tables key");
1002 assert!(!obj.get("tables").unwrap().as_bool().unwrap());
1003 }
1004}