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_yml::Value>);
13
14pub fn load_markdownlint_config(path: &str) -> Result<MarkdownlintConfig, String> {
16 let content = fs::read_to_string(path).map_err(|e| format!("Failed to read config file {path}: {e}"))?;
17
18 if path.ends_with(".json") || path.ends_with(".jsonc") {
19 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {e}"))
20 } else if path.ends_with(".yaml") || path.ends_with(".yml") {
21 serde_yml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {e}"))
22 } else {
23 serde_json::from_str(&content)
24 .or_else(|_| serde_yml::from_str(&content))
25 .map_err(|e| format!("Failed to parse config as JSON or YAML: {e}"))
26 }
27}
28
29pub fn markdownlint_to_rumdl_rule_key(key: &str) -> Option<&'static str> {
33 crate::config::resolve_rule_name_alias(key)
35}
36
37fn normalize_toml_table_keys(val: toml::Value) -> toml::Value {
38 match val {
39 toml::Value::Table(table) => {
40 let mut new_table = toml::map::Map::new();
41 for (k, v) in table {
42 let norm_k = crate::config::normalize_key(&k);
43 new_table.insert(norm_k, normalize_toml_table_keys(v));
44 }
45 toml::Value::Table(new_table)
46 }
47 toml::Value::Array(arr) => toml::Value::Array(arr.into_iter().map(normalize_toml_table_keys).collect()),
48 other => other,
49 }
50}
51
52fn map_markdownlint_options_to_rumdl(
56 rule_key: &str,
57 table: toml::map::Map<String, toml::Value>,
58) -> Option<toml::map::Map<String, toml::Value>> {
59 let mut mapped = toml::map::Map::new();
60
61 match rule_key {
62 "MD013" => {
63 for (k, v) in table {
65 match k.as_str() {
66 "code-block-line-length" | "code_block_line_length" => {
69 log::warn!(
72 "Ignoring markdownlint option 'code_block_line_length' for MD013. Use 'code-blocks = false' in rumdl to disable line length checking in code blocks."
73 );
74 }
75 "heading-line-length" | "heading_line_length" => {
76 log::warn!(
78 "Ignoring markdownlint option 'heading_line_length' for MD013. Use 'headings = false' in rumdl to disable line length checking in headings."
79 );
80 }
81 "stern" => {
82 mapped.insert("strict".to_string(), v);
84 }
85 _ => {
87 mapped.insert(k, v);
88 }
89 }
90 }
91 Some(mapped)
92 }
93 "MD054" => {
94 for (k, v) in table {
97 match k.as_str() {
98 "style" | "styles" => {
99 log::warn!(
102 "Ignoring markdownlint option '{k}' for MD054. rumdl uses individual boolean flags (autolink, inline, full, collapsed, shortcut, url-inline) instead. Please configure these directly."
103 );
104 }
105 _ => {
107 mapped.insert(k, v);
108 }
109 }
110 }
111 Some(mapped)
112 }
113 _ => Some(table),
115 }
116}
117
118impl MarkdownlintConfig {
120 pub fn map_to_sourced_rumdl_config(&self, file_path: Option<&str>) -> SourcedConfig {
122 let mut sourced_config = SourcedConfig::default();
123 let file = file_path.map(|s| s.to_string());
124
125 let default_enabled = self.0.get("default").and_then(|v| v.as_bool()).unwrap_or(true);
127
128 let mut disabled_rules = Vec::new();
129 let mut enabled_rules = Vec::new();
130
131 for (key, value) in &self.0 {
132 if key == "default" {
134 continue;
135 }
136
137 let mapped = markdownlint_to_rumdl_rule_key(key);
138 if let Some(rumdl_key) = mapped {
139 let norm_rule_key = rumdl_key.to_ascii_uppercase();
140
141 if value.is_bool() {
143 let is_enabled = value.as_bool().unwrap_or(false);
144 if default_enabled {
145 if !is_enabled {
146 disabled_rules.push(norm_rule_key.clone());
147 }
148 } else if is_enabled {
149 enabled_rules.push(norm_rule_key.clone());
150 }
151 continue;
152 }
153
154 let toml_value: Option<toml::Value> = serde_yml::from_value::<toml::Value>(value.clone()).ok();
155 let toml_value = toml_value.map(normalize_toml_table_keys);
156 let rule_config = sourced_config.rules.entry(norm_rule_key.clone()).or_default();
157 if let Some(tv) = toml_value {
158 if let toml::Value::Table(mut table) = tv {
159 table = match map_markdownlint_options_to_rumdl(&norm_rule_key, table) {
161 Some(mapped) => mapped,
162 None => continue, };
164
165 if norm_rule_key == "MD007" && !table.contains_key("style") {
167 table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
168 }
169
170 for (k, v) in table {
171 let norm_config_key = k; rule_config
173 .values
174 .entry(norm_config_key.clone())
175 .and_modify(|sv| {
176 sv.value = v.clone();
177 sv.source = ConfigSource::ProjectConfig;
178 sv.overrides.push(crate::config::ConfigOverride {
179 value: v.clone(),
180 source: ConfigSource::ProjectConfig,
181 file: file.clone(),
182 line: None,
183 });
184 })
185 .or_insert_with(|| SourcedValue {
186 value: v.clone(),
187 source: ConfigSource::ProjectConfig,
188 overrides: vec![crate::config::ConfigOverride {
189 value: v,
190 source: ConfigSource::ProjectConfig,
191 file: file.clone(),
192 line: None,
193 }],
194 });
195 }
196 } else {
197 rule_config
198 .values
199 .entry("value".to_string())
200 .and_modify(|sv| {
201 sv.value = tv.clone();
202 sv.source = ConfigSource::ProjectConfig;
203 sv.overrides.push(crate::config::ConfigOverride {
204 value: tv.clone(),
205 source: ConfigSource::ProjectConfig,
206 file: file.clone(),
207 line: None,
208 });
209 })
210 .or_insert_with(|| SourcedValue {
211 value: tv.clone(),
212 source: ConfigSource::ProjectConfig,
213 overrides: vec![crate::config::ConfigOverride {
214 value: tv,
215 source: ConfigSource::ProjectConfig,
216 file: file.clone(),
217 line: None,
218 }],
219 });
220
221 if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
223 rule_config.values.insert(
224 "style".to_string(),
225 SourcedValue {
226 value: toml::Value::String("fixed".to_string()),
227 source: ConfigSource::ProjectConfig,
228 overrides: vec![crate::config::ConfigOverride {
229 value: toml::Value::String("fixed".to_string()),
230 source: ConfigSource::ProjectConfig,
231 file: file.clone(),
232 line: None,
233 }],
234 },
235 );
236 }
237 }
238 if !default_enabled {
240 enabled_rules.push(norm_rule_key.clone());
241 }
242 } else {
243 log::error!(
244 "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."
245 );
246 std::process::exit(1);
247 }
248 }
249 }
250
251 if !disabled_rules.is_empty() {
253 sourced_config.global.disable = SourcedValue::new(disabled_rules, ConfigSource::ProjectConfig);
254 }
255 if !enabled_rules.is_empty() || !default_enabled {
256 sourced_config.global.enable = SourcedValue::new(enabled_rules, ConfigSource::ProjectConfig);
257 }
258
259 if let Some(_f) = file {
260 sourced_config.loaded_files.push(_f);
261 }
262 sourced_config
263 }
264
265 pub fn map_to_sourced_rumdl_config_fragment(
267 &self,
268 file_path: Option<&str>,
269 ) -> crate::config::SourcedConfigFragment {
270 let mut fragment = crate::config::SourcedConfigFragment::default();
271 let file = file_path.map(|s| s.to_string());
272
273 let default_enabled = self.0.get("default").and_then(|v| v.as_bool()).unwrap_or(true);
277
278 let mut disabled_rules = Vec::new();
280 let mut enabled_rules = Vec::new();
281
282 for (key, value) in &self.0 {
283 if key == "default" {
285 continue;
286 }
287
288 let mapped = markdownlint_to_rumdl_rule_key(key);
289 if let Some(rumdl_key) = mapped {
290 let norm_rule_key = rumdl_key.to_ascii_uppercase();
291 if value.is_bool() {
293 let enabled = value.as_bool().unwrap_or(false);
294 if default_enabled {
295 if !enabled {
298 disabled_rules.push(norm_rule_key.clone());
299 }
300 } else {
301 if enabled {
304 enabled_rules.push(norm_rule_key.clone());
305 }
306 }
307 continue;
308 }
309 let toml_value: Option<toml::Value> = serde_yml::from_value::<toml::Value>(value.clone()).ok();
310 let toml_value = toml_value.map(normalize_toml_table_keys);
311 let rule_config = fragment.rules.entry(norm_rule_key.clone()).or_default();
312 if let Some(tv) = toml_value {
313 let tv = if norm_rule_key == "MD013" && tv.is_integer() {
316 let mut table = toml::map::Map::new();
317 table.insert("line-length".to_string(), tv);
318 toml::Value::Table(table)
319 } else {
320 tv
321 };
322
323 if let toml::Value::Table(mut table) = tv {
324 table = match map_markdownlint_options_to_rumdl(&norm_rule_key, table) {
326 Some(mapped) => mapped,
327 None => continue, };
329
330 if norm_rule_key == "MD007" && !table.contains_key("style") {
332 table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
333 }
334
335 for (rk, rv) in table {
336 let norm_rk = crate::config::normalize_key(&rk);
337 let sv = rule_config.values.entry(norm_rk.clone()).or_insert_with(|| {
338 crate::config::SourcedValue::new(rv.clone(), crate::config::ConfigSource::ProjectConfig)
339 });
340 sv.push_override(rv, crate::config::ConfigSource::ProjectConfig, file.clone(), None);
341 }
342 } else {
343 rule_config
344 .values
345 .entry("value".to_string())
346 .and_modify(|sv| {
347 sv.value = tv.clone();
348 sv.source = crate::config::ConfigSource::ProjectConfig;
349 sv.overrides.push(crate::config::ConfigOverride {
350 value: tv.clone(),
351 source: crate::config::ConfigSource::ProjectConfig,
352 file: file.clone(),
353 line: None,
354 });
355 })
356 .or_insert_with(|| crate::config::SourcedValue {
357 value: tv.clone(),
358 source: crate::config::ConfigSource::ProjectConfig,
359 overrides: vec![crate::config::ConfigOverride {
360 value: tv,
361 source: crate::config::ConfigSource::ProjectConfig,
362 file: file.clone(),
363 line: None,
364 }],
365 });
366
367 if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
369 rule_config.values.insert(
370 "style".to_string(),
371 crate::config::SourcedValue {
372 value: toml::Value::String("fixed".to_string()),
373 source: crate::config::ConfigSource::ProjectConfig,
374 overrides: vec![crate::config::ConfigOverride {
375 value: toml::Value::String("fixed".to_string()),
376 source: crate::config::ConfigSource::ProjectConfig,
377 file: file.clone(),
378 line: None,
379 }],
380 },
381 );
382 }
383 }
384
385 if !default_enabled {
387 enabled_rules.push(norm_rule_key.clone());
388 }
389 }
390 }
391 }
392
393 if !disabled_rules.is_empty() {
395 fragment.global.disable.push_override(
396 disabled_rules,
397 crate::config::ConfigSource::ProjectConfig,
398 file.clone(),
399 None,
400 );
401 }
402
403 if !enabled_rules.is_empty() || !default_enabled {
408 fragment.global.enable.push_override(
409 enabled_rules,
410 crate::config::ConfigSource::ProjectConfig,
411 file.clone(),
412 None,
413 );
414 }
415
416 if let Some(_f) = file {
417 }
419 fragment
420 }
421}
422
423#[cfg(test)]
426mod tests {
427 use super::*;
428 use std::io::Write;
429 use tempfile::NamedTempFile;
430
431 #[test]
432 fn test_markdownlint_to_rumdl_rule_key() {
433 assert_eq!(markdownlint_to_rumdl_rule_key("MD001"), Some("MD001"));
435 assert_eq!(markdownlint_to_rumdl_rule_key("MD058"), Some("MD058"));
436
437 assert_eq!(markdownlint_to_rumdl_rule_key("heading-increment"), Some("MD001"));
439 assert_eq!(markdownlint_to_rumdl_rule_key("HEADING-INCREMENT"), Some("MD001"));
440 assert_eq!(markdownlint_to_rumdl_rule_key("ul-style"), Some("MD004"));
441 assert_eq!(markdownlint_to_rumdl_rule_key("no-trailing-spaces"), Some("MD009"));
442 assert_eq!(markdownlint_to_rumdl_rule_key("line-length"), Some("MD013"));
443 assert_eq!(markdownlint_to_rumdl_rule_key("single-title"), Some("MD025"));
444 assert_eq!(markdownlint_to_rumdl_rule_key("single-h1"), Some("MD025"));
445 assert_eq!(markdownlint_to_rumdl_rule_key("no-bare-urls"), Some("MD034"));
446 assert_eq!(markdownlint_to_rumdl_rule_key("code-block-style"), Some("MD046"));
447 assert_eq!(markdownlint_to_rumdl_rule_key("code-fence-style"), Some("MD048"));
448
449 assert_eq!(markdownlint_to_rumdl_rule_key("heading_increment"), Some("MD001"));
451 assert_eq!(markdownlint_to_rumdl_rule_key("HEADING_INCREMENT"), Some("MD001"));
452 assert_eq!(markdownlint_to_rumdl_rule_key("ul_style"), Some("MD004"));
453 assert_eq!(markdownlint_to_rumdl_rule_key("no_trailing_spaces"), Some("MD009"));
454 assert_eq!(markdownlint_to_rumdl_rule_key("line_length"), Some("MD013"));
455 assert_eq!(markdownlint_to_rumdl_rule_key("single_title"), Some("MD025"));
456 assert_eq!(markdownlint_to_rumdl_rule_key("single_h1"), Some("MD025"));
457 assert_eq!(markdownlint_to_rumdl_rule_key("no_bare_urls"), Some("MD034"));
458 assert_eq!(markdownlint_to_rumdl_rule_key("code_block_style"), Some("MD046"));
459 assert_eq!(markdownlint_to_rumdl_rule_key("code_fence_style"), Some("MD048"));
460
461 assert_eq!(markdownlint_to_rumdl_rule_key("md001"), Some("MD001"));
463 assert_eq!(markdownlint_to_rumdl_rule_key("Md001"), Some("MD001"));
464 assert_eq!(markdownlint_to_rumdl_rule_key("Line-Length"), Some("MD013"));
465 assert_eq!(markdownlint_to_rumdl_rule_key("Line_Length"), Some("MD013"));
466
467 assert_eq!(markdownlint_to_rumdl_rule_key("MD999"), None);
469 assert_eq!(markdownlint_to_rumdl_rule_key("invalid-rule"), None);
470 assert_eq!(markdownlint_to_rumdl_rule_key(""), None);
471 }
472
473 #[test]
474 fn test_normalize_toml_table_keys() {
475 use toml::map::Map;
476
477 let mut table = Map::new();
479 table.insert("snake_case".to_string(), toml::Value::String("value1".to_string()));
480 table.insert("kebab-case".to_string(), toml::Value::String("value2".to_string()));
481 table.insert("MD013".to_string(), toml::Value::Integer(100));
482
483 let normalized = normalize_toml_table_keys(toml::Value::Table(table));
484
485 if let toml::Value::Table(norm_table) = normalized {
486 assert!(norm_table.contains_key("snake-case"));
487 assert!(norm_table.contains_key("kebab-case"));
488 assert!(norm_table.contains_key("MD013"));
489 assert_eq!(
490 norm_table.get("snake-case").unwrap(),
491 &toml::Value::String("value1".to_string())
492 );
493 assert_eq!(
494 norm_table.get("kebab-case").unwrap(),
495 &toml::Value::String("value2".to_string())
496 );
497 } else {
498 panic!("Expected normalized value to be a table");
499 }
500
501 let array = toml::Value::Array(vec![toml::Value::String("test".to_string()), toml::Value::Integer(42)]);
503 let normalized_array = normalize_toml_table_keys(array.clone());
504 assert_eq!(normalized_array, array);
505
506 let simple = toml::Value::String("simple".to_string());
508 assert_eq!(normalize_toml_table_keys(simple.clone()), simple);
509 }
510
511 #[test]
512 fn test_load_markdownlint_config_json() {
513 let mut temp_file = NamedTempFile::new().unwrap();
514 writeln!(
515 temp_file,
516 r#"{{
517 "MD013": {{ "line_length": 100 }},
518 "MD025": true,
519 "MD026": false,
520 "heading-style": {{ "style": "atx" }}
521 }}"#
522 )
523 .unwrap();
524
525 let config = load_markdownlint_config(temp_file.path().to_str().unwrap()).unwrap();
526 assert_eq!(config.0.len(), 4);
527 assert!(config.0.contains_key("MD013"));
528 assert!(config.0.contains_key("MD025"));
529 assert!(config.0.contains_key("MD026"));
530 assert!(config.0.contains_key("heading-style"));
531 }
532
533 #[test]
534 fn test_load_markdownlint_config_yaml() {
535 let mut temp_file = NamedTempFile::new().unwrap();
536 writeln!(
537 temp_file,
538 r#"MD013:
539 line_length: 120
540MD025: true
541MD026: false
542ul-style:
543 style: dash"#
544 )
545 .unwrap();
546
547 let path = temp_file.path().with_extension("yaml");
548 std::fs::rename(temp_file.path(), &path).unwrap();
549
550 let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
551 assert_eq!(config.0.len(), 4);
552 assert!(config.0.contains_key("MD013"));
553 assert!(config.0.contains_key("ul-style"));
554 }
555
556 #[test]
557 fn test_load_markdownlint_config_invalid() {
558 let mut temp_file = NamedTempFile::new().unwrap();
559 writeln!(temp_file, "invalid json/yaml content {{").unwrap();
560
561 let result = load_markdownlint_config(temp_file.path().to_str().unwrap());
562 assert!(result.is_err());
563 }
564
565 #[test]
566 fn test_load_markdownlint_config_nonexistent() {
567 let result = load_markdownlint_config("/nonexistent/file.json");
568 assert!(result.is_err());
569 assert!(result.unwrap_err().contains("Failed to read config file"));
570 }
571
572 #[test]
573 fn test_map_to_sourced_rumdl_config() {
574 let mut config_map = HashMap::new();
575 config_map.insert(
576 "MD013".to_string(),
577 serde_yml::Value::Mapping({
578 let mut map = serde_yml::Mapping::new();
579 map.insert(
580 serde_yml::Value::String("line_length".to_string()),
581 serde_yml::Value::Number(serde_yml::Number::from(100)),
582 );
583 map
584 }),
585 );
586 config_map.insert("MD025".to_string(), serde_yml::Value::Bool(true));
587 config_map.insert("MD026".to_string(), serde_yml::Value::Bool(false));
588
589 let mdl_config = MarkdownlintConfig(config_map);
590 let sourced_config = mdl_config.map_to_sourced_rumdl_config(Some("test.json"));
591
592 assert!(sourced_config.rules.contains_key("MD013"));
594 let md013_config = &sourced_config.rules["MD013"];
595 assert!(md013_config.values.contains_key("line-length"));
596 assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(100));
597 assert_eq!(md013_config.values["line-length"].source, ConfigSource::ProjectConfig);
598
599 assert_eq!(sourced_config.loaded_files.len(), 1);
601 assert_eq!(sourced_config.loaded_files[0], "test.json");
602 }
603
604 #[test]
605 fn test_map_to_sourced_rumdl_config_fragment() {
606 let mut config_map = HashMap::new();
607
608 config_map.insert(
610 "line-length".to_string(),
611 serde_yml::Value::Number(serde_yml::Number::from(120)),
612 );
613
614 config_map.insert("MD025".to_string(), serde_yml::Value::Bool(false));
616
617 config_map.insert("MD026".to_string(), serde_yml::Value::Bool(true));
619
620 config_map.insert(
622 "MD003".to_string(),
623 serde_yml::Value::Mapping({
624 let mut map = serde_yml::Mapping::new();
625 map.insert(
626 serde_yml::Value::String("style".to_string()),
627 serde_yml::Value::String("atx".to_string()),
628 );
629 map
630 }),
631 );
632
633 let mdl_config = MarkdownlintConfig(config_map);
634 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
635
636 assert!(fragment.rules.contains_key("MD013"));
638 let md013_config = &fragment.rules["MD013"];
639 assert!(md013_config.values.contains_key("line-length"));
640 assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(120));
641
642 assert!(fragment.global.disable.value.contains(&"MD025".to_string()));
644
645 assert!(
647 !fragment.global.enable.value.contains(&"MD026".to_string()),
648 "Boolean true should be no-op when default is absent (treated as true)"
649 );
650 assert!(fragment.global.enable.value.is_empty());
651
652 assert!(fragment.rules.contains_key("MD003"));
654 let md003_config = &fragment.rules["MD003"];
655 assert!(md003_config.values.contains_key("style"));
656 }
657
658 #[test]
659 fn test_edge_cases() {
660 let mut config_map = HashMap::new();
661
662 let empty_config = MarkdownlintConfig(HashMap::new());
664 let sourced = empty_config.map_to_sourced_rumdl_config(None);
665 assert!(sourced.rules.is_empty());
666
667 config_map.insert("unknown-rule".to_string(), serde_yml::Value::Bool(true));
669 config_map.insert("MD999".to_string(), serde_yml::Value::Bool(true));
670
671 let config = MarkdownlintConfig(config_map);
672 let sourced = config.map_to_sourced_rumdl_config(None);
673 assert!(sourced.rules.is_empty()); }
675
676 #[test]
677 fn test_complex_rule_configurations() {
678 let mut config_map = HashMap::new();
679
680 config_map.insert(
682 "MD044".to_string(),
683 serde_yml::Value::Mapping({
684 let mut map = serde_yml::Mapping::new();
685 map.insert(
686 serde_yml::Value::String("names".to_string()),
687 serde_yml::Value::Sequence(vec![
688 serde_yml::Value::String("JavaScript".to_string()),
689 serde_yml::Value::String("GitHub".to_string()),
690 ]),
691 );
692 map
693 }),
694 );
695
696 config_map.insert(
698 "MD003".to_string(),
699 serde_yml::Value::Mapping({
700 let mut map = serde_yml::Mapping::new();
701 map.insert(
702 serde_yml::Value::String("style".to_string()),
703 serde_yml::Value::String("atx".to_string()),
704 );
705 map
706 }),
707 );
708
709 let mdl_config = MarkdownlintConfig(config_map);
710 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
711
712 assert!(sourced.rules.contains_key("MD044"));
714 let md044_config = &sourced.rules["MD044"];
715 assert!(md044_config.values.contains_key("names"));
716
717 assert!(sourced.rules.contains_key("MD003"));
719 let md003_config = &sourced.rules["MD003"];
720 assert!(md003_config.values.contains_key("style"));
721 assert_eq!(
722 md003_config.values["style"].value,
723 toml::Value::String("atx".to_string())
724 );
725 }
726
727 #[test]
728 fn test_value_types() {
729 let mut config_map = HashMap::new();
730
731 config_map.insert(
733 "MD007".to_string(),
734 serde_yml::Value::Number(serde_yml::Number::from(4)),
735 ); config_map.insert(
737 "MD009".to_string(),
738 serde_yml::Value::Mapping({
739 let mut map = serde_yml::Mapping::new();
740 map.insert(
741 serde_yml::Value::String("br_spaces".to_string()),
742 serde_yml::Value::Number(serde_yml::Number::from(2)),
743 );
744 map.insert(
745 serde_yml::Value::String("strict".to_string()),
746 serde_yml::Value::Bool(true),
747 );
748 map
749 }),
750 );
751
752 let mdl_config = MarkdownlintConfig(config_map);
753 let sourced = mdl_config.map_to_sourced_rumdl_config(None);
754
755 assert!(sourced.rules.contains_key("MD007"));
757 assert!(sourced.rules["MD007"].values.contains_key("value"));
758
759 assert!(sourced.rules.contains_key("MD009"));
761 let md009_config = &sourced.rules["MD009"];
762 assert!(md009_config.values.contains_key("br-spaces"));
763 assert!(md009_config.values.contains_key("strict"));
764 }
765
766 #[test]
767 fn test_all_rule_aliases() {
768 let aliases = vec![
770 ("heading-increment", "MD001"),
771 ("heading-style", "MD003"),
772 ("ul-style", "MD004"),
773 ("list-indent", "MD005"),
774 ("ul-indent", "MD007"),
775 ("no-trailing-spaces", "MD009"),
776 ("no-hard-tabs", "MD010"),
777 ("no-reversed-links", "MD011"),
778 ("no-multiple-blanks", "MD012"),
779 ("line-length", "MD013"),
780 ("commands-show-output", "MD014"),
781 ("no-missing-space-atx", "MD018"),
783 ("no-multiple-space-atx", "MD019"),
784 ("no-missing-space-closed-atx", "MD020"),
785 ("no-multiple-space-closed-atx", "MD021"),
786 ("blanks-around-headings", "MD022"),
787 ("heading-start-left", "MD023"),
788 ("no-duplicate-heading", "MD024"),
789 ("single-title", "MD025"),
790 ("single-h1", "MD025"),
791 ("no-trailing-punctuation", "MD026"),
792 ("no-multiple-space-blockquote", "MD027"),
793 ("no-blanks-blockquote", "MD028"),
794 ("ol-prefix", "MD029"),
795 ("list-marker-space", "MD030"),
796 ("blanks-around-fences", "MD031"),
797 ("blanks-around-lists", "MD032"),
798 ("no-inline-html", "MD033"),
799 ("no-bare-urls", "MD034"),
800 ("hr-style", "MD035"),
801 ("no-emphasis-as-heading", "MD036"),
802 ("no-space-in-emphasis", "MD037"),
803 ("no-space-in-code", "MD038"),
804 ("no-space-in-links", "MD039"),
805 ("fenced-code-language", "MD040"),
806 ("first-line-heading", "MD041"),
807 ("first-line-h1", "MD041"),
808 ("no-empty-links", "MD042"),
809 ("required-headings", "MD043"),
810 ("proper-names", "MD044"),
811 ("no-alt-text", "MD045"),
812 ("code-block-style", "MD046"),
813 ("single-trailing-newline", "MD047"),
814 ("code-fence-style", "MD048"),
815 ("emphasis-style", "MD049"),
816 ("strong-style", "MD050"),
817 ("link-fragments", "MD051"),
818 ("reference-links-images", "MD052"),
819 ("link-image-reference-definitions", "MD053"),
820 ("link-image-style", "MD054"),
821 ("table-pipe-style", "MD055"),
822 ("table-column-count", "MD056"),
823 ("existing-relative-links", "MD057"),
824 ("blanks-around-tables", "MD058"),
825 ("descriptive-link-text", "MD059"),
826 ("table-cell-alignment", "MD060"),
827 ("table-format", "MD060"),
828 ("forbidden-terms", "MD061"),
829 ("nested-code-fence", "MD070"),
830 ("blank-line-after-frontmatter", "MD071"),
831 ("frontmatter-key-sort", "MD072"),
832 ];
833
834 for (alias, expected) in aliases {
835 assert_eq!(
836 markdownlint_to_rumdl_rule_key(alias),
837 Some(expected),
838 "Alias {alias} should map to {expected}"
839 );
840 }
841 }
842
843 #[test]
844 fn test_default_true_with_boolean_rules() {
845 let mut config_map = HashMap::new();
848 config_map.insert("default".to_string(), serde_yml::Value::Bool(true));
849 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
850 config_map.insert(
851 "MD013".to_string(),
852 serde_yml::Value::Mapping({
853 let mut map = serde_yml::Mapping::new();
854 map.insert(
855 serde_yml::Value::String("line_length".to_string()),
856 serde_yml::Value::Number(serde_yml::Number::from(120)),
857 );
858 map
859 }),
860 );
861
862 let mdl_config = MarkdownlintConfig(config_map);
863 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
864
865 assert!(
867 fragment.global.enable.value.is_empty(),
868 "Enable list should be empty when default: true"
869 );
870 assert!(fragment.global.disable.value.is_empty(), "Disable list should be empty");
872 assert!(fragment.rules.contains_key("MD013"));
874 assert_eq!(
875 fragment.rules["MD013"].values["line-length"].value,
876 toml::Value::Integer(120)
877 );
878 }
879
880 #[test]
881 fn test_default_false_with_boolean_and_config_rules() {
882 let mut config_map = HashMap::new();
885 config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
886 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
887 config_map.insert(
888 "MD013".to_string(),
889 serde_yml::Value::Mapping({
890 let mut map = serde_yml::Mapping::new();
891 map.insert(
892 serde_yml::Value::String("line_length".to_string()),
893 serde_yml::Value::Number(serde_yml::Number::from(120)),
894 );
895 map
896 }),
897 );
898
899 let mdl_config = MarkdownlintConfig(config_map);
900 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
901
902 let mut enabled_sorted = fragment.global.enable.value.clone();
903 enabled_sorted.sort();
904 assert_eq!(
905 enabled_sorted,
906 vec!["MD001", "MD013"],
907 "Both boolean-true and config-object rules should be in enable list"
908 );
909 assert!(fragment.global.disable.value.is_empty(), "No rules should be disabled");
910 assert!(fragment.rules.contains_key("MD013"));
912 assert_eq!(
913 fragment.rules["MD013"].values["line-length"].value,
914 toml::Value::Integer(120)
915 );
916 }
917
918 #[test]
919 fn test_default_absent_with_boolean_rules() {
920 let mut config_map = HashMap::new();
922 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
923 config_map.insert("MD009".to_string(), serde_yml::Value::Bool(false));
924
925 let mdl_config = MarkdownlintConfig(config_map);
926 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
927
928 assert!(
930 fragment.global.enable.value.is_empty(),
931 "Enable list should be empty when default is absent"
932 );
933 assert_eq!(fragment.global.disable.value, vec!["MD009"]);
935 }
936
937 #[test]
938 fn test_default_false_only_booleans() {
939 let mut config_map = HashMap::new();
942 config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
943 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
944 config_map.insert("MD009".to_string(), serde_yml::Value::Bool(false));
945
946 let mdl_config = MarkdownlintConfig(config_map);
947 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
948
949 assert_eq!(fragment.global.enable.value, vec!["MD001"]);
950 assert!(
951 fragment.global.disable.value.is_empty(),
952 "Disable list should be empty when default: false (false is no-op)"
953 );
954 }
955
956 #[test]
957 fn test_default_true_with_boolean_rules_legacy() {
958 let mut config_map = HashMap::new();
960 config_map.insert("default".to_string(), serde_yml::Value::Bool(true));
961 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
962 config_map.insert("MD009".to_string(), serde_yml::Value::Bool(false));
963 config_map.insert(
964 "MD013".to_string(),
965 serde_yml::Value::Mapping({
966 let mut map = serde_yml::Mapping::new();
967 map.insert(
968 serde_yml::Value::String("line_length".to_string()),
969 serde_yml::Value::Number(serde_yml::Number::from(120)),
970 );
971 map
972 }),
973 );
974
975 let mdl_config = MarkdownlintConfig(config_map);
976 let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
977
978 assert!(sourced.global.enable.value.is_empty());
980 assert_eq!(sourced.global.disable.value, vec!["MD009"]);
982 assert!(sourced.rules.contains_key("MD013"));
984 assert_eq!(
985 sourced.rules["MD013"].values["line-length"].value,
986 toml::Value::Integer(120)
987 );
988 }
989
990 #[test]
991 fn test_default_false_with_config_rules_legacy() {
992 let mut config_map = HashMap::new();
994 config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
995 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
996 config_map.insert(
997 "MD013".to_string(),
998 serde_yml::Value::Mapping({
999 let mut map = serde_yml::Mapping::new();
1000 map.insert(
1001 serde_yml::Value::String("line_length".to_string()),
1002 serde_yml::Value::Number(serde_yml::Number::from(120)),
1003 );
1004 map
1005 }),
1006 );
1007
1008 let mdl_config = MarkdownlintConfig(config_map);
1009 let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
1010
1011 let mut enabled_sorted = sourced.global.enable.value.clone();
1012 enabled_sorted.sort();
1013 assert_eq!(enabled_sorted, vec!["MD001", "MD013"]);
1014 assert!(sourced.global.disable.value.is_empty());
1015 }
1016
1017 #[test]
1018 fn test_default_false_no_rules_disables_everything() {
1019 let mut config_map = HashMap::new();
1021 config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
1022
1023 let mdl_config = MarkdownlintConfig(config_map);
1024 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1025
1026 assert!(fragment.global.enable.value.is_empty());
1028 assert_eq!(
1029 fragment.global.enable.source,
1030 crate::config::ConfigSource::ProjectConfig,
1031 "Enable source should be ProjectConfig when default: false"
1032 );
1033 }
1034
1035 #[test]
1036 fn test_default_false_only_false_rules_disables_everything() {
1037 let mut config_map = HashMap::new();
1039 config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
1040 config_map.insert("MD001".to_string(), serde_yml::Value::Bool(false));
1041
1042 let mdl_config = MarkdownlintConfig(config_map);
1043 let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
1044
1045 assert!(fragment.global.enable.value.is_empty());
1046 assert_eq!(
1047 fragment.global.enable.source,
1048 crate::config::ConfigSource::ProjectConfig,
1049 );
1050 }
1051}