use crate::config::{ConfigSource, SourcedConfig, SourcedValue};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
#[derive(Debug, Deserialize)]
pub struct MarkdownlintConfig(pub HashMap<String, serde_yml::Value>);
pub fn load_markdownlint_config(path: &str) -> Result<MarkdownlintConfig, String> {
let content = fs::read_to_string(path).map_err(|e| format!("Failed to read config file {path}: {e}"))?;
if path.ends_with(".json") || path.ends_with(".jsonc") {
serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {e}"))
} else if path.ends_with(".yaml") || path.ends_with(".yml") {
serde_yml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {e}"))
} else {
serde_json::from_str(&content)
.or_else(|_| serde_yml::from_str(&content))
.map_err(|e| format!("Failed to parse config as JSON or YAML: {e}"))
}
}
pub fn markdownlint_to_rumdl_rule_key(key: &str) -> Option<&'static str> {
crate::config::resolve_rule_name_alias(key)
}
fn normalize_toml_table_keys(val: toml::Value) -> toml::Value {
match val {
toml::Value::Table(table) => {
let mut new_table = toml::map::Map::new();
for (k, v) in table {
let norm_k = crate::config::normalize_key(&k);
new_table.insert(norm_k, normalize_toml_table_keys(v));
}
toml::Value::Table(new_table)
}
toml::Value::Array(arr) => toml::Value::Array(arr.into_iter().map(normalize_toml_table_keys).collect()),
other => other,
}
}
fn map_markdownlint_options_to_rumdl(
rule_key: &str,
table: toml::map::Map<String, toml::Value>,
) -> Option<toml::map::Map<String, toml::Value>> {
let mut mapped = toml::map::Map::new();
match rule_key {
"MD013" => {
for (k, v) in table {
match k.as_str() {
"code-block-line-length" | "code_block_line_length" => {
log::warn!(
"Ignoring markdownlint option 'code_block_line_length' for MD013. Use 'code-blocks = false' in rumdl to disable line length checking in code blocks."
);
}
"heading-line-length" | "heading_line_length" => {
log::warn!(
"Ignoring markdownlint option 'heading_line_length' for MD013. Use 'headings = false' in rumdl to disable line length checking in headings."
);
}
"stern" => {
mapped.insert("strict".to_string(), v);
}
_ => {
mapped.insert(k, v);
}
}
}
Some(mapped)
}
"MD054" => {
for (k, v) in table {
match k.as_str() {
"style" | "styles" => {
log::warn!(
"Ignoring markdownlint option '{k}' for MD054. rumdl uses individual boolean flags (autolink, inline, full, collapsed, shortcut, url-inline) instead. Please configure these directly."
);
}
_ => {
mapped.insert(k, v);
}
}
}
Some(mapped)
}
_ => Some(table),
}
}
impl MarkdownlintConfig {
pub fn map_to_sourced_rumdl_config(&self, file_path: Option<&str>) -> SourcedConfig {
let mut sourced_config = SourcedConfig::default();
let file = file_path.map(|s| s.to_string());
let default_enabled = self.0.get("default").and_then(|v| v.as_bool()).unwrap_or(true);
let mut disabled_rules = Vec::new();
let mut enabled_rules = Vec::new();
for (key, value) in &self.0 {
if key == "default" {
continue;
}
let mapped = markdownlint_to_rumdl_rule_key(key);
if let Some(rumdl_key) = mapped {
let norm_rule_key = rumdl_key.to_ascii_uppercase();
if value.is_bool() {
let is_enabled = value.as_bool().unwrap_or(false);
if default_enabled {
if !is_enabled {
disabled_rules.push(norm_rule_key.clone());
}
} else if is_enabled {
enabled_rules.push(norm_rule_key.clone());
}
continue;
}
let toml_value: Option<toml::Value> = serde_yml::from_value::<toml::Value>(value.clone()).ok();
let toml_value = toml_value.map(normalize_toml_table_keys);
let rule_config = sourced_config.rules.entry(norm_rule_key.clone()).or_default();
if let Some(tv) = toml_value {
if let toml::Value::Table(mut table) = tv {
table = match map_markdownlint_options_to_rumdl(&norm_rule_key, table) {
Some(mapped) => mapped,
None => continue, };
if norm_rule_key == "MD007" && !table.contains_key("style") {
table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
}
for (k, v) in table {
let norm_config_key = k; rule_config
.values
.entry(norm_config_key.clone())
.and_modify(|sv| {
sv.value = v.clone();
sv.source = ConfigSource::ProjectConfig;
sv.overrides.push(crate::config::ConfigOverride {
value: v.clone(),
source: ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
});
})
.or_insert_with(|| SourcedValue {
value: v.clone(),
source: ConfigSource::ProjectConfig,
overrides: vec![crate::config::ConfigOverride {
value: v,
source: ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
}],
});
}
} else {
rule_config
.values
.entry("value".to_string())
.and_modify(|sv| {
sv.value = tv.clone();
sv.source = ConfigSource::ProjectConfig;
sv.overrides.push(crate::config::ConfigOverride {
value: tv.clone(),
source: ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
});
})
.or_insert_with(|| SourcedValue {
value: tv.clone(),
source: ConfigSource::ProjectConfig,
overrides: vec![crate::config::ConfigOverride {
value: tv,
source: ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
}],
});
if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
rule_config.values.insert(
"style".to_string(),
SourcedValue {
value: toml::Value::String("fixed".to_string()),
source: ConfigSource::ProjectConfig,
overrides: vec![crate::config::ConfigOverride {
value: toml::Value::String("fixed".to_string()),
source: ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
}],
},
);
}
}
if !default_enabled {
enabled_rules.push(norm_rule_key.clone());
}
} else {
log::error!(
"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."
);
std::process::exit(1);
}
}
}
if !disabled_rules.is_empty() {
sourced_config.global.disable = SourcedValue::new(disabled_rules, ConfigSource::ProjectConfig);
}
if !enabled_rules.is_empty() || !default_enabled {
sourced_config.global.enable = SourcedValue::new(enabled_rules, ConfigSource::ProjectConfig);
}
if let Some(_f) = file {
sourced_config.loaded_files.push(_f);
}
sourced_config
}
pub fn map_to_sourced_rumdl_config_fragment(
&self,
file_path: Option<&str>,
) -> crate::config::SourcedConfigFragment {
let mut fragment = crate::config::SourcedConfigFragment::default();
let file = file_path.map(|s| s.to_string());
let default_enabled = self.0.get("default").and_then(|v| v.as_bool()).unwrap_or(true);
let mut disabled_rules = Vec::new();
let mut enabled_rules = Vec::new();
for (key, value) in &self.0 {
if key == "default" {
continue;
}
let mapped = markdownlint_to_rumdl_rule_key(key);
if let Some(rumdl_key) = mapped {
let norm_rule_key = rumdl_key.to_ascii_uppercase();
let display_name = if key.to_ascii_uppercase() == norm_rule_key {
norm_rule_key.clone()
} else {
key.to_lowercase().replace('_', "-")
};
fragment
.rule_display_names
.insert(norm_rule_key.clone(), display_name.clone());
if value.is_bool() {
let enabled = value.as_bool().unwrap_or(false);
if default_enabled {
if !enabled {
disabled_rules.push(display_name);
}
} else {
if enabled {
enabled_rules.push(display_name);
}
}
continue;
}
let toml_value: Option<toml::Value> = serde_yml::from_value::<toml::Value>(value.clone()).ok();
let toml_value = toml_value.map(normalize_toml_table_keys);
let rule_config = fragment.rules.entry(norm_rule_key.clone()).or_default();
if let Some(tv) = toml_value {
let tv = if norm_rule_key == "MD013" && tv.is_integer() {
let mut table = toml::map::Map::new();
table.insert("line-length".to_string(), tv);
toml::Value::Table(table)
} else {
tv
};
if let toml::Value::Table(mut table) = tv {
table = match map_markdownlint_options_to_rumdl(&norm_rule_key, table) {
Some(mapped) => mapped,
None => continue, };
if norm_rule_key == "MD007" && !table.contains_key("style") {
table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
}
for (rk, rv) in table {
let norm_rk = crate::config::normalize_key(&rk);
let sv = rule_config.values.entry(norm_rk.clone()).or_insert_with(|| {
crate::config::SourcedValue::new(rv.clone(), crate::config::ConfigSource::ProjectConfig)
});
sv.push_override(rv, crate::config::ConfigSource::ProjectConfig, file.clone(), None);
}
} else {
rule_config
.values
.entry("value".to_string())
.and_modify(|sv| {
sv.value = tv.clone();
sv.source = crate::config::ConfigSource::ProjectConfig;
sv.overrides.push(crate::config::ConfigOverride {
value: tv.clone(),
source: crate::config::ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
});
})
.or_insert_with(|| crate::config::SourcedValue {
value: tv.clone(),
source: crate::config::ConfigSource::ProjectConfig,
overrides: vec![crate::config::ConfigOverride {
value: tv,
source: crate::config::ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
}],
});
if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
rule_config.values.insert(
"style".to_string(),
crate::config::SourcedValue {
value: toml::Value::String("fixed".to_string()),
source: crate::config::ConfigSource::ProjectConfig,
overrides: vec![crate::config::ConfigOverride {
value: toml::Value::String("fixed".to_string()),
source: crate::config::ConfigSource::ProjectConfig,
file: file.clone(),
line: None,
}],
},
);
}
}
if !default_enabled {
enabled_rules.push(display_name.clone());
}
}
}
}
if !disabled_rules.is_empty() {
fragment.global.disable.push_override(
disabled_rules,
crate::config::ConfigSource::ProjectConfig,
file.clone(),
None,
);
}
if !enabled_rules.is_empty() || !default_enabled {
fragment.global.enable.push_override(
enabled_rules,
crate::config::ConfigSource::ProjectConfig,
file.clone(),
None,
);
}
if let Some(_f) = file {
}
fragment
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_markdownlint_to_rumdl_rule_key() {
assert_eq!(markdownlint_to_rumdl_rule_key("MD001"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("MD058"), Some("MD058"));
assert_eq!(markdownlint_to_rumdl_rule_key("heading-increment"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("HEADING-INCREMENT"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("ul-style"), Some("MD004"));
assert_eq!(markdownlint_to_rumdl_rule_key("no-trailing-spaces"), Some("MD009"));
assert_eq!(markdownlint_to_rumdl_rule_key("line-length"), Some("MD013"));
assert_eq!(markdownlint_to_rumdl_rule_key("single-title"), Some("MD025"));
assert_eq!(markdownlint_to_rumdl_rule_key("single-h1"), Some("MD025"));
assert_eq!(markdownlint_to_rumdl_rule_key("no-bare-urls"), Some("MD034"));
assert_eq!(markdownlint_to_rumdl_rule_key("code-block-style"), Some("MD046"));
assert_eq!(markdownlint_to_rumdl_rule_key("code-fence-style"), Some("MD048"));
assert_eq!(markdownlint_to_rumdl_rule_key("heading_increment"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("HEADING_INCREMENT"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("ul_style"), Some("MD004"));
assert_eq!(markdownlint_to_rumdl_rule_key("no_trailing_spaces"), Some("MD009"));
assert_eq!(markdownlint_to_rumdl_rule_key("line_length"), Some("MD013"));
assert_eq!(markdownlint_to_rumdl_rule_key("single_title"), Some("MD025"));
assert_eq!(markdownlint_to_rumdl_rule_key("single_h1"), Some("MD025"));
assert_eq!(markdownlint_to_rumdl_rule_key("no_bare_urls"), Some("MD034"));
assert_eq!(markdownlint_to_rumdl_rule_key("code_block_style"), Some("MD046"));
assert_eq!(markdownlint_to_rumdl_rule_key("code_fence_style"), Some("MD048"));
assert_eq!(markdownlint_to_rumdl_rule_key("md001"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("Md001"), Some("MD001"));
assert_eq!(markdownlint_to_rumdl_rule_key("Line-Length"), Some("MD013"));
assert_eq!(markdownlint_to_rumdl_rule_key("Line_Length"), Some("MD013"));
assert_eq!(markdownlint_to_rumdl_rule_key("MD999"), None);
assert_eq!(markdownlint_to_rumdl_rule_key("invalid-rule"), None);
assert_eq!(markdownlint_to_rumdl_rule_key(""), None);
}
#[test]
fn test_normalize_toml_table_keys() {
use toml::map::Map;
let mut table = Map::new();
table.insert("snake_case".to_string(), toml::Value::String("value1".to_string()));
table.insert("kebab-case".to_string(), toml::Value::String("value2".to_string()));
table.insert("MD013".to_string(), toml::Value::Integer(100));
let normalized = normalize_toml_table_keys(toml::Value::Table(table));
if let toml::Value::Table(norm_table) = normalized {
assert!(norm_table.contains_key("snake-case"));
assert!(norm_table.contains_key("kebab-case"));
assert!(norm_table.contains_key("MD013"));
assert_eq!(
norm_table.get("snake-case").unwrap(),
&toml::Value::String("value1".to_string())
);
assert_eq!(
norm_table.get("kebab-case").unwrap(),
&toml::Value::String("value2".to_string())
);
} else {
panic!("Expected normalized value to be a table");
}
let array = toml::Value::Array(vec![toml::Value::String("test".to_string()), toml::Value::Integer(42)]);
let normalized_array = normalize_toml_table_keys(array.clone());
assert_eq!(normalized_array, array);
let simple = toml::Value::String("simple".to_string());
assert_eq!(normalize_toml_table_keys(simple.clone()), simple);
}
#[test]
fn test_load_markdownlint_config_json() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"{{
"MD013": {{ "line_length": 100 }},
"MD025": true,
"MD026": false,
"heading-style": {{ "style": "atx" }}
}}"#
)
.unwrap();
let config = load_markdownlint_config(temp_file.path().to_str().unwrap()).unwrap();
assert_eq!(config.0.len(), 4);
assert!(config.0.contains_key("MD013"));
assert!(config.0.contains_key("MD025"));
assert!(config.0.contains_key("MD026"));
assert!(config.0.contains_key("heading-style"));
}
#[test]
fn test_load_markdownlint_config_yaml() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"MD013:
line_length: 120
MD025: true
MD026: false
ul-style:
style: dash"#
)
.unwrap();
let path = temp_file.path().with_extension("yaml");
std::fs::rename(temp_file.path(), &path).unwrap();
let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
assert_eq!(config.0.len(), 4);
assert!(config.0.contains_key("MD013"));
assert!(config.0.contains_key("ul-style"));
}
#[test]
fn test_load_markdownlint_config_invalid() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "invalid json/yaml content {{").unwrap();
let result = load_markdownlint_config(temp_file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_load_markdownlint_config_nonexistent() {
let result = load_markdownlint_config("/nonexistent/file.json");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to read config file"));
}
#[test]
fn test_map_to_sourced_rumdl_config() {
let mut config_map = HashMap::new();
config_map.insert(
"MD013".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(100)),
);
map
}),
);
config_map.insert("MD025".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD026".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let sourced_config = mdl_config.map_to_sourced_rumdl_config(Some("test.json"));
assert!(sourced_config.rules.contains_key("MD013"));
let md013_config = &sourced_config.rules["MD013"];
assert!(md013_config.values.contains_key("line-length"));
assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(100));
assert_eq!(md013_config.values["line-length"].source, ConfigSource::ProjectConfig);
assert_eq!(sourced_config.loaded_files.len(), 1);
assert_eq!(sourced_config.loaded_files[0], "test.json");
}
#[test]
fn test_map_to_sourced_rumdl_config_fragment() {
let mut config_map = HashMap::new();
config_map.insert(
"line-length".to_string(),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
config_map.insert("MD025".to_string(), serde_yml::Value::Bool(false));
config_map.insert("MD026".to_string(), serde_yml::Value::Bool(true));
config_map.insert(
"MD003".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("style".to_string()),
serde_yml::Value::String("atx".to_string()),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
assert!(fragment.rules.contains_key("MD013"));
let md013_config = &fragment.rules["MD013"];
assert!(md013_config.values.contains_key("line-length"));
assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(120));
assert!(fragment.global.disable.value.contains(&"MD025".to_string()));
assert!(
!fragment.global.enable.value.contains(&"MD026".to_string()),
"Boolean true should be no-op when default is absent (treated as true)"
);
assert!(fragment.global.enable.value.is_empty());
assert!(fragment.rules.contains_key("MD003"));
let md003_config = &fragment.rules["MD003"];
assert!(md003_config.values.contains_key("style"));
}
#[test]
fn test_edge_cases() {
let mut config_map = HashMap::new();
let empty_config = MarkdownlintConfig(HashMap::new());
let sourced = empty_config.map_to_sourced_rumdl_config(None);
assert!(sourced.rules.is_empty());
config_map.insert("unknown-rule".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD999".to_string(), serde_yml::Value::Bool(true));
let config = MarkdownlintConfig(config_map);
let sourced = config.map_to_sourced_rumdl_config(None);
assert!(sourced.rules.is_empty()); }
#[test]
fn test_complex_rule_configurations() {
let mut config_map = HashMap::new();
config_map.insert(
"MD044".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("names".to_string()),
serde_yml::Value::Sequence(vec![
serde_yml::Value::String("JavaScript".to_string()),
serde_yml::Value::String("GitHub".to_string()),
]),
);
map
}),
);
config_map.insert(
"MD003".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("style".to_string()),
serde_yml::Value::String("atx".to_string()),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let sourced = mdl_config.map_to_sourced_rumdl_config(None);
assert!(sourced.rules.contains_key("MD044"));
let md044_config = &sourced.rules["MD044"];
assert!(md044_config.values.contains_key("names"));
assert!(sourced.rules.contains_key("MD003"));
let md003_config = &sourced.rules["MD003"];
assert!(md003_config.values.contains_key("style"));
assert_eq!(
md003_config.values["style"].value,
toml::Value::String("atx".to_string())
);
}
#[test]
fn test_value_types() {
let mut config_map = HashMap::new();
config_map.insert(
"MD007".to_string(),
serde_yml::Value::Number(serde_yml::Number::from(4)),
); config_map.insert(
"MD009".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("br_spaces".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(2)),
);
map.insert(
serde_yml::Value::String("strict".to_string()),
serde_yml::Value::Bool(true),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let sourced = mdl_config.map_to_sourced_rumdl_config(None);
assert!(sourced.rules.contains_key("MD007"));
assert!(sourced.rules["MD007"].values.contains_key("value"));
assert!(sourced.rules.contains_key("MD009"));
let md009_config = &sourced.rules["MD009"];
assert!(md009_config.values.contains_key("br-spaces"));
assert!(md009_config.values.contains_key("strict"));
}
#[test]
fn test_all_rule_aliases() {
let aliases = vec![
("heading-increment", "MD001"),
("heading-style", "MD003"),
("ul-style", "MD004"),
("list-indent", "MD005"),
("ul-indent", "MD007"),
("no-trailing-spaces", "MD009"),
("no-hard-tabs", "MD010"),
("no-reversed-links", "MD011"),
("no-multiple-blanks", "MD012"),
("line-length", "MD013"),
("commands-show-output", "MD014"),
("no-missing-space-atx", "MD018"),
("no-multiple-space-atx", "MD019"),
("no-missing-space-closed-atx", "MD020"),
("no-multiple-space-closed-atx", "MD021"),
("blanks-around-headings", "MD022"),
("heading-start-left", "MD023"),
("no-duplicate-heading", "MD024"),
("single-title", "MD025"),
("single-h1", "MD025"),
("no-trailing-punctuation", "MD026"),
("no-multiple-space-blockquote", "MD027"),
("no-blanks-blockquote", "MD028"),
("ol-prefix", "MD029"),
("list-marker-space", "MD030"),
("blanks-around-fences", "MD031"),
("blanks-around-lists", "MD032"),
("no-inline-html", "MD033"),
("no-bare-urls", "MD034"),
("hr-style", "MD035"),
("no-emphasis-as-heading", "MD036"),
("no-space-in-emphasis", "MD037"),
("no-space-in-code", "MD038"),
("no-space-in-links", "MD039"),
("fenced-code-language", "MD040"),
("first-line-heading", "MD041"),
("first-line-h1", "MD041"),
("no-empty-links", "MD042"),
("required-headings", "MD043"),
("proper-names", "MD044"),
("no-alt-text", "MD045"),
("code-block-style", "MD046"),
("single-trailing-newline", "MD047"),
("code-fence-style", "MD048"),
("emphasis-style", "MD049"),
("strong-style", "MD050"),
("link-fragments", "MD051"),
("reference-links-images", "MD052"),
("link-image-reference-definitions", "MD053"),
("link-image-style", "MD054"),
("table-pipe-style", "MD055"),
("table-column-count", "MD056"),
("existing-relative-links", "MD057"),
("blanks-around-tables", "MD058"),
("descriptive-link-text", "MD059"),
("table-cell-alignment", "MD060"),
("table-format", "MD060"),
("forbidden-terms", "MD061"),
("nested-code-fence", "MD070"),
("blank-line-after-frontmatter", "MD071"),
("frontmatter-key-sort", "MD072"),
];
for (alias, expected) in aliases {
assert_eq!(
markdownlint_to_rumdl_rule_key(alias),
Some(expected),
"Alias {alias} should map to {expected}"
);
}
}
#[test]
fn test_default_true_with_boolean_rules() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
config_map.insert(
"MD013".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
assert!(
fragment.global.enable.value.is_empty(),
"Enable list should be empty when default: true"
);
assert!(fragment.global.disable.value.is_empty(), "Disable list should be empty");
assert!(fragment.rules.contains_key("MD013"));
assert_eq!(
fragment.rules["MD013"].values["line-length"].value,
toml::Value::Integer(120)
);
}
#[test]
fn test_default_false_with_boolean_and_config_rules() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
config_map.insert(
"MD013".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
let mut enabled_sorted = fragment.global.enable.value.clone();
enabled_sorted.sort();
assert_eq!(
enabled_sorted,
vec!["MD001", "MD013"],
"Both boolean-true and config-object rules should be in enable list"
);
assert!(fragment.global.disable.value.is_empty(), "No rules should be disabled");
assert!(fragment.rules.contains_key("MD013"));
assert_eq!(
fragment.rules["MD013"].values["line-length"].value,
toml::Value::Integer(120)
);
}
#[test]
fn test_default_absent_with_boolean_rules() {
let mut config_map = HashMap::new();
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD009".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
assert!(
fragment.global.enable.value.is_empty(),
"Enable list should be empty when default is absent"
);
assert_eq!(fragment.global.disable.value, vec!["MD009"]);
}
#[test]
fn test_default_false_only_booleans() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD009".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
assert_eq!(fragment.global.enable.value, vec!["MD001"]);
assert!(
fragment.global.disable.value.is_empty(),
"Disable list should be empty when default: false (false is no-op)"
);
}
#[test]
fn test_default_true_with_boolean_rules_legacy() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
config_map.insert("MD009".to_string(), serde_yml::Value::Bool(false));
config_map.insert(
"MD013".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
assert!(sourced.global.enable.value.is_empty());
assert_eq!(sourced.global.disable.value, vec!["MD009"]);
assert!(sourced.rules.contains_key("MD013"));
assert_eq!(
sourced.rules["MD013"].values["line-length"].value,
toml::Value::Integer(120)
);
}
#[test]
fn test_default_false_with_config_rules_legacy() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(true));
config_map.insert(
"MD013".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));
let mut enabled_sorted = sourced.global.enable.value.clone();
enabled_sorted.sort();
assert_eq!(enabled_sorted, vec!["MD001", "MD013"]);
assert!(sourced.global.disable.value.is_empty());
}
#[test]
fn test_default_false_no_rules_disables_everything() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
assert!(fragment.global.enable.value.is_empty());
assert_eq!(
fragment.global.enable.source,
crate::config::ConfigSource::ProjectConfig,
"Enable source should be ProjectConfig when default: false"
);
}
#[test]
fn test_default_false_only_false_rules_disables_everything() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
config_map.insert("MD001".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));
assert!(fragment.global.enable.value.is_empty());
assert_eq!(
fragment.global.enable.source,
crate::config::ConfigSource::ProjectConfig,
);
}
#[test]
fn test_import_preserves_aliases_in_rules() {
let mut config_map = HashMap::new();
config_map.insert(
"line-length".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
config_map.insert("no-bare-urls".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
}
#[test]
fn test_import_preserves_canonical_ids() {
let mut config_map = HashMap::new();
config_map.insert(
"MD013".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
config_map.insert("MD034".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "MD013");
assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
assert!(fragment.global.disable.value.contains(&"MD034".to_string()));
}
#[test]
fn test_import_mixed_aliases_and_ids() {
let mut config_map = HashMap::new();
config_map.insert(
"line-length".to_string(),
serde_yml::Value::Mapping({
let mut map = serde_yml::Mapping::new();
map.insert(
serde_yml::Value::String("line_length".to_string()),
serde_yml::Value::Number(serde_yml::Number::from(120)),
);
map
}),
);
config_map.insert("MD034".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
}
#[test]
fn test_import_disable_list_uses_aliases() {
let mut config_map = HashMap::new();
config_map.insert("line-length".to_string(), serde_yml::Value::Bool(false));
config_map.insert("no-bare-urls".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
let mut disable_sorted = fragment.global.disable.value.clone();
disable_sorted.sort();
assert_eq!(disable_sorted, vec!["line-length", "no-bare-urls"]);
}
#[test]
fn test_import_enable_list_uses_aliases_when_default_false() {
let mut config_map = HashMap::new();
config_map.insert("default".to_string(), serde_yml::Value::Bool(false));
config_map.insert("line-length".to_string(), serde_yml::Value::Bool(true));
config_map.insert("no-bare-urls".to_string(), serde_yml::Value::Bool(true));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
let mut enable_sorted = fragment.global.enable.value.clone();
enable_sorted.sort();
assert_eq!(enable_sorted, vec!["line-length", "no-bare-urls"]);
}
#[test]
fn test_import_underscore_aliases_normalized_to_kebab() {
let mut config_map = HashMap::new();
config_map.insert("no_bare_urls".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
assert!(fragment.global.disable.value.contains(&"no-bare-urls".to_string()));
}
#[test]
fn test_import_case_insensitive_alias_preserved_lowercase() {
let mut config_map = HashMap::new();
config_map.insert("Line-Length".to_string(), serde_yml::Value::Bool(false));
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
}
}