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
41pub type FileIndexExport = (
44 HashSet<String>,
45 Vec<(usize, HashSet<String>, HashSet<String>)>,
46 HashMap<usize, HashSet<String>>,
47);
48
49#[derive(Debug, Clone)]
53struct StateTransition {
54 line: usize,
56 disabled: HashSet<String>,
58 enabled: HashSet<String>,
60}
61
62#[derive(Debug, Clone)]
63pub struct InlineConfig {
64 transitions: Vec<StateTransition>,
67 line_disabled_rules: HashMap<usize, HashSet<String>>,
69 file_disabled_rules: HashSet<String>,
71 file_enabled_rules: HashSet<String>,
73 file_rule_config: HashMap<String, JsonValue>,
76}
77
78impl Default for InlineConfig {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl InlineConfig {
85 pub fn new() -> Self {
86 Self {
87 transitions: Vec::new(),
88 line_disabled_rules: HashMap::new(),
89 file_disabled_rules: HashSet::new(),
90 file_enabled_rules: HashSet::new(),
91 file_rule_config: HashMap::new(),
92 }
93 }
94
95 fn find_transition(&self, line_number: usize) -> Option<&StateTransition> {
98 if self.transitions.is_empty() {
99 return None;
100 }
101 match self.transitions.binary_search_by_key(&line_number, |t| t.line) {
103 Ok(idx) => Some(&self.transitions[idx]),
104 Err(idx) => {
105 if idx > 0 {
106 Some(&self.transitions[idx - 1])
107 } else {
108 None
109 }
110 }
111 }
112 }
113
114 pub fn from_content(content: &str) -> Self {
116 if !has_inline_config_markers(content) {
117 return Self::new();
118 }
119
120 let code_blocks = CodeBlockUtils::detect_code_blocks(content);
121 Self::from_content_with_code_blocks_internal(content, &code_blocks)
122 }
123
124 pub fn from_content_with_code_blocks(content: &str, code_blocks: &[(usize, usize)]) -> Self {
126 if !has_inline_config_markers(content) {
127 return Self::new();
128 }
129
130 Self::from_content_with_code_blocks_internal(content, code_blocks)
131 }
132
133 fn from_content_with_code_blocks_internal(content: &str, code_blocks: &[(usize, usize)]) -> Self {
134 let mut config = Self::new();
135 let lines: Vec<&str> = content.lines().collect();
136
137 let mut line_positions = Vec::with_capacity(lines.len());
139 let mut pos = 0;
140 for line in &lines {
141 line_positions.push(pos);
142 pos += line.len() + 1; }
144
145 let mut currently_disabled: HashSet<String> = HashSet::new();
147 let mut currently_enabled: HashSet<String> = HashSet::new();
148 let mut capture_stack: Vec<(HashSet<String>, HashSet<String>)> = Vec::new();
149
150 let mut prev_disabled: HashSet<String> = HashSet::new();
152 let mut prev_enabled: HashSet<String> = HashSet::new();
153
154 config.transitions.push(StateTransition {
156 line: 1,
157 disabled: HashSet::new(),
158 enabled: HashSet::new(),
159 });
160
161 for (idx, line) in lines.iter().enumerate() {
162 let line_num = idx + 1; if currently_disabled != prev_disabled || currently_enabled != prev_enabled {
167 config.transitions.push(StateTransition {
168 line: line_num,
169 disabled: currently_disabled.clone(),
170 enabled: currently_enabled.clone(),
171 });
172 prev_disabled.clone_from(¤tly_disabled);
173 prev_enabled.clone_from(¤tly_enabled);
174 }
175
176 let line_start = line_positions[idx];
178 let line_end = line_start + line.len();
179 let in_code_block = code_blocks
180 .iter()
181 .any(|&(block_start, block_end)| line_start >= block_start && line_end <= block_end);
182
183 if in_code_block {
184 continue;
185 }
186
187 if let Some(rules) = parse_disable_file_comment(line) {
190 if rules.is_empty() {
191 config.file_disabled_rules.clear();
193 config.file_disabled_rules.insert("*".to_string());
194 } else {
195 if config.file_disabled_rules.contains("*") {
197 for rule in rules {
199 config.file_enabled_rules.remove(&normalize_rule_name(rule));
200 }
201 } else {
202 for rule in rules {
204 config.file_disabled_rules.insert(normalize_rule_name(rule));
205 }
206 }
207 }
208 }
209
210 if let Some(rules) = parse_enable_file_comment(line) {
212 if rules.is_empty() {
213 config.file_disabled_rules.clear();
215 config.file_enabled_rules.clear();
216 } else {
217 if config.file_disabled_rules.contains("*") {
219 for rule in rules {
221 config.file_enabled_rules.insert(normalize_rule_name(rule));
222 }
223 } else {
224 for rule in rules {
226 config.file_disabled_rules.remove(&normalize_rule_name(rule));
227 }
228 }
229 }
230 }
231
232 if let Some(json_config) = parse_configure_file_comment(line) {
234 if let Some(obj) = json_config.as_object() {
236 for (rule_name, rule_config) in obj {
237 config.file_rule_config.insert(rule_name.clone(), rule_config.clone());
238 }
239 }
240 }
241
242 if let Some(rules) = parse_disable_next_line_comment(line) {
247 let next_line = line_num + 1;
248 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
249 if rules.is_empty() {
250 line_rules.insert("*".to_string());
252 } else {
253 for rule in rules {
254 line_rules.insert(normalize_rule_name(rule));
255 }
256 }
257 }
258
259 if line.contains("<!-- prettier-ignore -->") {
261 let next_line = line_num + 1;
262 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
263 line_rules.insert("*".to_string());
264 }
265
266 if let Some(rules) = parse_disable_line_comment(line) {
268 let line_rules = config.line_disabled_rules.entry(line_num).or_default();
269 if rules.is_empty() {
270 line_rules.insert("*".to_string());
272 } else {
273 for rule in rules {
274 line_rules.insert(normalize_rule_name(rule));
275 }
276 }
277 }
278
279 let mut processed_capture = false;
282 let mut processed_restore = false;
283
284 let mut comment_positions = Vec::new();
286
287 if let Some(pos) = line.find("<!-- markdownlint-disable")
288 && !line[pos..].contains("<!-- markdownlint-disable-line")
289 && !line[pos..].contains("<!-- markdownlint-disable-next-line")
290 {
291 comment_positions.push((pos, "disable"));
292 }
293 if let Some(pos) = line.find("<!-- rumdl-disable")
294 && !line[pos..].contains("<!-- rumdl-disable-line")
295 && !line[pos..].contains("<!-- rumdl-disable-next-line")
296 {
297 comment_positions.push((pos, "disable"));
298 }
299
300 if let Some(pos) = line.find("<!-- markdownlint-enable") {
301 comment_positions.push((pos, "enable"));
302 }
303 if let Some(pos) = line.find("<!-- rumdl-enable") {
304 comment_positions.push((pos, "enable"));
305 }
306
307 if let Some(pos) = line.find("<!-- markdownlint-capture") {
308 comment_positions.push((pos, "capture"));
309 }
310 if let Some(pos) = line.find("<!-- rumdl-capture") {
311 comment_positions.push((pos, "capture"));
312 }
313
314 if let Some(pos) = line.find("<!-- markdownlint-restore") {
315 comment_positions.push((pos, "restore"));
316 }
317 if let Some(pos) = line.find("<!-- rumdl-restore") {
318 comment_positions.push((pos, "restore"));
319 }
320
321 comment_positions.sort_by_key(|&(pos, _)| pos);
323
324 for (_, comment_type) in comment_positions {
326 match comment_type {
327 "disable" => {
328 if let Some(rules) = parse_disable_comment(line) {
329 if rules.is_empty() {
330 currently_disabled.clear();
332 currently_disabled.insert("*".to_string());
333 currently_enabled.clear(); } else {
335 if currently_disabled.contains("*") {
337 for rule in rules {
339 currently_enabled.remove(&normalize_rule_name(rule));
340 }
341 } else {
342 for rule in rules {
344 currently_disabled.insert(normalize_rule_name(rule));
345 }
346 }
347 }
348 }
349 }
350 "enable" => {
351 if let Some(rules) = parse_enable_comment(line) {
352 if rules.is_empty() {
353 currently_disabled.clear();
355 currently_enabled.clear();
356 } else {
357 if currently_disabled.contains("*") {
359 for rule in rules {
361 currently_enabled.insert(normalize_rule_name(rule));
362 }
363 } else {
364 for rule in rules {
366 currently_disabled.remove(&normalize_rule_name(rule));
367 }
368 }
369 }
370 }
371 }
372 "capture" => {
373 if !processed_capture && is_capture_comment(line) {
374 capture_stack.push((currently_disabled.clone(), currently_enabled.clone()));
375 processed_capture = true;
376 }
377 }
378 "restore" => {
379 if !processed_restore && is_restore_comment(line) {
380 if let Some((disabled, enabled)) = capture_stack.pop() {
381 currently_disabled = disabled;
382 currently_enabled = enabled;
383 }
384 processed_restore = true;
385 }
386 }
387 _ => {}
388 }
389 }
390 }
391
392 if currently_disabled != prev_disabled || currently_enabled != prev_enabled {
394 config.transitions.push(StateTransition {
395 line: lines.len() + 1,
396 disabled: currently_disabled,
397 enabled: currently_enabled,
398 });
399 }
400
401 config
402 }
403
404 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
406 if self.file_disabled_rules.contains("*") {
408 return !self.file_enabled_rules.contains(rule_name);
410 } else if self.file_disabled_rules.contains(rule_name) {
411 return true;
412 }
413
414 if let Some(line_rules) = self.line_disabled_rules.get(&line_number)
416 && (line_rules.contains("*") || line_rules.contains(rule_name))
417 {
418 return true;
419 }
420
421 if let Some(transition) = self.find_transition(line_number) {
423 if transition.disabled.contains("*") {
424 return !transition.enabled.contains(rule_name);
425 } else {
426 return transition.disabled.contains(rule_name);
427 }
428 }
429
430 false
431 }
432
433 pub fn get_disabled_rules(&self, line_number: usize) -> HashSet<String> {
435 let mut disabled = HashSet::new();
436
437 if let Some(transition) = self.find_transition(line_number) {
439 if transition.disabled.contains("*") {
440 disabled.insert("*".to_string());
441 } else {
442 for rule in &transition.disabled {
443 disabled.insert(rule.clone());
444 }
445 }
446 }
447
448 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
450 for rule in line_rules {
451 disabled.insert(rule.clone());
452 }
453 }
454
455 disabled
456 }
457
458 pub fn get_rule_config(&self, rule_name: &str) -> Option<&JsonValue> {
460 self.file_rule_config.get(rule_name)
461 }
462
463 pub fn get_all_rule_configs(&self) -> &HashMap<String, JsonValue> {
465 &self.file_rule_config
466 }
467
468 pub fn export_for_file_index(&self) -> FileIndexExport {
472 let file_disabled = self.file_disabled_rules.clone();
473
474 let persistent_transitions: Vec<(usize, HashSet<String>, HashSet<String>)> = self
475 .transitions
476 .iter()
477 .map(|t| (t.line, t.disabled.clone(), t.enabled.clone()))
478 .collect();
479
480 let line_disabled = self.line_disabled_rules.clone();
481
482 (file_disabled, persistent_transitions, line_disabled)
483 }
484}
485
486pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
488 for prefix in &["<!-- rumdl-disable", "<!-- markdownlint-disable"] {
490 if let Some(start) = line.find(prefix) {
491 let after_prefix = &line[start + prefix.len()..];
492
493 if after_prefix.starts_with('-') {
495 continue;
496 }
497
498 if after_prefix.trim_start().starts_with("-->") {
500 return Some(Vec::new()); }
502
503 if let Some(end) = after_prefix.find("-->") {
505 let rules_str = after_prefix[..end].trim();
506 if !rules_str.is_empty() {
507 let rules: Vec<&str> = rules_str.split_whitespace().collect();
508 return Some(rules);
509 }
510 }
511 }
512 }
513
514 None
515}
516
517pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
519 for prefix in &["<!-- rumdl-enable", "<!-- markdownlint-enable"] {
521 if let Some(start) = line.find(prefix) {
522 let after_prefix = &line[start + prefix.len()..];
523
524 if after_prefix.starts_with('-') {
526 continue;
527 }
528
529 if after_prefix.trim_start().starts_with("-->") {
531 return Some(Vec::new()); }
533
534 if let Some(end) = after_prefix.find("-->") {
536 let rules_str = after_prefix[..end].trim();
537 if !rules_str.is_empty() {
538 let rules: Vec<&str> = rules_str.split_whitespace().collect();
539 return Some(rules);
540 }
541 }
542 }
543 }
544
545 None
546}
547
548pub fn parse_disable_line_comment(line: &str) -> Option<Vec<&str>> {
550 for prefix in &["<!-- rumdl-disable-line", "<!-- markdownlint-disable-line"] {
552 if let Some(start) = line.find(prefix) {
553 let after_prefix = &line[start + prefix.len()..];
554
555 if after_prefix.trim_start().starts_with("-->") {
557 return Some(Vec::new()); }
559
560 if let Some(end) = after_prefix.find("-->") {
562 let rules_str = after_prefix[..end].trim();
563 if !rules_str.is_empty() {
564 let rules: Vec<&str> = rules_str.split_whitespace().collect();
565 return Some(rules);
566 }
567 }
568 }
569 }
570
571 None
572}
573
574pub fn parse_disable_next_line_comment(line: &str) -> Option<Vec<&str>> {
576 for prefix in &["<!-- rumdl-disable-next-line", "<!-- markdownlint-disable-next-line"] {
578 if let Some(start) = line.find(prefix) {
579 let after_prefix = &line[start + prefix.len()..];
580
581 if after_prefix.trim_start().starts_with("-->") {
583 return Some(Vec::new()); }
585
586 if let Some(end) = after_prefix.find("-->") {
588 let rules_str = after_prefix[..end].trim();
589 if !rules_str.is_empty() {
590 let rules: Vec<&str> = rules_str.split_whitespace().collect();
591 return Some(rules);
592 }
593 }
594 }
595 }
596
597 None
598}
599
600pub fn is_capture_comment(line: &str) -> bool {
602 line.contains("<!-- markdownlint-capture -->") || line.contains("<!-- rumdl-capture -->")
603}
604
605pub fn is_restore_comment(line: &str) -> bool {
607 line.contains("<!-- markdownlint-restore -->") || line.contains("<!-- rumdl-restore -->")
608}
609
610pub fn parse_disable_file_comment(line: &str) -> Option<Vec<&str>> {
612 for prefix in &["<!-- rumdl-disable-file", "<!-- markdownlint-disable-file"] {
614 if let Some(start) = line.find(prefix) {
615 let after_prefix = &line[start + prefix.len()..];
616
617 if after_prefix.trim_start().starts_with("-->") {
619 return Some(Vec::new()); }
621
622 if let Some(end) = after_prefix.find("-->") {
624 let rules_str = after_prefix[..end].trim();
625 if !rules_str.is_empty() {
626 let rules: Vec<&str> = rules_str.split_whitespace().collect();
627 return Some(rules);
628 }
629 }
630 }
631 }
632
633 None
634}
635
636pub fn parse_enable_file_comment(line: &str) -> Option<Vec<&str>> {
638 for prefix in &["<!-- rumdl-enable-file", "<!-- markdownlint-enable-file"] {
640 if let Some(start) = line.find(prefix) {
641 let after_prefix = &line[start + prefix.len()..];
642
643 if after_prefix.trim_start().starts_with("-->") {
645 return Some(Vec::new()); }
647
648 if let Some(end) = after_prefix.find("-->") {
650 let rules_str = after_prefix[..end].trim();
651 if !rules_str.is_empty() {
652 let rules: Vec<&str> = rules_str.split_whitespace().collect();
653 return Some(rules);
654 }
655 }
656 }
657 }
658
659 None
660}
661
662pub fn parse_configure_file_comment(line: &str) -> Option<JsonValue> {
664 for prefix in &["<!-- rumdl-configure-file", "<!-- markdownlint-configure-file"] {
666 if let Some(start) = line.find(prefix) {
667 let after_prefix = &line[start + prefix.len()..];
668
669 if let Some(end) = after_prefix.find("-->") {
671 let json_str = after_prefix[..end].trim();
672 if !json_str.is_empty() {
673 if let Ok(value) = serde_json::from_str(json_str) {
675 return Some(value);
676 }
677 }
678 }
679 }
680 }
681
682 None
683}
684
685#[derive(Debug, Clone, PartialEq, Eq)]
687pub struct InlineConfigWarning {
688 pub line_number: usize,
690 pub rule_name: String,
692 pub comment_type: String,
694 pub suggestion: Option<String>,
696}
697
698impl InlineConfigWarning {
699 pub fn format_message(&self) -> String {
701 if let Some(ref suggestion) = self.suggestion {
702 format!(
703 "Unknown rule in inline {} comment: {} (did you mean: {}?)",
704 self.comment_type, self.rule_name, suggestion
705 )
706 } else {
707 format!(
708 "Unknown rule in inline {} comment: {}",
709 self.comment_type, self.rule_name
710 )
711 }
712 }
713
714 pub fn print_warning(&self, file_path: &str) {
716 eprintln!(
717 "\x1b[33m[inline config warning]\x1b[0m {}:{}: {}",
718 file_path,
719 self.line_number,
720 self.format_message()
721 );
722 }
723}
724
725pub fn validate_inline_config_rules(content: &str) -> Vec<InlineConfigWarning> {
731 use crate::config::{RULE_ALIAS_MAP, is_valid_rule_name, suggest_similar_key};
732
733 let mut warnings = Vec::new();
734 let all_rule_names: Vec<String> = RULE_ALIAS_MAP.keys().map(|s| s.to_string()).collect();
735
736 for (idx, line) in content.lines().enumerate() {
737 let line_num = idx + 1;
738
739 let mut rule_entries: Vec<(&str, &str)> = Vec::new();
741
742 if let Some(rules) = parse_disable_comment(line) {
744 for rule in rules {
745 rule_entries.push((rule, "disable"));
746 }
747 }
748 if let Some(rules) = parse_enable_comment(line) {
749 for rule in rules {
750 rule_entries.push((rule, "enable"));
751 }
752 }
753 if let Some(rules) = parse_disable_line_comment(line) {
754 for rule in rules {
755 rule_entries.push((rule, "disable-line"));
756 }
757 }
758 if let Some(rules) = parse_disable_next_line_comment(line) {
759 for rule in rules {
760 rule_entries.push((rule, "disable-next-line"));
761 }
762 }
763 if let Some(rules) = parse_disable_file_comment(line) {
764 for rule in rules {
765 rule_entries.push((rule, "disable-file"));
766 }
767 }
768 if let Some(rules) = parse_enable_file_comment(line) {
769 for rule in rules {
770 rule_entries.push((rule, "enable-file"));
771 }
772 }
773
774 if let Some(json_config) = parse_configure_file_comment(line)
776 && let Some(obj) = json_config.as_object()
777 {
778 for rule_name in obj.keys() {
779 if !is_valid_rule_name(rule_name) {
780 let suggestion = suggest_similar_key(rule_name, &all_rule_names)
781 .map(|s| if s.starts_with("MD") { s } else { s.to_lowercase() });
782 warnings.push(InlineConfigWarning {
783 line_number: line_num,
784 rule_name: rule_name.to_string(),
785 comment_type: "configure-file".to_string(),
786 suggestion,
787 });
788 }
789 }
790 }
791
792 for (rule_name, comment_type) in rule_entries {
794 if !is_valid_rule_name(rule_name) {
795 let suggestion = suggest_similar_key(rule_name, &all_rule_names)
796 .map(|s| if s.starts_with("MD") { s } else { s.to_lowercase() });
797 warnings.push(InlineConfigWarning {
798 line_number: line_num,
799 rule_name: rule_name.to_string(),
800 comment_type: comment_type.to_string(),
801 suggestion,
802 });
803 }
804 }
805 }
806
807 warnings
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn test_parse_disable_comment() {
816 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
818 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
819
820 assert_eq!(
822 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
823 Some(vec!["MD001", "MD002"])
824 );
825
826 assert_eq!(parse_disable_comment("Some regular text"), None);
828 }
829
830 #[test]
831 fn test_parse_disable_line_comment() {
832 assert_eq!(
834 parse_disable_line_comment("<!-- markdownlint-disable-line -->"),
835 Some(vec![])
836 );
837
838 assert_eq!(
840 parse_disable_line_comment("<!-- markdownlint-disable-line MD013 -->"),
841 Some(vec!["MD013"])
842 );
843
844 assert_eq!(parse_disable_line_comment("Some regular text"), None);
846 }
847
848 #[test]
849 fn test_inline_config_from_content() {
850 let content = r#"# Test Document
851
852<!-- markdownlint-disable MD013 -->
853This is a very long line that would normally trigger MD013 but it's disabled
854
855<!-- markdownlint-enable MD013 -->
856This line will be checked again
857
858<!-- markdownlint-disable-next-line MD001 -->
859# This heading will not be checked for MD001
860## But this one will
861
862Some text <!-- markdownlint-disable-line MD013 -->
863
864<!-- markdownlint-capture -->
865<!-- markdownlint-disable MD001 MD002 -->
866# Heading with MD001 disabled
867<!-- markdownlint-restore -->
868# Heading with MD001 enabled again
869"#;
870
871 let config = InlineConfig::from_content(content);
872
873 assert!(config.is_rule_disabled("MD013", 4));
875
876 assert!(!config.is_rule_disabled("MD013", 7));
878
879 assert!(config.is_rule_disabled("MD001", 10));
881
882 assert!(!config.is_rule_disabled("MD001", 11));
884
885 assert!(config.is_rule_disabled("MD013", 13));
887
888 assert!(!config.is_rule_disabled("MD001", 19));
890 }
891
892 #[test]
893 fn test_capture_restore() {
894 let content = r#"<!-- markdownlint-disable MD001 -->
895<!-- markdownlint-capture -->
896<!-- markdownlint-disable MD002 MD003 -->
897<!-- markdownlint-restore -->
898Some content after restore
899"#;
900
901 let config = InlineConfig::from_content(content);
902
903 assert!(config.is_rule_disabled("MD001", 5));
905 assert!(!config.is_rule_disabled("MD002", 5));
906 assert!(!config.is_rule_disabled("MD003", 5));
907 }
908
909 #[test]
910 fn test_validate_inline_config_rules_unknown_rule() {
911 let content = "<!-- rumdl-disable abc -->\nSome content";
912 let warnings = validate_inline_config_rules(content);
913 assert_eq!(warnings.len(), 1);
914 assert_eq!(warnings[0].line_number, 1);
915 assert_eq!(warnings[0].rule_name, "abc");
916 assert_eq!(warnings[0].comment_type, "disable");
917 }
918
919 #[test]
920 fn test_validate_inline_config_rules_valid_rule() {
921 let content = "<!-- rumdl-disable MD001 -->\nSome content";
922 let warnings = validate_inline_config_rules(content);
923 assert!(
924 warnings.is_empty(),
925 "MD001 is a valid rule, should not produce warnings"
926 );
927 }
928
929 #[test]
930 fn test_validate_inline_config_rules_alias() {
931 let content = "<!-- rumdl-disable heading-increment -->\nSome content";
932 let warnings = validate_inline_config_rules(content);
933 assert!(warnings.is_empty(), "heading-increment is a valid alias for MD001");
934 }
935
936 #[test]
937 fn test_validate_inline_config_rules_multiple_unknown() {
938 let content = r#"<!-- rumdl-disable abc xyz -->
939<!-- rumdl-disable-line foo -->
940<!-- markdownlint-disable-next-line bar -->
941"#;
942 let warnings = validate_inline_config_rules(content);
943 assert_eq!(warnings.len(), 4);
944 assert_eq!(warnings[0].rule_name, "abc");
945 assert_eq!(warnings[1].rule_name, "xyz");
946 assert_eq!(warnings[2].rule_name, "foo");
947 assert_eq!(warnings[3].rule_name, "bar");
948 }
949
950 #[test]
951 fn test_validate_inline_config_rules_suggestion() {
952 let content = "<!-- rumdl-disable MD00 -->\n";
954 let warnings = validate_inline_config_rules(content);
955 assert_eq!(warnings.len(), 1);
956 assert!(warnings[0].suggestion.is_some());
958 }
959
960 #[test]
961 fn test_validate_inline_config_rules_file_comments() {
962 let content = "<!-- rumdl-disable-file nonexistent -->\n<!-- markdownlint-enable-file another_fake -->";
963 let warnings = validate_inline_config_rules(content);
964 assert_eq!(warnings.len(), 2);
965 assert_eq!(warnings[0].comment_type, "disable-file");
966 assert_eq!(warnings[1].comment_type, "enable-file");
967 }
968
969 #[test]
970 fn test_validate_inline_config_rules_global_disable() {
971 let content = "<!-- rumdl-disable -->\n<!-- markdownlint-enable -->";
973 let warnings = validate_inline_config_rules(content);
974 assert!(warnings.is_empty(), "Global disable/enable should not produce warnings");
975 }
976
977 #[test]
978 fn test_validate_inline_config_rules_mixed_valid_invalid() {
979 let content = "<!-- rumdl-disable MD001 abc MD003 xyz -->";
981 let warnings = validate_inline_config_rules(content);
982 assert_eq!(warnings.len(), 2);
983 assert_eq!(warnings[0].rule_name, "abc");
984 assert_eq!(warnings[1].rule_name, "xyz");
985 }
986
987 #[test]
988 fn test_validate_inline_config_rules_configure_file() {
989 let content =
991 r#"<!-- rumdl-configure-file { "MD013": { "line_length": 120 }, "nonexistent": { "foo": true } } -->"#;
992 let warnings = validate_inline_config_rules(content);
993 assert_eq!(warnings.len(), 1);
994 assert_eq!(warnings[0].rule_name, "nonexistent");
995 assert_eq!(warnings[0].comment_type, "configure-file");
996 }
997
998 #[test]
999 fn test_validate_inline_config_rules_markdownlint_variants() {
1000 let content = r#"<!-- markdownlint-disable unknown_rule -->
1002<!-- markdownlint-enable another_fake -->
1003<!-- markdownlint-disable-line bad_rule -->
1004<!-- markdownlint-disable-next-line fake_rule -->
1005<!-- markdownlint-disable-file missing_rule -->
1006<!-- markdownlint-enable-file nonexistent -->
1007"#;
1008 let warnings = validate_inline_config_rules(content);
1009 assert_eq!(warnings.len(), 6);
1010 assert_eq!(warnings[0].rule_name, "unknown_rule");
1011 assert_eq!(warnings[1].rule_name, "another_fake");
1012 assert_eq!(warnings[2].rule_name, "bad_rule");
1013 assert_eq!(warnings[3].rule_name, "fake_rule");
1014 assert_eq!(warnings[4].rule_name, "missing_rule");
1015 assert_eq!(warnings[5].rule_name, "nonexistent");
1016 }
1017
1018 #[test]
1019 fn test_validate_inline_config_rules_markdownlint_configure_file() {
1020 let content = r#"<!-- markdownlint-configure-file { "fake_rule": {} } -->"#;
1021 let warnings = validate_inline_config_rules(content);
1022 assert_eq!(warnings.len(), 1);
1023 assert_eq!(warnings[0].rule_name, "fake_rule");
1024 assert_eq!(warnings[0].comment_type, "configure-file");
1025 }
1026
1027 #[test]
1028 fn test_get_rule_config_from_configure_file() {
1029 let content = r#"<!-- markdownlint-configure-file {"MD013": {"line_length": 50}} -->
1030
1031This is a test line."#;
1032
1033 let inline_config = InlineConfig::from_content(content);
1034 let config_override = inline_config.get_rule_config("MD013");
1035
1036 assert!(config_override.is_some(), "MD013 config should be found");
1037 let json = config_override.unwrap();
1038 assert!(json.is_object(), "Config should be an object");
1039 let obj = json.as_object().unwrap();
1040 assert!(obj.contains_key("line_length"), "Should have line_length key");
1041 assert_eq!(obj.get("line_length").unwrap().as_u64().unwrap(), 50);
1042 }
1043
1044 #[test]
1045 fn test_get_rule_config_tables_false() {
1046 let content = r#"<!-- markdownlint-configure-file {"MD013": {"tables": false}} -->"#;
1048
1049 let inline_config = InlineConfig::from_content(content);
1050 let config_override = inline_config.get_rule_config("MD013");
1051
1052 assert!(config_override.is_some(), "MD013 config should be found");
1053 let json = config_override.unwrap();
1054 let obj = json.as_object().unwrap();
1055 assert!(obj.contains_key("tables"), "Should have tables key");
1056 assert!(!obj.get("tables").unwrap().as_bool().unwrap());
1057 }
1058}