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
34#[derive(Debug, Clone)]
35pub struct InlineConfig {
36 disabled_at_line: HashMap<usize, HashSet<String>>,
38 enabled_at_line: HashMap<usize, HashSet<String>>,
41 line_disabled_rules: HashMap<usize, HashSet<String>>,
43 file_disabled_rules: HashSet<String>,
45 file_enabled_rules: HashSet<String>,
47 file_rule_config: HashMap<String, JsonValue>,
50}
51
52impl Default for InlineConfig {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl InlineConfig {
59 pub fn new() -> Self {
60 Self {
61 disabled_at_line: HashMap::new(),
62 enabled_at_line: HashMap::new(),
63 line_disabled_rules: HashMap::new(),
64 file_disabled_rules: HashSet::new(),
65 file_enabled_rules: HashSet::new(),
66 file_rule_config: HashMap::new(),
67 }
68 }
69
70 pub fn from_content(content: &str) -> Self {
72 let mut config = Self::new();
73 let lines: Vec<&str> = content.lines().collect();
74
75 let code_blocks = CodeBlockUtils::detect_code_blocks(content);
77
78 let mut line_positions = Vec::with_capacity(lines.len());
80 let mut pos = 0;
81 for line in &lines {
82 line_positions.push(pos);
83 pos += line.len() + 1; }
85
86 let mut currently_disabled = HashSet::new();
88 let mut currently_enabled = HashSet::new(); let mut capture_stack: Vec<(HashSet<String>, HashSet<String>)> = Vec::new();
90
91 for (idx, line) in lines.iter().enumerate() {
92 let line_num = idx + 1; config.disabled_at_line.insert(line_num, currently_disabled.clone());
97 config.enabled_at_line.insert(line_num, currently_enabled.clone());
98
99 let line_start = line_positions[idx];
101 let line_end = line_start + line.len();
102 let in_code_block = code_blocks
103 .iter()
104 .any(|&(block_start, block_end)| line_start >= block_start && line_end <= block_end);
105
106 if in_code_block {
107 continue;
108 }
109
110 if let Some(rules) = parse_disable_file_comment(line) {
113 if rules.is_empty() {
114 config.file_disabled_rules.clear();
116 config.file_disabled_rules.insert("*".to_string());
117 } else {
118 if config.file_disabled_rules.contains("*") {
120 for rule in rules {
122 config.file_enabled_rules.remove(&normalize_rule_name(rule));
123 }
124 } else {
125 for rule in rules {
127 config.file_disabled_rules.insert(normalize_rule_name(rule));
128 }
129 }
130 }
131 }
132
133 if let Some(rules) = parse_enable_file_comment(line) {
135 if rules.is_empty() {
136 config.file_disabled_rules.clear();
138 config.file_enabled_rules.clear();
139 } else {
140 if config.file_disabled_rules.contains("*") {
142 for rule in rules {
144 config.file_enabled_rules.insert(normalize_rule_name(rule));
145 }
146 } else {
147 for rule in rules {
149 config.file_disabled_rules.remove(&normalize_rule_name(rule));
150 }
151 }
152 }
153 }
154
155 if let Some(json_config) = parse_configure_file_comment(line) {
157 if let Some(obj) = json_config.as_object() {
159 for (rule_name, rule_config) in obj {
160 config.file_rule_config.insert(rule_name.clone(), rule_config.clone());
161 }
162 }
163 }
164
165 if let Some(rules) = parse_disable_next_line_comment(line) {
170 let next_line = line_num + 1;
171 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
172 if rules.is_empty() {
173 line_rules.insert("*".to_string());
175 } else {
176 for rule in rules {
177 line_rules.insert(normalize_rule_name(rule));
178 }
179 }
180 }
181
182 if line.contains("<!-- prettier-ignore -->") {
184 let next_line = line_num + 1;
185 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
186 line_rules.insert("*".to_string());
187 }
188
189 if let Some(rules) = parse_disable_line_comment(line) {
191 let line_rules = config.line_disabled_rules.entry(line_num).or_default();
192 if rules.is_empty() {
193 line_rules.insert("*".to_string());
195 } else {
196 for rule in rules {
197 line_rules.insert(normalize_rule_name(rule));
198 }
199 }
200 }
201
202 let mut processed_capture = false;
205 let mut processed_restore = false;
206
207 let mut comment_positions = Vec::new();
209
210 if let Some(pos) = line.find("<!-- markdownlint-disable")
211 && !line[pos..].contains("<!-- markdownlint-disable-line")
212 && !line[pos..].contains("<!-- markdownlint-disable-next-line")
213 {
214 comment_positions.push((pos, "disable"));
215 }
216 if let Some(pos) = line.find("<!-- rumdl-disable")
217 && !line[pos..].contains("<!-- rumdl-disable-line")
218 && !line[pos..].contains("<!-- rumdl-disable-next-line")
219 {
220 comment_positions.push((pos, "disable"));
221 }
222
223 if let Some(pos) = line.find("<!-- markdownlint-enable") {
224 comment_positions.push((pos, "enable"));
225 }
226 if let Some(pos) = line.find("<!-- rumdl-enable") {
227 comment_positions.push((pos, "enable"));
228 }
229
230 if let Some(pos) = line.find("<!-- markdownlint-capture") {
231 comment_positions.push((pos, "capture"));
232 }
233 if let Some(pos) = line.find("<!-- rumdl-capture") {
234 comment_positions.push((pos, "capture"));
235 }
236
237 if let Some(pos) = line.find("<!-- markdownlint-restore") {
238 comment_positions.push((pos, "restore"));
239 }
240 if let Some(pos) = line.find("<!-- rumdl-restore") {
241 comment_positions.push((pos, "restore"));
242 }
243
244 comment_positions.sort_by_key(|&(pos, _)| pos);
246
247 for (_, comment_type) in comment_positions {
249 match comment_type {
250 "disable" => {
251 if let Some(rules) = parse_disable_comment(line) {
252 if rules.is_empty() {
253 currently_disabled.clear();
255 currently_disabled.insert("*".to_string());
256 currently_enabled.clear(); } else {
258 if currently_disabled.contains("*") {
260 for rule in rules {
262 currently_enabled.remove(&normalize_rule_name(rule));
263 }
264 } else {
265 for rule in rules {
267 currently_disabled.insert(normalize_rule_name(rule));
268 }
269 }
270 }
271 }
272 }
273 "enable" => {
274 if let Some(rules) = parse_enable_comment(line) {
275 if rules.is_empty() {
276 currently_disabled.clear();
278 currently_enabled.clear();
279 } else {
280 if currently_disabled.contains("*") {
282 for rule in rules {
284 currently_enabled.insert(normalize_rule_name(rule));
285 }
286 } else {
287 for rule in rules {
289 currently_disabled.remove(&normalize_rule_name(rule));
290 }
291 }
292 }
293 }
294 }
295 "capture" => {
296 if !processed_capture && is_capture_comment(line) {
297 capture_stack.push((currently_disabled.clone(), currently_enabled.clone()));
298 processed_capture = true;
299 }
300 }
301 "restore" => {
302 if !processed_restore && is_restore_comment(line) {
303 if let Some((disabled, enabled)) = capture_stack.pop() {
304 currently_disabled = disabled;
305 currently_enabled = enabled;
306 }
307 processed_restore = true;
308 }
309 }
310 _ => {}
311 }
312 }
313 }
314
315 config
316 }
317
318 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
320 if self.file_disabled_rules.contains("*") {
322 return !self.file_enabled_rules.contains(rule_name);
324 } else if self.file_disabled_rules.contains(rule_name) {
325 return true;
326 }
327
328 if let Some(line_rules) = self.line_disabled_rules.get(&line_number)
330 && (line_rules.contains("*") || line_rules.contains(rule_name))
331 {
332 return true;
333 }
334
335 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
337 if disabled_set.contains("*") {
338 if let Some(enabled_set) = self.enabled_at_line.get(&line_number) {
340 return !enabled_set.contains(rule_name);
341 }
342 return true; } else {
344 return disabled_set.contains(rule_name);
345 }
346 }
347
348 false
349 }
350
351 pub fn get_disabled_rules(&self, line_number: usize) -> HashSet<String> {
353 let mut disabled = HashSet::new();
354
355 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
357 if disabled_set.contains("*") {
358 disabled.insert("*".to_string());
360 } else {
363 for rule in disabled_set {
364 disabled.insert(rule.clone());
365 }
366 }
367 }
368
369 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
371 for rule in line_rules {
372 disabled.insert(rule.clone());
373 }
374 }
375
376 disabled
377 }
378
379 pub fn get_rule_config(&self, rule_name: &str) -> Option<&JsonValue> {
381 self.file_rule_config.get(rule_name)
382 }
383
384 pub fn get_all_rule_configs(&self) -> &HashMap<String, JsonValue> {
386 &self.file_rule_config
387 }
388
389 pub fn export_for_file_index(&self) -> (HashSet<String>, HashMap<usize, HashSet<String>>) {
394 let file_disabled = self.file_disabled_rules.clone();
395
396 let mut line_disabled: HashMap<usize, HashSet<String>> = HashMap::new();
398
399 for (line, rules) in &self.disabled_at_line {
400 line_disabled.entry(*line).or_default().extend(rules.clone());
401 }
402 for (line, rules) in &self.line_disabled_rules {
403 line_disabled.entry(*line).or_default().extend(rules.clone());
404 }
405
406 (file_disabled, line_disabled)
407 }
408}
409
410pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
412 for prefix in &["<!-- rumdl-disable", "<!-- markdownlint-disable"] {
414 if let Some(start) = line.find(prefix) {
415 let after_prefix = &line[start + prefix.len()..];
416
417 if after_prefix.starts_with('-') {
419 continue;
420 }
421
422 if after_prefix.trim_start().starts_with("-->") {
424 return Some(Vec::new()); }
426
427 if let Some(end) = after_prefix.find("-->") {
429 let rules_str = after_prefix[..end].trim();
430 if !rules_str.is_empty() {
431 let rules: Vec<&str> = rules_str.split_whitespace().collect();
432 return Some(rules);
433 }
434 }
435 }
436 }
437
438 None
439}
440
441pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
443 for prefix in &["<!-- rumdl-enable", "<!-- markdownlint-enable"] {
445 if let Some(start) = line.find(prefix) {
446 let after_prefix = &line[start + prefix.len()..];
447
448 if after_prefix.starts_with('-') {
450 continue;
451 }
452
453 if after_prefix.trim_start().starts_with("-->") {
455 return Some(Vec::new()); }
457
458 if let Some(end) = after_prefix.find("-->") {
460 let rules_str = after_prefix[..end].trim();
461 if !rules_str.is_empty() {
462 let rules: Vec<&str> = rules_str.split_whitespace().collect();
463 return Some(rules);
464 }
465 }
466 }
467 }
468
469 None
470}
471
472pub fn parse_disable_line_comment(line: &str) -> Option<Vec<&str>> {
474 for prefix in &["<!-- rumdl-disable-line", "<!-- markdownlint-disable-line"] {
476 if let Some(start) = line.find(prefix) {
477 let after_prefix = &line[start + prefix.len()..];
478
479 if after_prefix.trim_start().starts_with("-->") {
481 return Some(Vec::new()); }
483
484 if let Some(end) = after_prefix.find("-->") {
486 let rules_str = after_prefix[..end].trim();
487 if !rules_str.is_empty() {
488 let rules: Vec<&str> = rules_str.split_whitespace().collect();
489 return Some(rules);
490 }
491 }
492 }
493 }
494
495 None
496}
497
498pub fn parse_disable_next_line_comment(line: &str) -> Option<Vec<&str>> {
500 for prefix in &["<!-- rumdl-disable-next-line", "<!-- markdownlint-disable-next-line"] {
502 if let Some(start) = line.find(prefix) {
503 let after_prefix = &line[start + prefix.len()..];
504
505 if after_prefix.trim_start().starts_with("-->") {
507 return Some(Vec::new()); }
509
510 if let Some(end) = after_prefix.find("-->") {
512 let rules_str = after_prefix[..end].trim();
513 if !rules_str.is_empty() {
514 let rules: Vec<&str> = rules_str.split_whitespace().collect();
515 return Some(rules);
516 }
517 }
518 }
519 }
520
521 None
522}
523
524pub fn is_capture_comment(line: &str) -> bool {
526 line.contains("<!-- markdownlint-capture -->") || line.contains("<!-- rumdl-capture -->")
527}
528
529pub fn is_restore_comment(line: &str) -> bool {
531 line.contains("<!-- markdownlint-restore -->") || line.contains("<!-- rumdl-restore -->")
532}
533
534pub fn parse_disable_file_comment(line: &str) -> Option<Vec<&str>> {
536 for prefix in &["<!-- rumdl-disable-file", "<!-- markdownlint-disable-file"] {
538 if let Some(start) = line.find(prefix) {
539 let after_prefix = &line[start + prefix.len()..];
540
541 if after_prefix.trim_start().starts_with("-->") {
543 return Some(Vec::new()); }
545
546 if let Some(end) = after_prefix.find("-->") {
548 let rules_str = after_prefix[..end].trim();
549 if !rules_str.is_empty() {
550 let rules: Vec<&str> = rules_str.split_whitespace().collect();
551 return Some(rules);
552 }
553 }
554 }
555 }
556
557 None
558}
559
560pub fn parse_enable_file_comment(line: &str) -> Option<Vec<&str>> {
562 for prefix in &["<!-- rumdl-enable-file", "<!-- markdownlint-enable-file"] {
564 if let Some(start) = line.find(prefix) {
565 let after_prefix = &line[start + prefix.len()..];
566
567 if after_prefix.trim_start().starts_with("-->") {
569 return Some(Vec::new()); }
571
572 if let Some(end) = after_prefix.find("-->") {
574 let rules_str = after_prefix[..end].trim();
575 if !rules_str.is_empty() {
576 let rules: Vec<&str> = rules_str.split_whitespace().collect();
577 return Some(rules);
578 }
579 }
580 }
581 }
582
583 None
584}
585
586pub fn parse_configure_file_comment(line: &str) -> Option<JsonValue> {
588 for prefix in &["<!-- rumdl-configure-file", "<!-- markdownlint-configure-file"] {
590 if let Some(start) = line.find(prefix) {
591 let after_prefix = &line[start + prefix.len()..];
592
593 if let Some(end) = after_prefix.find("-->") {
595 let json_str = after_prefix[..end].trim();
596 if !json_str.is_empty() {
597 if let Ok(value) = serde_json::from_str(json_str) {
599 return Some(value);
600 }
601 }
602 }
603 }
604 }
605
606 None
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
611pub struct InlineConfigWarning {
612 pub line_number: usize,
614 pub rule_name: String,
616 pub comment_type: String,
618 pub suggestion: Option<String>,
620}
621
622impl InlineConfigWarning {
623 pub fn format_message(&self) -> String {
625 if let Some(ref suggestion) = self.suggestion {
626 format!(
627 "Unknown rule in inline {} comment: {} (did you mean: {}?)",
628 self.comment_type, self.rule_name, suggestion
629 )
630 } else {
631 format!(
632 "Unknown rule in inline {} comment: {}",
633 self.comment_type, self.rule_name
634 )
635 }
636 }
637
638 pub fn print_warning(&self, file_path: &str) {
640 eprintln!(
641 "\x1b[33m[inline config warning]\x1b[0m {}:{}: {}",
642 file_path,
643 self.line_number,
644 self.format_message()
645 );
646 }
647}
648
649pub fn validate_inline_config_rules(content: &str) -> Vec<InlineConfigWarning> {
655 use crate::config::{RULE_ALIAS_MAP, is_valid_rule_name, suggest_similar_key};
656
657 let mut warnings = Vec::new();
658 let all_rule_names: Vec<String> = RULE_ALIAS_MAP.keys().map(|s| s.to_string()).collect();
659
660 for (idx, line) in content.lines().enumerate() {
661 let line_num = idx + 1;
662
663 let mut rule_entries: Vec<(&str, &str)> = Vec::new();
665
666 if let Some(rules) = parse_disable_comment(line) {
668 for rule in rules {
669 rule_entries.push((rule, "disable"));
670 }
671 }
672 if let Some(rules) = parse_enable_comment(line) {
673 for rule in rules {
674 rule_entries.push((rule, "enable"));
675 }
676 }
677 if let Some(rules) = parse_disable_line_comment(line) {
678 for rule in rules {
679 rule_entries.push((rule, "disable-line"));
680 }
681 }
682 if let Some(rules) = parse_disable_next_line_comment(line) {
683 for rule in rules {
684 rule_entries.push((rule, "disable-next-line"));
685 }
686 }
687 if let Some(rules) = parse_disable_file_comment(line) {
688 for rule in rules {
689 rule_entries.push((rule, "disable-file"));
690 }
691 }
692 if let Some(rules) = parse_enable_file_comment(line) {
693 for rule in rules {
694 rule_entries.push((rule, "enable-file"));
695 }
696 }
697
698 if let Some(json_config) = parse_configure_file_comment(line)
700 && let Some(obj) = json_config.as_object()
701 {
702 for rule_name in obj.keys() {
703 if !is_valid_rule_name(rule_name) {
704 let suggestion = suggest_similar_key(rule_name, &all_rule_names)
705 .map(|s| if s.starts_with("MD") { s } else { s.to_lowercase() });
706 warnings.push(InlineConfigWarning {
707 line_number: line_num,
708 rule_name: rule_name.to_string(),
709 comment_type: "configure-file".to_string(),
710 suggestion,
711 });
712 }
713 }
714 }
715
716 for (rule_name, comment_type) in rule_entries {
718 if !is_valid_rule_name(rule_name) {
719 let suggestion = suggest_similar_key(rule_name, &all_rule_names)
720 .map(|s| if s.starts_with("MD") { s } else { s.to_lowercase() });
721 warnings.push(InlineConfigWarning {
722 line_number: line_num,
723 rule_name: rule_name.to_string(),
724 comment_type: comment_type.to_string(),
725 suggestion,
726 });
727 }
728 }
729 }
730
731 warnings
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn test_parse_disable_comment() {
740 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
742 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
743
744 assert_eq!(
746 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
747 Some(vec!["MD001", "MD002"])
748 );
749
750 assert_eq!(parse_disable_comment("Some regular text"), None);
752 }
753
754 #[test]
755 fn test_parse_disable_line_comment() {
756 assert_eq!(
758 parse_disable_line_comment("<!-- markdownlint-disable-line -->"),
759 Some(vec![])
760 );
761
762 assert_eq!(
764 parse_disable_line_comment("<!-- markdownlint-disable-line MD013 -->"),
765 Some(vec!["MD013"])
766 );
767
768 assert_eq!(parse_disable_line_comment("Some regular text"), None);
770 }
771
772 #[test]
773 fn test_inline_config_from_content() {
774 let content = r#"# Test Document
775
776<!-- markdownlint-disable MD013 -->
777This is a very long line that would normally trigger MD013 but it's disabled
778
779<!-- markdownlint-enable MD013 -->
780This line will be checked again
781
782<!-- markdownlint-disable-next-line MD001 -->
783# This heading will not be checked for MD001
784## But this one will
785
786Some text <!-- markdownlint-disable-line MD013 -->
787
788<!-- markdownlint-capture -->
789<!-- markdownlint-disable MD001 MD002 -->
790# Heading with MD001 disabled
791<!-- markdownlint-restore -->
792# Heading with MD001 enabled again
793"#;
794
795 let config = InlineConfig::from_content(content);
796
797 assert!(config.is_rule_disabled("MD013", 4));
799
800 assert!(!config.is_rule_disabled("MD013", 7));
802
803 assert!(config.is_rule_disabled("MD001", 10));
805
806 assert!(!config.is_rule_disabled("MD001", 11));
808
809 assert!(config.is_rule_disabled("MD013", 13));
811
812 assert!(!config.is_rule_disabled("MD001", 19));
814 }
815
816 #[test]
817 fn test_capture_restore() {
818 let content = r#"<!-- markdownlint-disable MD001 -->
819<!-- markdownlint-capture -->
820<!-- markdownlint-disable MD002 MD003 -->
821<!-- markdownlint-restore -->
822Some content after restore
823"#;
824
825 let config = InlineConfig::from_content(content);
826
827 assert!(config.is_rule_disabled("MD001", 5));
829 assert!(!config.is_rule_disabled("MD002", 5));
830 assert!(!config.is_rule_disabled("MD003", 5));
831 }
832
833 #[test]
834 fn test_validate_inline_config_rules_unknown_rule() {
835 let content = "<!-- rumdl-disable abc -->\nSome content";
836 let warnings = validate_inline_config_rules(content);
837 assert_eq!(warnings.len(), 1);
838 assert_eq!(warnings[0].line_number, 1);
839 assert_eq!(warnings[0].rule_name, "abc");
840 assert_eq!(warnings[0].comment_type, "disable");
841 }
842
843 #[test]
844 fn test_validate_inline_config_rules_valid_rule() {
845 let content = "<!-- rumdl-disable MD001 -->\nSome content";
846 let warnings = validate_inline_config_rules(content);
847 assert!(
848 warnings.is_empty(),
849 "MD001 is a valid rule, should not produce warnings"
850 );
851 }
852
853 #[test]
854 fn test_validate_inline_config_rules_alias() {
855 let content = "<!-- rumdl-disable heading-increment -->\nSome content";
856 let warnings = validate_inline_config_rules(content);
857 assert!(warnings.is_empty(), "heading-increment is a valid alias for MD001");
858 }
859
860 #[test]
861 fn test_validate_inline_config_rules_multiple_unknown() {
862 let content = r#"<!-- rumdl-disable abc xyz -->
863<!-- rumdl-disable-line foo -->
864<!-- markdownlint-disable-next-line bar -->
865"#;
866 let warnings = validate_inline_config_rules(content);
867 assert_eq!(warnings.len(), 4);
868 assert_eq!(warnings[0].rule_name, "abc");
869 assert_eq!(warnings[1].rule_name, "xyz");
870 assert_eq!(warnings[2].rule_name, "foo");
871 assert_eq!(warnings[3].rule_name, "bar");
872 }
873
874 #[test]
875 fn test_validate_inline_config_rules_suggestion() {
876 let content = "<!-- rumdl-disable MD00 -->\n";
878 let warnings = validate_inline_config_rules(content);
879 assert_eq!(warnings.len(), 1);
880 assert!(warnings[0].suggestion.is_some());
882 }
883
884 #[test]
885 fn test_validate_inline_config_rules_file_comments() {
886 let content = "<!-- rumdl-disable-file nonexistent -->\n<!-- markdownlint-enable-file another_fake -->";
887 let warnings = validate_inline_config_rules(content);
888 assert_eq!(warnings.len(), 2);
889 assert_eq!(warnings[0].comment_type, "disable-file");
890 assert_eq!(warnings[1].comment_type, "enable-file");
891 }
892
893 #[test]
894 fn test_validate_inline_config_rules_global_disable() {
895 let content = "<!-- rumdl-disable -->\n<!-- markdownlint-enable -->";
897 let warnings = validate_inline_config_rules(content);
898 assert!(warnings.is_empty(), "Global disable/enable should not produce warnings");
899 }
900
901 #[test]
902 fn test_validate_inline_config_rules_mixed_valid_invalid() {
903 let content = "<!-- rumdl-disable MD001 abc MD003 xyz -->";
905 let warnings = validate_inline_config_rules(content);
906 assert_eq!(warnings.len(), 2);
907 assert_eq!(warnings[0].rule_name, "abc");
908 assert_eq!(warnings[1].rule_name, "xyz");
909 }
910
911 #[test]
912 fn test_validate_inline_config_rules_configure_file() {
913 let content =
915 r#"<!-- rumdl-configure-file { "MD013": { "line_length": 120 }, "nonexistent": { "foo": true } } -->"#;
916 let warnings = validate_inline_config_rules(content);
917 assert_eq!(warnings.len(), 1);
918 assert_eq!(warnings[0].rule_name, "nonexistent");
919 assert_eq!(warnings[0].comment_type, "configure-file");
920 }
921
922 #[test]
923 fn test_validate_inline_config_rules_markdownlint_variants() {
924 let content = r#"<!-- markdownlint-disable unknown_rule -->
926<!-- markdownlint-enable another_fake -->
927<!-- markdownlint-disable-line bad_rule -->
928<!-- markdownlint-disable-next-line fake_rule -->
929<!-- markdownlint-disable-file missing_rule -->
930<!-- markdownlint-enable-file nonexistent -->
931"#;
932 let warnings = validate_inline_config_rules(content);
933 assert_eq!(warnings.len(), 6);
934 assert_eq!(warnings[0].rule_name, "unknown_rule");
935 assert_eq!(warnings[1].rule_name, "another_fake");
936 assert_eq!(warnings[2].rule_name, "bad_rule");
937 assert_eq!(warnings[3].rule_name, "fake_rule");
938 assert_eq!(warnings[4].rule_name, "missing_rule");
939 assert_eq!(warnings[5].rule_name, "nonexistent");
940 }
941
942 #[test]
943 fn test_validate_inline_config_rules_markdownlint_configure_file() {
944 let content = r#"<!-- markdownlint-configure-file { "fake_rule": {} } -->"#;
945 let warnings = validate_inline_config_rules(content);
946 assert_eq!(warnings.len(), 1);
947 assert_eq!(warnings[0].rule_name, "fake_rule");
948 assert_eq!(warnings[0].comment_type, "configure-file");
949 }
950
951 #[test]
952 fn test_get_rule_config_from_configure_file() {
953 let content = r#"<!-- markdownlint-configure-file {"MD013": {"line_length": 50}} -->
954
955This is a test line."#;
956
957 let inline_config = InlineConfig::from_content(content);
958 let config_override = inline_config.get_rule_config("MD013");
959
960 assert!(config_override.is_some(), "MD013 config should be found");
961 let json = config_override.unwrap();
962 assert!(json.is_object(), "Config should be an object");
963 let obj = json.as_object().unwrap();
964 assert!(obj.contains_key("line_length"), "Should have line_length key");
965 assert_eq!(obj.get("line_length").unwrap().as_u64().unwrap(), 50);
966 }
967
968 #[test]
969 fn test_get_rule_config_tables_false() {
970 let content = r#"<!-- markdownlint-configure-file {"MD013": {"tables": false}} -->"#;
972
973 let inline_config = InlineConfig::from_content(content);
974 let config_override = inline_config.get_rule_config("MD013");
975
976 assert!(config_override.is_some(), "MD013 config should be found");
977 let json = config_override.unwrap();
978 let obj = json.as_object().unwrap();
979 assert!(obj.contains_key("tables"), "Should have tables key");
980 assert!(!obj.get("tables").unwrap().as_bool().unwrap());
981 }
982}