1use crate::config::{ConfigSource, SourcedConfig, SourcedValue};
6use serde::Deserialize;
7use std::collections::HashMap;
8use std::fs;
9
10#[derive(Debug, Deserialize)]
12pub struct MarkdownlintConfig(pub HashMap<String, serde_yaml::Value>);
13
14fn strip_jsonc_comments(content: &str) -> String {
15 let mut result = String::with_capacity(content.len());
16 let mut chars = content.chars().peekable();
17 let mut in_string = false;
18 let mut escape = false;
19 let mut line_comment = false;
20 let mut block_comment = false;
21
22 while let Some(ch) = chars.next() {
23 if line_comment {
24 if ch == '\n' {
25 line_comment = false;
26 result.push('\n');
27 }
28 continue;
29 }
30
31 if block_comment {
32 if ch == '*' && matches!(chars.peek(), Some('/')) {
33 chars.next();
34 block_comment = false;
35 } else if ch == '\n' {
36 result.push('\n');
37 }
38 continue;
39 }
40
41 if in_string {
42 result.push(ch);
43 if escape {
44 escape = false;
45 } else if ch == '\\' {
46 escape = true;
47 } else if ch == '"' {
48 in_string = false;
49 }
50 continue;
51 }
52
53 if ch == '"' {
54 in_string = true;
55 result.push(ch);
56 continue;
57 }
58
59 if ch == '/' {
60 match chars.peek() {
61 Some('/') => {
62 chars.next();
63 line_comment = true;
64 continue;
65 }
66 Some('*') => {
67 chars.next();
68 block_comment = true;
69 continue;
70 }
71 _ => {}
72 }
73 }
74
75 result.push(ch);
76 }
77
78 result
79}
80
81pub fn load_markdownlint_config(path: &str) -> Result<MarkdownlintConfig, String> {
85 let content = fs::read_to_string(path).map_err(|e| format!("Failed to read config file {path}: {e}"))?;
86
87 let config: MarkdownlintConfig = if path.ends_with(".json") || path.ends_with(".jsonc") {
88 let json_content = if path.ends_with(".jsonc") {
89 strip_jsonc_comments(&content)
90 } else {
91 content.clone()
92 };
93 serde_json::from_str(&json_content).map_err(|e| format!("Failed to parse JSON: {e}"))?
94 } else if path.ends_with(".yaml") || path.ends_with(".yml") {
95 serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {e}"))?
96 } else {
97 let json_candidate = strip_jsonc_comments(&content);
98 serde_json::from_str(&json_candidate)
99 .or_else(|_| serde_yaml::from_str(&content))
100 .map_err(|e| format!("Failed to parse config as JSON or YAML: {e}"))?
101 };
102
103 Ok(unwrap_cli2_config(config))
104}
105
106fn unwrap_cli2_config(config: MarkdownlintConfig) -> MarkdownlintConfig {
110 if let Some(mapping) = config.0.get("config").and_then(|v| v.as_mapping()) {
111 let inner_map: HashMap<String, serde_yaml::Value> = mapping
112 .iter()
113 .filter_map(|(k, v)| k.as_str().map(|s| (s.to_string(), v.clone())))
114 .collect();
115 return MarkdownlintConfig(inner_map);
116 }
117 config
118}
119
120pub fn markdownlint_to_rumdl_rule_key(key: &str) -> Option<&'static str> {
124 crate::config::resolve_rule_name_alias(key)
126}
127
128fn normalize_toml_table_keys(val: toml::Value) -> toml::Value {
129 match val {
130 toml::Value::Table(table) => {
131 let mut new_table = toml::map::Map::new();
132 for (k, v) in table {
133 let norm_k = crate::config::normalize_key(&k);
134 new_table.insert(norm_k, normalize_toml_table_keys(v));
135 }
136 toml::Value::Table(new_table)
137 }
138 toml::Value::Array(arr) => toml::Value::Array(arr.into_iter().map(normalize_toml_table_keys).collect()),
139 other => other,
140 }
141}
142
143fn map_markdownlint_options_to_rumdl(
147 rule_key: &str,
148 table: toml::map::Map<String, toml::Value>,
149) -> toml::map::Map<String, toml::Value> {
150 let mut mapped = toml::map::Map::new();
151
152 match rule_key {
153 "MD013" => {
154 for (k, v) in table {
156 match k.as_str() {
157 "code-block-line-length" | "code_block_line_length" => {
160 log::warn!(
163 "Ignoring markdownlint option 'code_block_line_length' for MD013. Use 'code-blocks = false' in rumdl to disable line length checking in code blocks."
164 );
165 }
166 "heading-line-length" | "heading_line_length" => {
167 log::warn!(
169 "Ignoring markdownlint option 'heading_line_length' for MD013. Use 'headings = false' in rumdl to disable line length checking in headings."
170 );
171 }
172 "stern" => {
173 mapped.insert("strict".to_string(), v);
175 }
176 _ => {
178 mapped.insert(k, v);
179 }
180 }
181 }
182 mapped
183 }
184 "MD054" => {
185 for (k, v) in table {
188 match k.as_str() {
189 "style" | "styles" => {
190 log::warn!(
193 "Ignoring markdownlint option '{k}' for MD054. rumdl uses individual boolean flags (autolink, inline, full, collapsed, shortcut, url-inline) instead. Please configure these directly."
194 );
195 }
196 _ => {
198 mapped.insert(k, v);
199 }
200 }
201 }
202 mapped
203 }
204 _ => table,
206 }
207}
208
209impl MarkdownlintConfig {
211 pub fn map_to_sourced_rumdl_config(&self, file_path: Option<&str>) -> SourcedConfig {
213 let mut sourced_config = SourcedConfig::default();
214 let file = file_path.map(std::string::ToString::to_string);
215
216 let default_enabled = self
218 .0
219 .get("default")
220 .and_then(serde_yaml::Value::as_bool)
221 .unwrap_or(true);
222
223 let mut disabled_rules = Vec::new();
224 let mut enabled_rules = Vec::new();
225
226 for (key, value) in &self.0 {
227 if key == "default" {
229 continue;
230 }
231
232 let mapped = markdownlint_to_rumdl_rule_key(key);
233 if let Some(rumdl_key) = mapped {
234 let norm_rule_key = rumdl_key.to_ascii_uppercase();
235
236 if value.is_bool() {
238 let is_enabled = value.as_bool().unwrap_or(false);
239 if default_enabled {
240 if !is_enabled {
241 disabled_rules.push(norm_rule_key.clone());
242 }
243 } else if is_enabled {
244 enabled_rules.push(norm_rule_key.clone());
245 }
246 continue;
247 }
248
249 let toml_value: Option<toml::Value> = serde_yaml::from_value::<toml::Value>(value.clone()).ok();
250 let toml_value = toml_value.map(normalize_toml_table_keys);
251 let rule_config = sourced_config.rules.entry(norm_rule_key.clone()).or_default();
252 if let Some(tv) = toml_value {
253 if let toml::Value::Table(mut table) = tv {
254 table = map_markdownlint_options_to_rumdl(&norm_rule_key, table);
256
257 if norm_rule_key == "MD007" && !table.contains_key("style") {
259 table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
260 }
261
262 for (k, v) in table {
263 let norm_config_key = k; rule_config
265 .values
266 .entry(norm_config_key.clone())
267 .and_modify(|sv| {
268 sv.value = v.clone();
269 sv.source = ConfigSource::ProjectConfig;
270 sv.overrides.push(crate::config::ConfigOverride {
271 value: v.clone(),
272 source: ConfigSource::ProjectConfig,
273 file: file.clone(),
274 line: None,
275 });
276 })
277 .or_insert_with(|| SourcedValue {
278 value: v.clone(),
279 source: ConfigSource::ProjectConfig,
280 overrides: vec![crate::config::ConfigOverride {
281 value: v,
282 source: ConfigSource::ProjectConfig,
283 file: file.clone(),
284 line: None,
285 }],
286 });
287 }
288 } else {
289 rule_config
290 .values
291 .entry("value".to_string())
292 .and_modify(|sv| {
293 sv.value = tv.clone();
294 sv.source = ConfigSource::ProjectConfig;
295 sv.overrides.push(crate::config::ConfigOverride {
296 value: tv.clone(),
297 source: ConfigSource::ProjectConfig,
298 file: file.clone(),
299 line: None,
300 });
301 })
302 .or_insert_with(|| SourcedValue {
303 value: tv.clone(),
304 source: ConfigSource::ProjectConfig,
305 overrides: vec![crate::config::ConfigOverride {
306 value: tv,
307 source: ConfigSource::ProjectConfig,
308 file: file.clone(),
309 line: None,
310 }],
311 });
312
313 if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
315 rule_config.values.insert(
316 "style".to_string(),
317 SourcedValue {
318 value: toml::Value::String("fixed".to_string()),
319 source: ConfigSource::ProjectConfig,
320 overrides: vec![crate::config::ConfigOverride {
321 value: toml::Value::String("fixed".to_string()),
322 source: ConfigSource::ProjectConfig,
323 file: file.clone(),
324 line: None,
325 }],
326 },
327 );
328 }
329 }
330 if !default_enabled {
332 enabled_rules.push(norm_rule_key.clone());
333 }
334 } else {
335 log::error!(
336 "Could not convert value for rule key {key:?} to rumdl's internal config format. This likely means the configuration value is invalid or not supported for this rule. Please check your markdownlint config."
337 );
338 std::process::exit(1);
339 }
340 }
341 }
342
343 if !disabled_rules.is_empty() {
345 sourced_config.global.disable = SourcedValue::new(disabled_rules, ConfigSource::ProjectConfig);
346 }
347 if !enabled_rules.is_empty() || !default_enabled {
348 sourced_config.global.enable = SourcedValue::new(enabled_rules, ConfigSource::ProjectConfig);
349 }
350
351 if let Some(f) = file {
352 sourced_config.loaded_files.push(f);
353 }
354 sourced_config
355 }
356
357 pub fn map_to_sourced_rumdl_config_fragment(
359 &self,
360 file_path: Option<&str>,
361 ) -> crate::config::SourcedConfigFragment {
362 let mut fragment = crate::config::SourcedConfigFragment::default();
363 let file = file_path.map(std::string::ToString::to_string);
364
365 let default_enabled = self
369 .0
370 .get("default")
371 .and_then(serde_yaml::Value::as_bool)
372 .unwrap_or(true);
373
374 let mut disabled_rules = Vec::new();
376 let mut enabled_rules = Vec::new();
377
378 for (key, value) in &self.0 {
379 if key == "default" {
381 continue;
382 }
383
384 let mapped = markdownlint_to_rumdl_rule_key(key);
385 if let Some(rumdl_key) = mapped {
386 let norm_rule_key = rumdl_key.to_ascii_uppercase();
387
388 let display_name = if key.to_ascii_uppercase() == norm_rule_key {
391 norm_rule_key.clone()
392 } else {
393 key.to_lowercase().replace('_', "-")
394 };
395 fragment
396 .rule_display_names
397 .insert(norm_rule_key.clone(), display_name.clone());
398
399 if value.is_bool() {
401 let enabled = value.as_bool().unwrap_or(false);
402 if default_enabled {
403 if !enabled {
406 disabled_rules.push(display_name);
407 }
408 } else {
409 if enabled {
412 enabled_rules.push(display_name);
413 }
414 }
415 continue;
416 }
417 let toml_value: Option<toml::Value> = serde_yaml::from_value::<toml::Value>(value.clone()).ok();
418 let toml_value = toml_value.map(normalize_toml_table_keys);
419 let rule_config = fragment.rules.entry(norm_rule_key.clone()).or_default();
420 if let Some(tv) = toml_value {
421 let tv = if norm_rule_key == "MD013" && tv.is_integer() {
424 let mut table = toml::map::Map::new();
425 table.insert("line-length".to_string(), tv);
426 toml::Value::Table(table)
427 } else {
428 tv
429 };
430
431 if let toml::Value::Table(mut table) = tv {
432 table = map_markdownlint_options_to_rumdl(&norm_rule_key, table);
434
435 if norm_rule_key == "MD007" && !table.contains_key("style") {
437 table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
438 }
439
440 for (rk, rv) in table {
441 let norm_rk = crate::config::normalize_key(&rk);
442 let sv = rule_config.values.entry(norm_rk.clone()).or_insert_with(|| {
443 crate::config::SourcedValue::new(rv.clone(), crate::config::ConfigSource::ProjectConfig)
444 });
445 sv.push_override(rv, crate::config::ConfigSource::ProjectConfig, file.clone(), None);
446 }
447 } else {
448 rule_config
449 .values
450 .entry("value".to_string())
451 .and_modify(|sv| {
452 sv.value = tv.clone();
453 sv.source = crate::config::ConfigSource::ProjectConfig;
454 sv.overrides.push(crate::config::ConfigOverride {
455 value: tv.clone(),
456 source: crate::config::ConfigSource::ProjectConfig,
457 file: file.clone(),
458 line: None,
459 });
460 })
461 .or_insert_with(|| crate::config::SourcedValue {
462 value: tv.clone(),
463 source: crate::config::ConfigSource::ProjectConfig,
464 overrides: vec![crate::config::ConfigOverride {
465 value: tv,
466 source: crate::config::ConfigSource::ProjectConfig,
467 file: file.clone(),
468 line: None,
469 }],
470 });
471
472 if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
474 rule_config.values.insert(
475 "style".to_string(),
476 crate::config::SourcedValue {
477 value: toml::Value::String("fixed".to_string()),
478 source: crate::config::ConfigSource::ProjectConfig,
479 overrides: vec![crate::config::ConfigOverride {
480 value: toml::Value::String("fixed".to_string()),
481 source: crate::config::ConfigSource::ProjectConfig,
482 file: file.clone(),
483 line: None,
484 }],
485 },
486 );
487 }
488 }
489
490 if !default_enabled {
492 enabled_rules.push(display_name.clone());
493 }
494 }
495 }
496 }
497
498 if !disabled_rules.is_empty() {
500 fragment.global.disable.push_override(
501 disabled_rules,
502 crate::config::ConfigSource::ProjectConfig,
503 file.clone(),
504 None,
505 );
506 }
507
508 if !enabled_rules.is_empty() || !default_enabled {
513 fragment.global.enable.push_override(
514 enabled_rules,
515 crate::config::ConfigSource::ProjectConfig,
516 file.clone(),
517 None,
518 );
519 }
520
521 if let Some(_f) = file {
522 }
524 fragment
525 }
526}
527
528#[cfg(test)]
531mod tests {
532 use super::*;
533 use std::io::Write;
534 use tempfile::NamedTempFile;
535
536 #[test]
539 fn strip_jsonc_line_comment_removed() {
540 let input = r#"{ "key": 1 } // trailing comment"#;
541 assert_eq!(strip_jsonc_comments(input), r#"{ "key": 1 } "#);
542 }
543
544 #[test]
545 fn strip_jsonc_block_comment_removed() {
546 let input = r#"{ /* comment */ "key": 1 }"#;
547 assert_eq!(strip_jsonc_comments(input), r#"{ "key": 1 }"#);
548 }
549
550 #[test]
551 fn strip_jsonc_preserves_slash_slash_in_string() {
552 let input = r#"{ "url": "https://example.com" }"#;
554 assert_eq!(strip_jsonc_comments(input), input);
555 }
556
557 #[test]
558 fn strip_jsonc_preserves_block_comment_markers_in_string() {
559 let input = r#"{ "regex": "/* not a comment */" }"#;
561 assert_eq!(strip_jsonc_comments(input), input);
562 }
563
564 #[test]
565 fn strip_jsonc_slash_slash_inside_block_comment_is_ignored() {
566 let input = "{ /* // still in block */ \"k\": 1 }";
568 assert_eq!(strip_jsonc_comments(input), "{ \"k\": 1 }");
569 }
570
571 #[test]
572 fn strip_jsonc_block_comment_newlines_preserved() {
573 let input = "{\n/* line1\nline2 */\n\"k\": 1\n}";
575 let result = strip_jsonc_comments(input);
576 assert_eq!(result.lines().count(), input.lines().count());
577 }
578
579 #[test]
580 fn strip_jsonc_unterminated_block_comment_drops_to_eof() {
581 let input = r#"{ "k": 1 /* unclosed"#;
584 let result = strip_jsonc_comments(input);
585 assert!(
586 !result.contains("unclosed"),
587 "trailing content after /* should be dropped"
588 );
589 assert!(
590 result.starts_with("{ \"k\": 1 "),
591 "content before /* should be preserved"
592 );
593 }
594
595 #[test]
596 fn strip_jsonc_escaped_quote_in_string() {
597 let input = r#"{ "msg": "say \"hi\" // still string" }"#;
599 assert_eq!(strip_jsonc_comments(input), input);
600 }
601
602 #[test]
605 fn test_markdownlint_to_rumdl_rule_key() {
606 assert_eq!(markdownlint_to_rumdl_rule_key("MD001"), Some("MD001"));
608 assert_eq!(markdownlint_to_rumdl_rule_key("MD058"), Some("MD058"));
609
610 assert_eq!(markdownlint_to_rumdl_rule_key("heading-increment"), Some("MD001"));
612 assert_eq!(markdownlint_to_rumdl_rule_key("HEADING-INCREMENT"), Some("MD001"));
613 assert_eq!(markdownlint_to_rumdl_rule_key("ul-style"), Some("MD004"));
614 assert_eq!(markdownlint_to_rumdl_rule_key("no-trailing-spaces"), Some("MD009"));
615 assert_eq!(markdownlint_to_rumdl_rule_key("line-length"), Some("MD013"));
616 assert_eq!(markdownlint_to_rumdl_rule_key("single-title"), Some("MD025"));
617 assert_eq!(markdownlint_to_rumdl_rule_key("single-h1"), Some("MD025"));
618 assert_eq!(markdownlint_to_rumdl_rule_key("no-bare-urls"), Some("MD034"));
619 assert_eq!(markdownlint_to_rumdl_rule_key("code-block-style"), Some("MD046"));
620 assert_eq!(markdownlint_to_rumdl_rule_key("code-fence-style"), Some("MD048"));
621
622 assert_eq!(markdownlint_to_rumdl_rule_key("heading_increment"), Some("MD001"));
624 assert_eq!(markdownlint_to_rumdl_rule_key("HEADING_INCREMENT"), Some("MD001"));
625 assert_eq!(markdownlint_to_rumdl_rule_key("ul_style"), Some("MD004"));
626 assert_eq!(markdownlint_to_rumdl_rule_key("no_trailing_spaces"), Some("MD009"));
627 assert_eq!(markdownlint_to_rumdl_rule_key("line_length"), Some("MD013"));
628 assert_eq!(markdownlint_to_rumdl_rule_key("single_title"), Some("MD025"));
629 assert_eq!(markdownlint_to_rumdl_rule_key("single_h1"), Some("MD025"));
630 assert_eq!(markdownlint_to_rumdl_rule_key("no_bare_urls"), Some("MD034"));
631 assert_eq!(markdownlint_to_rumdl_rule_key("code_block_style"), Some("MD046"));
632 assert_eq!(markdownlint_to_rumdl_rule_key("code_fence_style"), Some("MD048"));
633
634 assert_eq!(markdownlint_to_rumdl_rule_key("md001"), Some("MD001"));
636 assert_eq!(markdownlint_to_rumdl_rule_key("Md001"), Some("MD001"));
637 assert_eq!(markdownlint_to_rumdl_rule_key("Line-Length"), Some("MD013"));
638 assert_eq!(markdownlint_to_rumdl_rule_key("Line_Length"), Some("MD013"));
639
640 assert_eq!(markdownlint_to_rumdl_rule_key("MD999"), None);
642 assert_eq!(markdownlint_to_rumdl_rule_key("invalid-rule"), None);
643 assert_eq!(markdownlint_to_rumdl_rule_key(""), None);
644 }
645
646 #[test]
647 fn test_normalize_toml_table_keys() {
648 use toml::map::Map;
649
650 let mut table = Map::new();
652 table.insert("snake_case".to_string(), toml::Value::String("value1".to_string()));
653 table.insert("kebab-case".to_string(), toml::Value::String("value2".to_string()));
654 table.insert("MD013".to_string(), toml::Value::Integer(100));
655
656 let normalized = normalize_toml_table_keys(toml::Value::Table(table));
657
658 if let toml::Value::Table(norm_table) = normalized {
659 assert!(norm_table.contains_key("snake-case"));
660 assert!(norm_table.contains_key("kebab-case"));
661 assert!(norm_table.contains_key("MD013"));
662 assert_eq!(
663 norm_table.get("snake-case").unwrap(),
664 &toml::Value::String("value1".to_string())
665 );
666 assert_eq!(
667 norm_table.get("kebab-case").unwrap(),
668 &toml::Value::String("value2".to_string())
669 );
670 } else {
671 panic!("Expected normalized value to be a table");
672 }
673
674 let array = toml::Value::Array(vec![toml::Value::String("test".to_string()), toml::Value::Integer(42)]);
676 let normalized_array = normalize_toml_table_keys(array.clone());
677 assert_eq!(normalized_array, array);
678
679 let simple = toml::Value::String("simple".to_string());
681 assert_eq!(normalize_toml_table_keys(simple.clone()), simple);
682 }
683
684 #[test]
685 fn test_load_markdownlint_config_json() {
686 let mut temp_file = NamedTempFile::new().unwrap();
687 writeln!(
688 temp_file,
689 r#"{{
690 "MD013": {{ "line_length": 100 }},
691 "MD025": true,
692 "MD026": false,
693 "heading-style": {{ "style": "atx" }}
694 }}"#
695 )
696 .unwrap();
697
698 let config = load_markdownlint_config(temp_file.path().to_str().unwrap()).unwrap();
699 assert_eq!(config.0.len(), 4);
700 assert!(config.0.contains_key("MD013"));
701 assert!(config.0.contains_key("MD025"));
702 assert!(config.0.contains_key("MD026"));
703 assert!(config.0.contains_key("heading-style"));
704 }
705
706 #[test]
707 fn test_load_markdownlint_config_yaml() {
708 let mut temp_file = NamedTempFile::new().unwrap();
709 writeln!(
710 temp_file,
711 r#"MD013:
712 line_length: 120
713MD025: true
714MD026: false
715ul-style:
716 style: dash"#
717 )
718 .unwrap();
719
720 let path = temp_file.path().with_extension("yaml");
721 std::fs::rename(temp_file.path(), &path).unwrap();
722
723 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
724 assert_eq!(config.0.len(), 4);
725 assert!(config.0.contains_key("MD013"));
726 assert!(config.0.contains_key("ul-style"));
727 }
728
729 #[test]
730 fn test_load_markdownlint_config_invalid() {
731 let mut temp_file = NamedTempFile::new().unwrap();
732 writeln!(temp_file, "invalid json/yaml content {{").unwrap();
733
734 let result = load_markdownlint_config(temp_file.path().to_str().unwrap());
735 assert!(result.is_err());
736 }
737
738 #[test]
739 fn test_load_markdownlint_config_nonexistent() {
740 let result = load_markdownlint_config("/nonexistent/file.json");
741 assert!(result.is_err());
742 assert!(result.unwrap_err().contains("Failed to read config file"));
743 }
744
745 #[test]
746 fn test_map_to_sourced_rumdl_config() {
747 let mut config_map = HashMap::new();
748 config_map.insert(
749 "MD013".to_string(),
750 serde_yaml::Value::Mapping({
751 let mut map = serde_yaml::Mapping::new();
752 map.insert(
753 serde_yaml::Value::String("line_length".to_string()),
754 serde_yaml::Value::Number(serde_yaml::Number::from(100)),
755 );
756 map
757 }),
758 );
759 config_map.insert("MD025".to_string(), serde_yaml::Value::Bool(true));
760 config_map.insert("MD026".to_string(), serde_yaml::Value::Bool(false));
761
762 let mdl_config = MarkdownlintConfig(config_map);
763 let sourced_config = mdl_config.map_to_sourced_rumdl_config(Some("test.json"));
764
765 assert!(sourced_config.rules.contains_key("MD013"));
767 let md013_config = &sourced_config.rules["MD013"];
768 assert!(md013_config.values.contains_key("line-length"));
769 assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(100));
770 assert_eq!(md013_config.values["line-length"].source, ConfigSource::ProjectConfig);
771
772 assert_eq!(sourced_config.loaded_files.len(), 1);
774 assert_eq!(sourced_config.loaded_files[0], "test.json");
775 }
776
777 #[test]
778 fn test_map_to_sourced_rumdl_config_fragment() {
779 let mut config_map = HashMap::new();
780
781 config_map.insert(
783 "line-length".to_string(),
784 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
785 );
786
787 config_map.insert("MD025".to_string(), serde_yaml::Value::Bool(false));
789
790 config_map.insert("MD026".to_string(), serde_yaml::Value::Bool(true));
792
793 config_map.insert(
795 "MD003".to_string(),
796 serde_yaml::Value::Mapping({
797 let mut map = serde_yaml::Mapping::new();
798 map.insert(
799 serde_yaml::Value::String("style".to_string()),
800 serde_yaml::Value::String("atx".to_string()),
801 );
802 map
803 }),
804 );
805
806 let mdl_config = MarkdownlintConfig(config_map);
807 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
808
809 assert!(fragment.rules.contains_key("MD013"));
811 let md013_config = &fragment.rules["MD013"];
812 assert!(md013_config.values.contains_key("line-length"));
813 assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(120));
814
815 assert!(fragment.global.disable.value.contains(&"MD025".to_string()));
817
818 assert!(
820 !fragment.global.enable.value.contains(&"MD026".to_string()),
821 "Boolean true should be no-op when default is absent (treated as true)"
822 );
823 assert!(fragment.global.enable.value.is_empty());
824
825 assert!(fragment.rules.contains_key("MD003"));
827 let md003_config = &fragment.rules["MD003"];
828 assert!(md003_config.values.contains_key("style"));
829 }
830
831 #[test]
832 fn test_edge_cases() {
833 let mut config_map = HashMap::new();
834
835 let empty_config = MarkdownlintConfig(HashMap::new());
837 let sourced = empty_config.map_to_sourced_rumdl_config(None);
838 assert!(sourced.rules.is_empty());
839
840 config_map.insert("unknown-rule".to_string(), serde_yaml::Value::Bool(true));
842 config_map.insert("MD999".to_string(), serde_yaml::Value::Bool(true));
843
844 let config = MarkdownlintConfig(config_map);
845 let sourced = config.map_to_sourced_rumdl_config(None);
846 assert!(sourced.rules.is_empty()); }
848
849 #[test]
850 fn test_complex_rule_configurations() {
851 let mut config_map = HashMap::new();
852
853 config_map.insert(
855 "MD044".to_string(),
856 serde_yaml::Value::Mapping({
857 let mut map = serde_yaml::Mapping::new();
858 map.insert(
859 serde_yaml::Value::String("names".to_string()),
860 serde_yaml::Value::Sequence(vec![
861 serde_yaml::Value::String("JavaScript".to_string()),
862 serde_yaml::Value::String("GitHub".to_string()),
863 ]),
864 );
865 map
866 }),
867 );
868
869 config_map.insert(
871 "MD003".to_string(),
872 serde_yaml::Value::Mapping({
873 let mut map = serde_yaml::Mapping::new();
874 map.insert(
875 serde_yaml::Value::String("style".to_string()),
876 serde_yaml::Value::String("atx".to_string()),
877 );
878 map
879 }),
880 );
881
882 let mdl_config = MarkdownlintConfig(config_map);
883 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
884
885 assert!(sourced.rules.contains_key("MD044"));
887 let md044_config = &sourced.rules["MD044"];
888 assert!(md044_config.values.contains_key("names"));
889
890 assert!(sourced.rules.contains_key("MD003"));
892 let md003_config = &sourced.rules["MD003"];
893 assert!(md003_config.values.contains_key("style"));
894 assert_eq!(
895 md003_config.values["style"].value,
896 toml::Value::String("atx".to_string())
897 );
898 }
899
900 #[test]
901 fn test_value_types() {
902 let mut config_map = HashMap::new();
903
904 config_map.insert(
906 "MD007".to_string(),
907 serde_yaml::Value::Number(serde_yaml::Number::from(4)),
908 ); config_map.insert(
910 "MD009".to_string(),
911 serde_yaml::Value::Mapping({
912 let mut map = serde_yaml::Mapping::new();
913 map.insert(
914 serde_yaml::Value::String("br_spaces".to_string()),
915 serde_yaml::Value::Number(serde_yaml::Number::from(2)),
916 );
917 map.insert(
918 serde_yaml::Value::String("strict".to_string()),
919 serde_yaml::Value::Bool(true),
920 );
921 map
922 }),
923 );
924
925 let mdl_config = MarkdownlintConfig(config_map);
926 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
927
928 assert!(sourced.rules.contains_key("MD007"));
930 assert!(sourced.rules["MD007"].values.contains_key("value"));
931
932 assert!(sourced.rules.contains_key("MD009"));
934 let md009_config = &sourced.rules["MD009"];
935 assert!(md009_config.values.contains_key("br-spaces"));
936 assert!(md009_config.values.contains_key("strict"));
937 }
938
939 #[test]
940 fn test_all_rule_aliases() {
941 let aliases = vec![
943 ("heading-increment", "MD001"),
944 ("heading-style", "MD003"),
945 ("ul-style", "MD004"),
946 ("list-indent", "MD005"),
947 ("ul-indent", "MD007"),
948 ("no-trailing-spaces", "MD009"),
949 ("no-hard-tabs", "MD010"),
950 ("no-reversed-links", "MD011"),
951 ("no-multiple-blanks", "MD012"),
952 ("line-length", "MD013"),
953 ("commands-show-output", "MD014"),
954 ("no-missing-space-atx", "MD018"),
956 ("no-multiple-space-atx", "MD019"),
957 ("no-missing-space-closed-atx", "MD020"),
958 ("no-multiple-space-closed-atx", "MD021"),
959 ("blanks-around-headings", "MD022"),
960 ("heading-start-left", "MD023"),
961 ("no-duplicate-heading", "MD024"),
962 ("single-title", "MD025"),
963 ("single-h1", "MD025"),
964 ("no-trailing-punctuation", "MD026"),
965 ("no-multiple-space-blockquote", "MD027"),
966 ("no-blanks-blockquote", "MD028"),
967 ("ol-prefix", "MD029"),
968 ("list-marker-space", "MD030"),
969 ("blanks-around-fences", "MD031"),
970 ("blanks-around-lists", "MD032"),
971 ("no-inline-html", "MD033"),
972 ("no-bare-urls", "MD034"),
973 ("hr-style", "MD035"),
974 ("no-emphasis-as-heading", "MD036"),
975 ("no-space-in-emphasis", "MD037"),
976 ("no-space-in-code", "MD038"),
977 ("no-space-in-links", "MD039"),
978 ("fenced-code-language", "MD040"),
979 ("first-line-heading", "MD041"),
980 ("first-line-h1", "MD041"),
981 ("no-empty-links", "MD042"),
982 ("required-headings", "MD043"),
983 ("proper-names", "MD044"),
984 ("no-alt-text", "MD045"),
985 ("code-block-style", "MD046"),
986 ("single-trailing-newline", "MD047"),
987 ("code-fence-style", "MD048"),
988 ("emphasis-style", "MD049"),
989 ("strong-style", "MD050"),
990 ("link-fragments", "MD051"),
991 ("reference-links-images", "MD052"),
992 ("link-image-reference-definitions", "MD053"),
993 ("link-image-style", "MD054"),
994 ("table-pipe-style", "MD055"),
995 ("table-column-count", "MD056"),
996 ("existing-relative-links", "MD057"),
997 ("blanks-around-tables", "MD058"),
998 ("descriptive-link-text", "MD059"),
999 ("table-cell-alignment", "MD060"),
1000 ("table-format", "MD060"),
1001 ("forbidden-terms", "MD061"),
1002 ("nested-code-fence", "MD070"),
1003 ("blank-line-after-frontmatter", "MD071"),
1004 ("frontmatter-key-sort", "MD072"),
1005 ];
1006
1007 for (alias, expected) in aliases {
1008 assert_eq!(
1009 markdownlint_to_rumdl_rule_key(alias),
1010 Some(expected),
1011 "Alias {alias} should map to {expected}"
1012 );
1013 }
1014 }
1015
1016 #[test]
1017 fn test_default_true_with_boolean_rules() {
1018 let mut config_map = HashMap::new();
1021 config_map.insert("default".to_string(), serde_yaml::Value::Bool(true));
1022 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1023 config_map.insert(
1024 "MD013".to_string(),
1025 serde_yaml::Value::Mapping({
1026 let mut map = serde_yaml::Mapping::new();
1027 map.insert(
1028 serde_yaml::Value::String("line_length".to_string()),
1029 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1030 );
1031 map
1032 }),
1033 );
1034
1035 let mdl_config = MarkdownlintConfig(config_map);
1036 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1037
1038 assert!(
1040 fragment.global.enable.value.is_empty(),
1041 "Enable list should be empty when default: true"
1042 );
1043 assert!(fragment.global.disable.value.is_empty(), "Disable list should be empty");
1045 assert!(fragment.rules.contains_key("MD013"));
1047 assert_eq!(
1048 fragment.rules["MD013"].values["line-length"].value,
1049 toml::Value::Integer(120)
1050 );
1051 }
1052
1053 #[test]
1054 fn test_default_false_with_boolean_and_config_rules() {
1055 let mut config_map = HashMap::new();
1058 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1059 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1060 config_map.insert(
1061 "MD013".to_string(),
1062 serde_yaml::Value::Mapping({
1063 let mut map = serde_yaml::Mapping::new();
1064 map.insert(
1065 serde_yaml::Value::String("line_length".to_string()),
1066 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1067 );
1068 map
1069 }),
1070 );
1071
1072 let mdl_config = MarkdownlintConfig(config_map);
1073 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1074
1075 let mut enabled_sorted = fragment.global.enable.value.clone();
1076 enabled_sorted.sort();
1077 assert_eq!(
1078 enabled_sorted,
1079 vec!["MD001", "MD013"],
1080 "Both boolean-true and config-object rules should be in enable list"
1081 );
1082 assert!(fragment.global.disable.value.is_empty(), "No rules should be disabled");
1083 assert!(fragment.rules.contains_key("MD013"));
1085 assert_eq!(
1086 fragment.rules["MD013"].values["line-length"].value,
1087 toml::Value::Integer(120)
1088 );
1089 }
1090
1091 #[test]
1092 fn test_default_absent_with_boolean_rules() {
1093 let mut config_map = HashMap::new();
1095 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1096 config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
1097
1098 let mdl_config = MarkdownlintConfig(config_map);
1099 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1100
1101 assert!(
1103 fragment.global.enable.value.is_empty(),
1104 "Enable list should be empty when default is absent"
1105 );
1106 assert_eq!(fragment.global.disable.value, vec!["MD009"]);
1108 }
1109
1110 #[test]
1111 fn test_default_false_only_booleans() {
1112 let mut config_map = HashMap::new();
1115 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1116 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1117 config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
1118
1119 let mdl_config = MarkdownlintConfig(config_map);
1120 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1121
1122 assert_eq!(fragment.global.enable.value, vec!["MD001"]);
1123 assert!(
1124 fragment.global.disable.value.is_empty(),
1125 "Disable list should be empty when default: false (false is no-op)"
1126 );
1127 }
1128
1129 #[test]
1130 fn test_default_true_with_boolean_rules_legacy() {
1131 let mut config_map = HashMap::new();
1133 config_map.insert("default".to_string(), serde_yaml::Value::Bool(true));
1134 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1135 config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
1136 config_map.insert(
1137 "MD013".to_string(),
1138 serde_yaml::Value::Mapping({
1139 let mut map = serde_yaml::Mapping::new();
1140 map.insert(
1141 serde_yaml::Value::String("line_length".to_string()),
1142 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1143 );
1144 map
1145 }),
1146 );
1147
1148 let mdl_config = MarkdownlintConfig(config_map);
1149 let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
1150
1151 assert!(sourced.global.enable.value.is_empty());
1153 assert_eq!(sourced.global.disable.value, vec!["MD009"]);
1155 assert!(sourced.rules.contains_key("MD013"));
1157 assert_eq!(
1158 sourced.rules["MD013"].values["line-length"].value,
1159 toml::Value::Integer(120)
1160 );
1161 }
1162
1163 #[test]
1164 fn test_default_false_with_config_rules_legacy() {
1165 let mut config_map = HashMap::new();
1167 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1168 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1169 config_map.insert(
1170 "MD013".to_string(),
1171 serde_yaml::Value::Mapping({
1172 let mut map = serde_yaml::Mapping::new();
1173 map.insert(
1174 serde_yaml::Value::String("line_length".to_string()),
1175 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1176 );
1177 map
1178 }),
1179 );
1180
1181 let mdl_config = MarkdownlintConfig(config_map);
1182 let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
1183
1184 let mut enabled_sorted = sourced.global.enable.value.clone();
1185 enabled_sorted.sort();
1186 assert_eq!(enabled_sorted, vec!["MD001", "MD013"]);
1187 assert!(sourced.global.disable.value.is_empty());
1188 }
1189
1190 #[test]
1191 fn test_default_false_no_rules_disables_everything() {
1192 let mut config_map = HashMap::new();
1194 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1195
1196 let mdl_config = MarkdownlintConfig(config_map);
1197 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1198
1199 assert!(fragment.global.enable.value.is_empty());
1201 assert_eq!(
1202 fragment.global.enable.source,
1203 crate::config::ConfigSource::ProjectConfig,
1204 "Enable source should be ProjectConfig when default: false"
1205 );
1206 }
1207
1208 #[test]
1209 fn test_default_false_only_false_rules_disables_everything() {
1210 let mut config_map = HashMap::new();
1212 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1213 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(false));
1214
1215 let mdl_config = MarkdownlintConfig(config_map);
1216 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1217
1218 assert!(fragment.global.enable.value.is_empty());
1219 assert_eq!(
1220 fragment.global.enable.source,
1221 crate::config::ConfigSource::ProjectConfig,
1222 );
1223 }
1224
1225 #[test]
1226 fn test_import_preserves_aliases_in_rules() {
1227 let mut config_map = HashMap::new();
1228 config_map.insert(
1229 "line-length".to_string(),
1230 serde_yaml::Value::Mapping({
1231 let mut map = serde_yaml::Mapping::new();
1232 map.insert(
1233 serde_yaml::Value::String("line_length".to_string()),
1234 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1235 );
1236 map
1237 }),
1238 );
1239 config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(false));
1240
1241 let mdl_config = MarkdownlintConfig(config_map);
1242 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1243
1244 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
1245 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
1246 }
1247
1248 #[test]
1249 fn test_import_preserves_canonical_ids() {
1250 let mut config_map = HashMap::new();
1251 config_map.insert(
1252 "MD013".to_string(),
1253 serde_yaml::Value::Mapping({
1254 let mut map = serde_yaml::Mapping::new();
1255 map.insert(
1256 serde_yaml::Value::String("line_length".to_string()),
1257 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1258 );
1259 map
1260 }),
1261 );
1262 config_map.insert("MD034".to_string(), serde_yaml::Value::Bool(false));
1263
1264 let mdl_config = MarkdownlintConfig(config_map);
1265 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1266
1267 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "MD013");
1268 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
1269 assert!(fragment.global.disable.value.contains(&"MD034".to_string()));
1270 }
1271
1272 #[test]
1273 fn test_import_mixed_aliases_and_ids() {
1274 let mut config_map = HashMap::new();
1275 config_map.insert(
1276 "line-length".to_string(),
1277 serde_yaml::Value::Mapping({
1278 let mut map = serde_yaml::Mapping::new();
1279 map.insert(
1280 serde_yaml::Value::String("line_length".to_string()),
1281 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1282 );
1283 map
1284 }),
1285 );
1286 config_map.insert("MD034".to_string(), serde_yaml::Value::Bool(false));
1287
1288 let mdl_config = MarkdownlintConfig(config_map);
1289 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1290
1291 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
1293 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
1295 }
1296
1297 #[test]
1298 fn test_import_disable_list_uses_aliases() {
1299 let mut config_map = HashMap::new();
1300 config_map.insert("line-length".to_string(), serde_yaml::Value::Bool(false));
1301 config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(false));
1302
1303 let mdl_config = MarkdownlintConfig(config_map);
1304 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1305
1306 let mut disable_sorted = fragment.global.disable.value.clone();
1307 disable_sorted.sort();
1308 assert_eq!(disable_sorted, vec!["line-length", "no-bare-urls"]);
1309 }
1310
1311 #[test]
1312 fn test_import_enable_list_uses_aliases_when_default_false() {
1313 let mut config_map = HashMap::new();
1314 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1315 config_map.insert("line-length".to_string(), serde_yaml::Value::Bool(true));
1316 config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(true));
1317
1318 let mdl_config = MarkdownlintConfig(config_map);
1319 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1320
1321 let mut enable_sorted = fragment.global.enable.value.clone();
1322 enable_sorted.sort();
1323 assert_eq!(enable_sorted, vec!["line-length", "no-bare-urls"]);
1324 }
1325
1326 #[test]
1327 fn test_import_underscore_aliases_normalized_to_kebab() {
1328 let mut config_map = HashMap::new();
1329 config_map.insert("no_bare_urls".to_string(), serde_yaml::Value::Bool(false));
1330
1331 let mdl_config = MarkdownlintConfig(config_map);
1332 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1333
1334 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
1336 assert!(fragment.global.disable.value.contains(&"no-bare-urls".to_string()));
1337 }
1338
1339 #[test]
1340 fn test_load_markdownlint_cli2_yaml_with_config_key() {
1341 let mut temp_file = NamedTempFile::new().unwrap();
1342 writeln!(
1343 temp_file,
1344 r#"config:
1345 MD013:
1346 line_length: 120
1347 MD025: true
1348 MD026: false
1349 ul-style:
1350 style: dash"#
1351 )
1352 .unwrap();
1353
1354 let path = temp_file.path().with_extension("yaml");
1355 std::fs::rename(temp_file.path(), &path).unwrap();
1356
1357 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1358 assert_eq!(config.0.len(), 4);
1359 assert!(config.0.contains_key("MD013"));
1360 assert!(config.0.contains_key("MD025"));
1361 assert!(config.0.contains_key("MD026"));
1362 assert!(config.0.contains_key("ul-style"));
1363 }
1364
1365 #[test]
1366 fn test_load_markdownlint_cli2_json_with_config_key() {
1367 let mut temp_file = NamedTempFile::new().unwrap();
1368 writeln!(
1369 temp_file,
1370 r#"{{
1371 "config": {{
1372 "MD049": {{ "style": "asterisk" }},
1373 "MD013": {{ "line_length": 100 }}
1374 }}
1375 }}"#
1376 )
1377 .unwrap();
1378
1379 let path = temp_file.path().with_extension("json");
1380 std::fs::rename(temp_file.path(), &path).unwrap();
1381
1382 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1383 assert_eq!(config.0.len(), 2);
1384 assert!(config.0.contains_key("MD049"));
1385 assert!(config.0.contains_key("MD013"));
1386 }
1387
1388 #[test]
1389 fn test_load_markdownlint_cli2_with_config_and_other_keys() {
1390 let mut temp_file = NamedTempFile::new().unwrap();
1391 writeln!(
1392 temp_file,
1393 r#"globs:
1394 - "**/*.md"
1395ignores:
1396 - "vendor/**"
1397config:
1398 MD013:
1399 line_length: 80
1400 MD049:
1401 style: underscore"#
1402 )
1403 .unwrap();
1404
1405 let path = temp_file.path().with_extension("yaml");
1406 std::fs::rename(temp_file.path(), &path).unwrap();
1407
1408 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1409 assert_eq!(config.0.len(), 2);
1411 assert!(config.0.contains_key("MD013"));
1412 assert!(config.0.contains_key("MD049"));
1413 assert!(!config.0.contains_key("globs"));
1414 assert!(!config.0.contains_key("ignores"));
1415 }
1416
1417 #[test]
1418 fn test_flat_format_still_works_with_config_as_rule() {
1419 let mut temp_file = NamedTempFile::new().unwrap();
1421 writeln!(
1422 temp_file,
1423 r#"MD013:
1424 line_length: 100
1425MD049:
1426 style: asterisk"#
1427 )
1428 .unwrap();
1429
1430 let path = temp_file.path().with_extension("yaml");
1431 std::fs::rename(temp_file.path(), &path).unwrap();
1432
1433 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1434 assert_eq!(config.0.len(), 2);
1435 assert!(config.0.contains_key("MD013"));
1436 assert!(config.0.contains_key("MD049"));
1437 }
1438
1439 #[test]
1440 fn test_load_markdownlint_cli2_empty_config_mapping() {
1441 let mut temp_file = NamedTempFile::new().unwrap();
1442 writeln!(temp_file, "config: {{}}").unwrap();
1443
1444 let path = temp_file.path().with_extension("yaml");
1445 std::fs::rename(temp_file.path(), &path).unwrap();
1446
1447 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1448 assert!(
1449 config.0.is_empty(),
1450 "Empty config: mapping should produce empty rule set"
1451 );
1452 }
1453
1454 #[test]
1455 fn test_scalar_config_key_not_treated_as_cli2_wrapper() {
1456 let mut temp_file = NamedTempFile::new().unwrap();
1458 writeln!(
1459 temp_file,
1460 r#"config: true
1461MD013:
1462 line_length: 100"#
1463 )
1464 .unwrap();
1465
1466 let path = temp_file.path().with_extension("yaml");
1467 std::fs::rename(temp_file.path(), &path).unwrap();
1468
1469 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1470 assert_eq!(config.0.len(), 2);
1472 assert!(config.0.contains_key("config"));
1473 assert!(config.0.contains_key("MD013"));
1474 }
1475
1476 #[test]
1477 fn test_import_case_insensitive_alias_preserved_lowercase() {
1478 let mut config_map = HashMap::new();
1479 config_map.insert("Line-Length".to_string(), serde_yaml::Value::Bool(false));
1480
1481 let mdl_config = MarkdownlintConfig(config_map);
1482 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1483
1484 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
1486 }
1487}