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.push_override(v.clone(), ConfigSource::ProjectConfig, file.clone());
269 })
270 .or_insert_with(|| SourcedValue {
271 value: v,
272 source: ConfigSource::ProjectConfig,
273 origin: file.clone(),
274 });
275 }
276 } else {
277 rule_config
278 .values
279 .entry("value".to_string())
280 .and_modify(|sv| {
281 sv.push_override(tv.clone(), ConfigSource::ProjectConfig, file.clone());
282 })
283 .or_insert_with(|| SourcedValue {
284 value: tv,
285 source: ConfigSource::ProjectConfig,
286 origin: file.clone(),
287 });
288
289 if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
291 rule_config.values.insert(
292 "style".to_string(),
293 SourcedValue {
294 value: toml::Value::String("fixed".to_string()),
295 source: ConfigSource::ProjectConfig,
296 origin: file.clone(),
297 },
298 );
299 }
300 }
301 if !default_enabled {
303 enabled_rules.push(norm_rule_key.clone());
304 }
305 } else {
306 log::error!(
310 "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."
311 );
312 }
313 }
314 }
315
316 if !disabled_rules.is_empty() {
318 sourced_config.global.disable = SourcedValue::new(disabled_rules, ConfigSource::ProjectConfig);
319 }
320 if !enabled_rules.is_empty() || !default_enabled {
321 sourced_config.global.enable = SourcedValue::new(enabled_rules, ConfigSource::ProjectConfig);
322 }
323
324 if let Some(f) = file {
325 sourced_config.loaded_files.push(f);
326 }
327 sourced_config
328 }
329
330 pub fn map_to_sourced_rumdl_config_fragment(
332 &self,
333 file_path: Option<&str>,
334 ) -> crate::config::SourcedConfigFragment {
335 let mut fragment = crate::config::SourcedConfigFragment::default();
336 let file = file_path.map(std::string::ToString::to_string);
337
338 let default_enabled = self
342 .0
343 .get("default")
344 .and_then(serde_yaml::Value::as_bool)
345 .unwrap_or(true);
346
347 let mut disabled_rules = Vec::new();
349 let mut enabled_rules = Vec::new();
350
351 for (key, value) in &self.0 {
352 if key == "default" {
354 continue;
355 }
356
357 let mapped = markdownlint_to_rumdl_rule_key(key);
358 if let Some(rumdl_key) = mapped {
359 let norm_rule_key = rumdl_key.to_ascii_uppercase();
360
361 let display_name = if key.to_ascii_uppercase() == norm_rule_key {
364 norm_rule_key.clone()
365 } else {
366 key.to_lowercase().replace('_', "-")
367 };
368 fragment
369 .rule_display_names
370 .insert(norm_rule_key.clone(), display_name.clone());
371
372 if value.is_bool() {
374 let enabled = value.as_bool().unwrap_or(false);
375 if default_enabled {
376 if !enabled {
379 disabled_rules.push(display_name);
380 }
381 } else {
382 if enabled {
385 enabled_rules.push(display_name);
386 }
387 }
388 continue;
389 }
390 let toml_value: Option<toml::Value> = serde_yaml::from_value::<toml::Value>(value.clone()).ok();
391 let toml_value = toml_value.map(normalize_toml_table_keys);
392 if let Some(tv) = toml_value {
393 let rule_config = fragment.rules.entry(norm_rule_key.clone()).or_default();
397 let tv = if norm_rule_key == "MD013" && tv.is_integer() {
400 let mut table = toml::map::Map::new();
401 table.insert("line-length".to_string(), tv);
402 toml::Value::Table(table)
403 } else {
404 tv
405 };
406
407 if let toml::Value::Table(mut table) = tv {
408 table = map_markdownlint_options_to_rumdl(&norm_rule_key, table);
410
411 if norm_rule_key == "MD007" && !table.contains_key("style") {
413 table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
414 }
415
416 for (rk, rv) in table {
417 let norm_rk = crate::config::normalize_key(&rk);
418 let sv = rule_config.values.entry(norm_rk.clone()).or_insert_with(|| {
419 crate::config::SourcedValue::new(rv.clone(), crate::config::ConfigSource::ProjectConfig)
420 });
421 sv.push_override(rv, crate::config::ConfigSource::ProjectConfig, file.clone());
422 }
423 } else {
424 rule_config
425 .values
426 .entry("value".to_string())
427 .and_modify(|sv| {
428 sv.push_override(tv.clone(), crate::config::ConfigSource::ProjectConfig, file.clone());
429 })
430 .or_insert_with(|| crate::config::SourcedValue {
431 value: tv,
432 source: crate::config::ConfigSource::ProjectConfig,
433 origin: file.clone(),
434 });
435
436 if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
438 rule_config.values.insert(
439 "style".to_string(),
440 crate::config::SourcedValue {
441 value: toml::Value::String("fixed".to_string()),
442 source: crate::config::ConfigSource::ProjectConfig,
443 origin: file.clone(),
444 },
445 );
446 }
447 }
448
449 if !default_enabled {
451 enabled_rules.push(display_name.clone());
452 }
453 } else {
454 log::error!(
455 "Could not convert value for rule key {key:?} to rumdl's internal config format; skipping this rule."
456 );
457 }
458 }
459 }
460
461 if !disabled_rules.is_empty() {
463 fragment.global.disable.push_override(
464 disabled_rules,
465 crate::config::ConfigSource::ProjectConfig,
466 file.clone(),
467 );
468 }
469
470 if !enabled_rules.is_empty() || !default_enabled {
475 fragment.global.enable.push_override(
476 enabled_rules,
477 crate::config::ConfigSource::ProjectConfig,
478 file.clone(),
479 );
480 }
481
482 if let Some(_f) = file {
483 }
485 fragment
486 }
487}
488
489#[cfg(test)]
492mod tests {
493 use super::*;
494 use std::io::Write;
495 use tempfile::NamedTempFile;
496
497 #[test]
500 fn strip_jsonc_line_comment_removed() {
501 let input = r#"{ "key": 1 } // trailing comment"#;
502 assert_eq!(strip_jsonc_comments(input), r#"{ "key": 1 } "#);
503 }
504
505 #[test]
506 fn strip_jsonc_block_comment_removed() {
507 let input = r#"{ /* comment */ "key": 1 }"#;
508 assert_eq!(strip_jsonc_comments(input), r#"{ "key": 1 }"#);
509 }
510
511 #[test]
512 fn strip_jsonc_preserves_slash_slash_in_string() {
513 let input = r#"{ "url": "https://example.com" }"#;
515 assert_eq!(strip_jsonc_comments(input), input);
516 }
517
518 #[test]
519 fn strip_jsonc_preserves_block_comment_markers_in_string() {
520 let input = r#"{ "regex": "/* not a comment */" }"#;
522 assert_eq!(strip_jsonc_comments(input), input);
523 }
524
525 #[test]
526 fn strip_jsonc_slash_slash_inside_block_comment_is_ignored() {
527 let input = "{ /* // still in block */ \"k\": 1 }";
529 assert_eq!(strip_jsonc_comments(input), "{ \"k\": 1 }");
530 }
531
532 #[test]
533 fn strip_jsonc_block_comment_newlines_preserved() {
534 let input = "{\n/* line1\nline2 */\n\"k\": 1\n}";
536 let result = strip_jsonc_comments(input);
537 assert_eq!(result.lines().count(), input.lines().count());
538 }
539
540 #[test]
541 fn strip_jsonc_unterminated_block_comment_drops_to_eof() {
542 let input = r#"{ "k": 1 /* unclosed"#;
545 let result = strip_jsonc_comments(input);
546 assert!(
547 !result.contains("unclosed"),
548 "trailing content after /* should be dropped"
549 );
550 assert!(
551 result.starts_with("{ \"k\": 1 "),
552 "content before /* should be preserved"
553 );
554 }
555
556 #[test]
557 fn strip_jsonc_escaped_quote_in_string() {
558 let input = r#"{ "msg": "say \"hi\" // still string" }"#;
560 assert_eq!(strip_jsonc_comments(input), input);
561 }
562
563 #[test]
566 fn test_markdownlint_to_rumdl_rule_key() {
567 assert_eq!(markdownlint_to_rumdl_rule_key("MD001"), Some("MD001"));
569 assert_eq!(markdownlint_to_rumdl_rule_key("MD058"), Some("MD058"));
570
571 assert_eq!(markdownlint_to_rumdl_rule_key("heading-increment"), Some("MD001"));
573 assert_eq!(markdownlint_to_rumdl_rule_key("HEADING-INCREMENT"), Some("MD001"));
574 assert_eq!(markdownlint_to_rumdl_rule_key("ul-style"), Some("MD004"));
575 assert_eq!(markdownlint_to_rumdl_rule_key("no-trailing-spaces"), Some("MD009"));
576 assert_eq!(markdownlint_to_rumdl_rule_key("line-length"), Some("MD013"));
577 assert_eq!(markdownlint_to_rumdl_rule_key("single-title"), Some("MD025"));
578 assert_eq!(markdownlint_to_rumdl_rule_key("single-h1"), Some("MD025"));
579 assert_eq!(markdownlint_to_rumdl_rule_key("no-bare-urls"), Some("MD034"));
580 assert_eq!(markdownlint_to_rumdl_rule_key("code-block-style"), Some("MD046"));
581 assert_eq!(markdownlint_to_rumdl_rule_key("code-fence-style"), Some("MD048"));
582
583 assert_eq!(markdownlint_to_rumdl_rule_key("heading_increment"), Some("MD001"));
585 assert_eq!(markdownlint_to_rumdl_rule_key("HEADING_INCREMENT"), Some("MD001"));
586 assert_eq!(markdownlint_to_rumdl_rule_key("ul_style"), Some("MD004"));
587 assert_eq!(markdownlint_to_rumdl_rule_key("no_trailing_spaces"), Some("MD009"));
588 assert_eq!(markdownlint_to_rumdl_rule_key("line_length"), Some("MD013"));
589 assert_eq!(markdownlint_to_rumdl_rule_key("single_title"), Some("MD025"));
590 assert_eq!(markdownlint_to_rumdl_rule_key("single_h1"), Some("MD025"));
591 assert_eq!(markdownlint_to_rumdl_rule_key("no_bare_urls"), Some("MD034"));
592 assert_eq!(markdownlint_to_rumdl_rule_key("code_block_style"), Some("MD046"));
593 assert_eq!(markdownlint_to_rumdl_rule_key("code_fence_style"), Some("MD048"));
594
595 assert_eq!(markdownlint_to_rumdl_rule_key("md001"), Some("MD001"));
597 assert_eq!(markdownlint_to_rumdl_rule_key("Md001"), Some("MD001"));
598 assert_eq!(markdownlint_to_rumdl_rule_key("Line-Length"), Some("MD013"));
599 assert_eq!(markdownlint_to_rumdl_rule_key("Line_Length"), Some("MD013"));
600
601 assert_eq!(markdownlint_to_rumdl_rule_key("MD999"), None);
603 assert_eq!(markdownlint_to_rumdl_rule_key("invalid-rule"), None);
604 assert_eq!(markdownlint_to_rumdl_rule_key(""), None);
605 }
606
607 #[test]
608 fn test_normalize_toml_table_keys() {
609 use toml::map::Map;
610
611 let mut table = Map::new();
613 table.insert("snake_case".to_string(), toml::Value::String("value1".to_string()));
614 table.insert("kebab-case".to_string(), toml::Value::String("value2".to_string()));
615 table.insert("MD013".to_string(), toml::Value::Integer(100));
616
617 let normalized = normalize_toml_table_keys(toml::Value::Table(table));
618
619 if let toml::Value::Table(norm_table) = normalized {
620 assert!(norm_table.contains_key("snake-case"));
621 assert!(norm_table.contains_key("kebab-case"));
622 assert!(norm_table.contains_key("MD013"));
623 assert_eq!(
624 norm_table.get("snake-case").unwrap(),
625 &toml::Value::String("value1".to_string())
626 );
627 assert_eq!(
628 norm_table.get("kebab-case").unwrap(),
629 &toml::Value::String("value2".to_string())
630 );
631 } else {
632 panic!("Expected normalized value to be a table");
633 }
634
635 let array = toml::Value::Array(vec![toml::Value::String("test".to_string()), toml::Value::Integer(42)]);
637 let normalized_array = normalize_toml_table_keys(array.clone());
638 assert_eq!(normalized_array, array);
639
640 let simple = toml::Value::String("simple".to_string());
642 assert_eq!(normalize_toml_table_keys(simple.clone()), simple);
643 }
644
645 #[test]
646 fn test_load_markdownlint_config_json() {
647 let mut temp_file = NamedTempFile::new().unwrap();
648 writeln!(
649 temp_file,
650 r#"{{
651 "MD013": {{ "line_length": 100 }},
652 "MD025": true,
653 "MD026": false,
654 "heading-style": {{ "style": "atx" }}
655 }}"#
656 )
657 .unwrap();
658
659 let config = load_markdownlint_config(temp_file.path().to_str().unwrap()).unwrap();
660 assert_eq!(config.0.len(), 4);
661 assert!(config.0.contains_key("MD013"));
662 assert!(config.0.contains_key("MD025"));
663 assert!(config.0.contains_key("MD026"));
664 assert!(config.0.contains_key("heading-style"));
665 }
666
667 #[test]
668 fn test_load_markdownlint_config_yaml() {
669 let mut temp_file = NamedTempFile::new().unwrap();
670 writeln!(
671 temp_file,
672 r#"MD013:
673 line_length: 120
674MD025: true
675MD026: false
676ul-style:
677 style: dash"#
678 )
679 .unwrap();
680
681 let path = temp_file.path().with_extension("yaml");
682 std::fs::rename(temp_file.path(), &path).unwrap();
683
684 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
685 assert_eq!(config.0.len(), 4);
686 assert!(config.0.contains_key("MD013"));
687 assert!(config.0.contains_key("ul-style"));
688 }
689
690 #[test]
691 fn test_load_markdownlint_config_invalid() {
692 let mut temp_file = NamedTempFile::new().unwrap();
693 writeln!(temp_file, "invalid json/yaml content {{").unwrap();
694
695 let result = load_markdownlint_config(temp_file.path().to_str().unwrap());
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn test_load_markdownlint_config_nonexistent() {
701 let result = load_markdownlint_config("/nonexistent/file.json");
702 assert!(result.is_err());
703 assert!(result.unwrap_err().contains("Failed to read config file"));
704 }
705
706 #[test]
707 fn test_map_to_sourced_rumdl_config() {
708 let mut config_map = HashMap::new();
709 config_map.insert(
710 "MD013".to_string(),
711 serde_yaml::Value::Mapping({
712 let mut map = serde_yaml::Mapping::new();
713 map.insert(
714 serde_yaml::Value::String("line_length".to_string()),
715 serde_yaml::Value::Number(serde_yaml::Number::from(100)),
716 );
717 map
718 }),
719 );
720 config_map.insert("MD025".to_string(), serde_yaml::Value::Bool(true));
721 config_map.insert("MD026".to_string(), serde_yaml::Value::Bool(false));
722
723 let mdl_config = MarkdownlintConfig(config_map);
724 let sourced_config = mdl_config.map_to_sourced_rumdl_config(Some("test.json"));
725
726 assert!(sourced_config.rules.contains_key("MD013"));
728 let md013_config = &sourced_config.rules["MD013"];
729 assert!(md013_config.values.contains_key("line-length"));
730 assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(100));
731 assert_eq!(md013_config.values["line-length"].source, ConfigSource::ProjectConfig);
732
733 assert_eq!(sourced_config.loaded_files.len(), 1);
735 assert_eq!(sourced_config.loaded_files[0], "test.json");
736 }
737
738 #[test]
739 fn test_fragment_skips_unconvertible_value_cleanly() {
740 let mut config_map = HashMap::new();
741 config_map.insert("MD013".to_string(), serde_yaml::Value::Null);
744
745 let mdl_config = MarkdownlintConfig(config_map);
746 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(None);
747
748 assert!(
751 !fragment.rules.contains_key("MD013"),
752 "unconvertible MD013 value should not create a rules entry, got {:?}",
753 fragment.rules.get("MD013")
754 );
755 }
756
757 #[test]
758 fn test_legacy_map_skips_unconvertible_value_without_exiting() {
759 let mut config_map = HashMap::new();
760 config_map.insert("MD013".to_string(), serde_yaml::Value::Null);
761
762 let mdl_config = MarkdownlintConfig(config_map);
763 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
766 assert!(sourced.rules.get("MD013").is_none_or(|r| r.values.is_empty()));
767 }
768
769 #[test]
770 fn test_map_to_sourced_rumdl_config_fragment() {
771 let mut config_map = HashMap::new();
772
773 config_map.insert(
775 "line-length".to_string(),
776 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
777 );
778
779 config_map.insert("MD025".to_string(), serde_yaml::Value::Bool(false));
781
782 config_map.insert("MD026".to_string(), serde_yaml::Value::Bool(true));
784
785 config_map.insert(
787 "MD003".to_string(),
788 serde_yaml::Value::Mapping({
789 let mut map = serde_yaml::Mapping::new();
790 map.insert(
791 serde_yaml::Value::String("style".to_string()),
792 serde_yaml::Value::String("atx".to_string()),
793 );
794 map
795 }),
796 );
797
798 let mdl_config = MarkdownlintConfig(config_map);
799 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
800
801 assert!(fragment.rules.contains_key("MD013"));
803 let md013_config = &fragment.rules["MD013"];
804 assert!(md013_config.values.contains_key("line-length"));
805 assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(120));
806
807 assert!(fragment.global.disable.value.contains(&"MD025".to_string()));
809
810 assert!(
812 !fragment.global.enable.value.contains(&"MD026".to_string()),
813 "Boolean true should be no-op when default is absent (treated as true)"
814 );
815 assert!(fragment.global.enable.value.is_empty());
816
817 assert!(fragment.rules.contains_key("MD003"));
819 let md003_config = &fragment.rules["MD003"];
820 assert!(md003_config.values.contains_key("style"));
821 }
822
823 #[test]
824 fn test_edge_cases() {
825 let mut config_map = HashMap::new();
826
827 let empty_config = MarkdownlintConfig(HashMap::new());
829 let sourced = empty_config.map_to_sourced_rumdl_config(None);
830 assert!(sourced.rules.is_empty());
831
832 config_map.insert("unknown-rule".to_string(), serde_yaml::Value::Bool(true));
834 config_map.insert("MD999".to_string(), serde_yaml::Value::Bool(true));
835
836 let config = MarkdownlintConfig(config_map);
837 let sourced = config.map_to_sourced_rumdl_config(None);
838 assert!(sourced.rules.is_empty()); }
840
841 #[test]
842 fn test_complex_rule_configurations() {
843 let mut config_map = HashMap::new();
844
845 config_map.insert(
847 "MD044".to_string(),
848 serde_yaml::Value::Mapping({
849 let mut map = serde_yaml::Mapping::new();
850 map.insert(
851 serde_yaml::Value::String("names".to_string()),
852 serde_yaml::Value::Sequence(vec![
853 serde_yaml::Value::String("JavaScript".to_string()),
854 serde_yaml::Value::String("GitHub".to_string()),
855 ]),
856 );
857 map
858 }),
859 );
860
861 config_map.insert(
863 "MD003".to_string(),
864 serde_yaml::Value::Mapping({
865 let mut map = serde_yaml::Mapping::new();
866 map.insert(
867 serde_yaml::Value::String("style".to_string()),
868 serde_yaml::Value::String("atx".to_string()),
869 );
870 map
871 }),
872 );
873
874 let mdl_config = MarkdownlintConfig(config_map);
875 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
876
877 assert!(sourced.rules.contains_key("MD044"));
879 let md044_config = &sourced.rules["MD044"];
880 assert!(md044_config.values.contains_key("names"));
881
882 assert!(sourced.rules.contains_key("MD003"));
884 let md003_config = &sourced.rules["MD003"];
885 assert!(md003_config.values.contains_key("style"));
886 assert_eq!(
887 md003_config.values["style"].value,
888 toml::Value::String("atx".to_string())
889 );
890 }
891
892 #[test]
893 fn test_value_types() {
894 let mut config_map = HashMap::new();
895
896 config_map.insert(
898 "MD007".to_string(),
899 serde_yaml::Value::Number(serde_yaml::Number::from(4)),
900 ); config_map.insert(
902 "MD009".to_string(),
903 serde_yaml::Value::Mapping({
904 let mut map = serde_yaml::Mapping::new();
905 map.insert(
906 serde_yaml::Value::String("br_spaces".to_string()),
907 serde_yaml::Value::Number(serde_yaml::Number::from(2)),
908 );
909 map.insert(
910 serde_yaml::Value::String("strict".to_string()),
911 serde_yaml::Value::Bool(true),
912 );
913 map
914 }),
915 );
916
917 let mdl_config = MarkdownlintConfig(config_map);
918 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
919
920 assert!(sourced.rules.contains_key("MD007"));
922 assert!(sourced.rules["MD007"].values.contains_key("value"));
923
924 assert!(sourced.rules.contains_key("MD009"));
926 let md009_config = &sourced.rules["MD009"];
927 assert!(md009_config.values.contains_key("br-spaces"));
928 assert!(md009_config.values.contains_key("strict"));
929 }
930
931 #[test]
932 fn test_all_rule_aliases() {
933 let aliases = vec![
935 ("heading-increment", "MD001"),
936 ("heading-style", "MD003"),
937 ("ul-style", "MD004"),
938 ("list-indent", "MD005"),
939 ("ul-indent", "MD007"),
940 ("no-trailing-spaces", "MD009"),
941 ("no-hard-tabs", "MD010"),
942 ("no-reversed-links", "MD011"),
943 ("no-multiple-blanks", "MD012"),
944 ("line-length", "MD013"),
945 ("commands-show-output", "MD014"),
946 ("no-missing-space-atx", "MD018"),
948 ("no-multiple-space-atx", "MD019"),
949 ("no-missing-space-closed-atx", "MD020"),
950 ("no-multiple-space-closed-atx", "MD021"),
951 ("blanks-around-headings", "MD022"),
952 ("heading-start-left", "MD023"),
953 ("no-duplicate-heading", "MD024"),
954 ("single-title", "MD025"),
955 ("single-h1", "MD025"),
956 ("no-trailing-punctuation", "MD026"),
957 ("no-multiple-space-blockquote", "MD027"),
958 ("no-blanks-blockquote", "MD028"),
959 ("ol-prefix", "MD029"),
960 ("list-marker-space", "MD030"),
961 ("blanks-around-fences", "MD031"),
962 ("blanks-around-lists", "MD032"),
963 ("no-inline-html", "MD033"),
964 ("no-bare-urls", "MD034"),
965 ("hr-style", "MD035"),
966 ("no-emphasis-as-heading", "MD036"),
967 ("no-space-in-emphasis", "MD037"),
968 ("no-space-in-code", "MD038"),
969 ("no-space-in-links", "MD039"),
970 ("fenced-code-language", "MD040"),
971 ("first-line-heading", "MD041"),
972 ("first-line-h1", "MD041"),
973 ("no-empty-links", "MD042"),
974 ("required-headings", "MD043"),
975 ("proper-names", "MD044"),
976 ("no-alt-text", "MD045"),
977 ("code-block-style", "MD046"),
978 ("single-trailing-newline", "MD047"),
979 ("code-fence-style", "MD048"),
980 ("emphasis-style", "MD049"),
981 ("strong-style", "MD050"),
982 ("link-fragments", "MD051"),
983 ("reference-links-images", "MD052"),
984 ("link-image-reference-definitions", "MD053"),
985 ("link-image-style", "MD054"),
986 ("table-pipe-style", "MD055"),
987 ("table-column-count", "MD056"),
988 ("existing-relative-links", "MD057"),
989 ("blanks-around-tables", "MD058"),
990 ("descriptive-link-text", "MD059"),
991 ("table-cell-alignment", "MD060"),
992 ("table-format", "MD060"),
993 ("forbidden-terms", "MD061"),
994 ("nested-code-fence", "MD070"),
995 ("blank-line-after-frontmatter", "MD071"),
996 ("frontmatter-key-sort", "MD072"),
997 ];
998
999 for (alias, expected) in aliases {
1000 assert_eq!(
1001 markdownlint_to_rumdl_rule_key(alias),
1002 Some(expected),
1003 "Alias {alias} should map to {expected}"
1004 );
1005 }
1006 }
1007
1008 #[test]
1009 fn test_default_true_with_boolean_rules() {
1010 let mut config_map = HashMap::new();
1013 config_map.insert("default".to_string(), serde_yaml::Value::Bool(true));
1014 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1015 config_map.insert(
1016 "MD013".to_string(),
1017 serde_yaml::Value::Mapping({
1018 let mut map = serde_yaml::Mapping::new();
1019 map.insert(
1020 serde_yaml::Value::String("line_length".to_string()),
1021 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1022 );
1023 map
1024 }),
1025 );
1026
1027 let mdl_config = MarkdownlintConfig(config_map);
1028 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1029
1030 assert!(
1032 fragment.global.enable.value.is_empty(),
1033 "Enable list should be empty when default: true"
1034 );
1035 assert!(fragment.global.disable.value.is_empty(), "Disable list should be empty");
1037 assert!(fragment.rules.contains_key("MD013"));
1039 assert_eq!(
1040 fragment.rules["MD013"].values["line-length"].value,
1041 toml::Value::Integer(120)
1042 );
1043 }
1044
1045 #[test]
1046 fn test_default_false_with_boolean_and_config_rules() {
1047 let mut config_map = HashMap::new();
1050 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1051 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1052 config_map.insert(
1053 "MD013".to_string(),
1054 serde_yaml::Value::Mapping({
1055 let mut map = serde_yaml::Mapping::new();
1056 map.insert(
1057 serde_yaml::Value::String("line_length".to_string()),
1058 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1059 );
1060 map
1061 }),
1062 );
1063
1064 let mdl_config = MarkdownlintConfig(config_map);
1065 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1066
1067 let mut enabled_sorted = fragment.global.enable.value.clone();
1068 enabled_sorted.sort();
1069 assert_eq!(
1070 enabled_sorted,
1071 vec!["MD001", "MD013"],
1072 "Both boolean-true and config-object rules should be in enable list"
1073 );
1074 assert!(fragment.global.disable.value.is_empty(), "No rules should be disabled");
1075 assert!(fragment.rules.contains_key("MD013"));
1077 assert_eq!(
1078 fragment.rules["MD013"].values["line-length"].value,
1079 toml::Value::Integer(120)
1080 );
1081 }
1082
1083 #[test]
1084 fn test_default_absent_with_boolean_rules() {
1085 let mut config_map = HashMap::new();
1087 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1088 config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
1089
1090 let mdl_config = MarkdownlintConfig(config_map);
1091 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1092
1093 assert!(
1095 fragment.global.enable.value.is_empty(),
1096 "Enable list should be empty when default is absent"
1097 );
1098 assert_eq!(fragment.global.disable.value, vec!["MD009"]);
1100 }
1101
1102 #[test]
1103 fn test_default_false_only_booleans() {
1104 let mut config_map = HashMap::new();
1107 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1108 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1109 config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
1110
1111 let mdl_config = MarkdownlintConfig(config_map);
1112 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1113
1114 assert_eq!(fragment.global.enable.value, vec!["MD001"]);
1115 assert!(
1116 fragment.global.disable.value.is_empty(),
1117 "Disable list should be empty when default: false (false is no-op)"
1118 );
1119 }
1120
1121 #[test]
1122 fn test_default_true_with_boolean_rules_legacy() {
1123 let mut config_map = HashMap::new();
1125 config_map.insert("default".to_string(), serde_yaml::Value::Bool(true));
1126 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1127 config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
1128 config_map.insert(
1129 "MD013".to_string(),
1130 serde_yaml::Value::Mapping({
1131 let mut map = serde_yaml::Mapping::new();
1132 map.insert(
1133 serde_yaml::Value::String("line_length".to_string()),
1134 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1135 );
1136 map
1137 }),
1138 );
1139
1140 let mdl_config = MarkdownlintConfig(config_map);
1141 let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
1142
1143 assert!(sourced.global.enable.value.is_empty());
1145 assert_eq!(sourced.global.disable.value, vec!["MD009"]);
1147 assert!(sourced.rules.contains_key("MD013"));
1149 assert_eq!(
1150 sourced.rules["MD013"].values["line-length"].value,
1151 toml::Value::Integer(120)
1152 );
1153 }
1154
1155 #[test]
1156 fn test_default_false_with_config_rules_legacy() {
1157 let mut config_map = HashMap::new();
1159 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1160 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
1161 config_map.insert(
1162 "MD013".to_string(),
1163 serde_yaml::Value::Mapping({
1164 let mut map = serde_yaml::Mapping::new();
1165 map.insert(
1166 serde_yaml::Value::String("line_length".to_string()),
1167 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1168 );
1169 map
1170 }),
1171 );
1172
1173 let mdl_config = MarkdownlintConfig(config_map);
1174 let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
1175
1176 let mut enabled_sorted = sourced.global.enable.value.clone();
1177 enabled_sorted.sort();
1178 assert_eq!(enabled_sorted, vec!["MD001", "MD013"]);
1179 assert!(sourced.global.disable.value.is_empty());
1180 }
1181
1182 #[test]
1183 fn test_default_false_no_rules_disables_everything() {
1184 let mut config_map = HashMap::new();
1186 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1187
1188 let mdl_config = MarkdownlintConfig(config_map);
1189 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1190
1191 assert!(fragment.global.enable.value.is_empty());
1193 assert_eq!(
1194 fragment.global.enable.source,
1195 crate::config::ConfigSource::ProjectConfig,
1196 "Enable source should be ProjectConfig when default: false"
1197 );
1198 }
1199
1200 #[test]
1201 fn test_default_false_only_false_rules_disables_everything() {
1202 let mut config_map = HashMap::new();
1204 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1205 config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(false));
1206
1207 let mdl_config = MarkdownlintConfig(config_map);
1208 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1209
1210 assert!(fragment.global.enable.value.is_empty());
1211 assert_eq!(
1212 fragment.global.enable.source,
1213 crate::config::ConfigSource::ProjectConfig,
1214 );
1215 }
1216
1217 #[test]
1218 fn test_import_preserves_aliases_in_rules() {
1219 let mut config_map = HashMap::new();
1220 config_map.insert(
1221 "line-length".to_string(),
1222 serde_yaml::Value::Mapping({
1223 let mut map = serde_yaml::Mapping::new();
1224 map.insert(
1225 serde_yaml::Value::String("line_length".to_string()),
1226 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1227 );
1228 map
1229 }),
1230 );
1231 config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(false));
1232
1233 let mdl_config = MarkdownlintConfig(config_map);
1234 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1235
1236 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
1237 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
1238 }
1239
1240 #[test]
1241 fn test_import_preserves_canonical_ids() {
1242 let mut config_map = HashMap::new();
1243 config_map.insert(
1244 "MD013".to_string(),
1245 serde_yaml::Value::Mapping({
1246 let mut map = serde_yaml::Mapping::new();
1247 map.insert(
1248 serde_yaml::Value::String("line_length".to_string()),
1249 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1250 );
1251 map
1252 }),
1253 );
1254 config_map.insert("MD034".to_string(), serde_yaml::Value::Bool(false));
1255
1256 let mdl_config = MarkdownlintConfig(config_map);
1257 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1258
1259 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "MD013");
1260 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
1261 assert!(fragment.global.disable.value.contains(&"MD034".to_string()));
1262 }
1263
1264 #[test]
1265 fn test_import_mixed_aliases_and_ids() {
1266 let mut config_map = HashMap::new();
1267 config_map.insert(
1268 "line-length".to_string(),
1269 serde_yaml::Value::Mapping({
1270 let mut map = serde_yaml::Mapping::new();
1271 map.insert(
1272 serde_yaml::Value::String("line_length".to_string()),
1273 serde_yaml::Value::Number(serde_yaml::Number::from(120)),
1274 );
1275 map
1276 }),
1277 );
1278 config_map.insert("MD034".to_string(), serde_yaml::Value::Bool(false));
1279
1280 let mdl_config = MarkdownlintConfig(config_map);
1281 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1282
1283 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
1285 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
1287 }
1288
1289 #[test]
1290 fn test_import_disable_list_uses_aliases() {
1291 let mut config_map = HashMap::new();
1292 config_map.insert("line-length".to_string(), serde_yaml::Value::Bool(false));
1293 config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(false));
1294
1295 let mdl_config = MarkdownlintConfig(config_map);
1296 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1297
1298 let mut disable_sorted = fragment.global.disable.value.clone();
1299 disable_sorted.sort();
1300 assert_eq!(disable_sorted, vec!["line-length", "no-bare-urls"]);
1301 }
1302
1303 #[test]
1304 fn test_import_enable_list_uses_aliases_when_default_false() {
1305 let mut config_map = HashMap::new();
1306 config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
1307 config_map.insert("line-length".to_string(), serde_yaml::Value::Bool(true));
1308 config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(true));
1309
1310 let mdl_config = MarkdownlintConfig(config_map);
1311 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1312
1313 let mut enable_sorted = fragment.global.enable.value.clone();
1314 enable_sorted.sort();
1315 assert_eq!(enable_sorted, vec!["line-length", "no-bare-urls"]);
1316 }
1317
1318 #[test]
1319 fn test_import_underscore_aliases_normalized_to_kebab() {
1320 let mut config_map = HashMap::new();
1321 config_map.insert("no_bare_urls".to_string(), serde_yaml::Value::Bool(false));
1322
1323 let mdl_config = MarkdownlintConfig(config_map);
1324 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1325
1326 assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
1328 assert!(fragment.global.disable.value.contains(&"no-bare-urls".to_string()));
1329 }
1330
1331 #[test]
1332 fn test_load_markdownlint_cli2_yaml_with_config_key() {
1333 let mut temp_file = NamedTempFile::new().unwrap();
1334 writeln!(
1335 temp_file,
1336 r#"config:
1337 MD013:
1338 line_length: 120
1339 MD025: true
1340 MD026: false
1341 ul-style:
1342 style: dash"#
1343 )
1344 .unwrap();
1345
1346 let path = temp_file.path().with_extension("yaml");
1347 std::fs::rename(temp_file.path(), &path).unwrap();
1348
1349 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1350 assert_eq!(config.0.len(), 4);
1351 assert!(config.0.contains_key("MD013"));
1352 assert!(config.0.contains_key("MD025"));
1353 assert!(config.0.contains_key("MD026"));
1354 assert!(config.0.contains_key("ul-style"));
1355 }
1356
1357 #[test]
1358 fn test_load_markdownlint_cli2_json_with_config_key() {
1359 let mut temp_file = NamedTempFile::new().unwrap();
1360 writeln!(
1361 temp_file,
1362 r#"{{
1363 "config": {{
1364 "MD049": {{ "style": "asterisk" }},
1365 "MD013": {{ "line_length": 100 }}
1366 }}
1367 }}"#
1368 )
1369 .unwrap();
1370
1371 let path = temp_file.path().with_extension("json");
1372 std::fs::rename(temp_file.path(), &path).unwrap();
1373
1374 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1375 assert_eq!(config.0.len(), 2);
1376 assert!(config.0.contains_key("MD049"));
1377 assert!(config.0.contains_key("MD013"));
1378 }
1379
1380 #[test]
1381 fn test_load_markdownlint_cli2_with_config_and_other_keys() {
1382 let mut temp_file = NamedTempFile::new().unwrap();
1383 writeln!(
1384 temp_file,
1385 r#"globs:
1386 - "**/*.md"
1387ignores:
1388 - "vendor/**"
1389config:
1390 MD013:
1391 line_length: 80
1392 MD049:
1393 style: underscore"#
1394 )
1395 .unwrap();
1396
1397 let path = temp_file.path().with_extension("yaml");
1398 std::fs::rename(temp_file.path(), &path).unwrap();
1399
1400 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1401 assert_eq!(config.0.len(), 2);
1403 assert!(config.0.contains_key("MD013"));
1404 assert!(config.0.contains_key("MD049"));
1405 assert!(!config.0.contains_key("globs"));
1406 assert!(!config.0.contains_key("ignores"));
1407 }
1408
1409 #[test]
1410 fn test_flat_format_still_works_with_config_as_rule() {
1411 let mut temp_file = NamedTempFile::new().unwrap();
1413 writeln!(
1414 temp_file,
1415 r#"MD013:
1416 line_length: 100
1417MD049:
1418 style: asterisk"#
1419 )
1420 .unwrap();
1421
1422 let path = temp_file.path().with_extension("yaml");
1423 std::fs::rename(temp_file.path(), &path).unwrap();
1424
1425 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1426 assert_eq!(config.0.len(), 2);
1427 assert!(config.0.contains_key("MD013"));
1428 assert!(config.0.contains_key("MD049"));
1429 }
1430
1431 #[test]
1432 fn test_load_markdownlint_cli2_empty_config_mapping() {
1433 let mut temp_file = NamedTempFile::new().unwrap();
1434 writeln!(temp_file, "config: {{}}").unwrap();
1435
1436 let path = temp_file.path().with_extension("yaml");
1437 std::fs::rename(temp_file.path(), &path).unwrap();
1438
1439 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1440 assert!(
1441 config.0.is_empty(),
1442 "Empty config: mapping should produce empty rule set"
1443 );
1444 }
1445
1446 #[test]
1447 fn test_scalar_config_key_not_treated_as_cli2_wrapper() {
1448 let mut temp_file = NamedTempFile::new().unwrap();
1450 writeln!(
1451 temp_file,
1452 r#"config: true
1453MD013:
1454 line_length: 100"#
1455 )
1456 .unwrap();
1457
1458 let path = temp_file.path().with_extension("yaml");
1459 std::fs::rename(temp_file.path(), &path).unwrap();
1460
1461 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
1462 assert_eq!(config.0.len(), 2);
1464 assert!(config.0.contains_key("config"));
1465 assert!(config.0.contains_key("MD013"));
1466 }
1467
1468 #[test]
1469 fn test_import_case_insensitive_alias_preserved_lowercase() {
1470 let mut config_map = HashMap::new();
1471 config_map.insert("Line-Length".to_string(), serde_yaml::Value::Bool(false));
1472
1473 let mdl_config = MarkdownlintConfig(config_map);
1474 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
1475
1476 assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
1478 }
1479}