use rumdl_lib::config::{Config, RuleConfig, normalize_key};
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::*;
use std::collections::BTreeMap;
use std::fs;
fn create_test_config() -> Config {
let mut config = Config::default();
let mut md013_values = BTreeMap::new();
md013_values.insert(normalize_key("line_length"), toml::Value::Integer(120));
md013_values.insert(normalize_key("code_blocks"), toml::Value::Boolean(false));
md013_values.insert(normalize_key("headings"), toml::Value::Boolean(true));
let md013_config = RuleConfig {
severity: None,
values: md013_values,
};
config.rules.insert(normalize_key("MD013"), md013_config);
let mut md004_values = BTreeMap::new();
md004_values.insert(normalize_key("style"), toml::Value::String("asterisk".to_string()));
let md004_config = RuleConfig {
severity: None,
values: md004_values,
};
config.rules.insert(normalize_key("MD004"), md004_config);
config
}
fn apply_rule_configs(rules_in: &Vec<Box<dyn Rule>>, config: &Config) -> Vec<Box<dyn Rule>> {
let mut rules_out: Vec<Box<dyn Rule>> = Vec::with_capacity(rules_in.len());
for rule_instance in rules_in {
let rule_name = rule_instance.name();
if rule_name == "MD013" {
let line_length = rumdl_lib::config::get_rule_config_value::<u64>(config, "MD013", "line_length")
.map_or(80, |v| v as usize);
let code_blocks =
rumdl_lib::config::get_rule_config_value::<bool>(config, "MD013", "code_blocks").unwrap_or(true);
let tables = rumdl_lib::config::get_rule_config_value::<bool>(config, "MD013", "tables").unwrap_or(false);
let headings =
rumdl_lib::config::get_rule_config_value::<bool>(config, "MD013", "headings").unwrap_or(true);
let strict = rumdl_lib::config::get_rule_config_value::<bool>(config, "MD013", "strict").unwrap_or(false);
rules_out.push(Box::new(MD013LineLength::new(
line_length,
code_blocks,
tables,
headings,
strict,
)));
continue; }
if rule_name == "MD004" {
let style = rumdl_lib::config::get_rule_config_value::<String>(config, "MD004", "style")
.unwrap_or_else(|| "consistent".to_string());
let ul_style = match style.as_str() {
"asterisk" => rumdl_lib::rules::md004_unordered_list_style::UnorderedListStyle::Asterisk,
"plus" => rumdl_lib::rules::md004_unordered_list_style::UnorderedListStyle::Plus,
"dash" => rumdl_lib::rules::md004_unordered_list_style::UnorderedListStyle::Dash,
_ => rumdl_lib::rules::md004_unordered_list_style::UnorderedListStyle::Consistent,
};
rules_out.push(Box::new(MD004UnorderedListStyle::new(ul_style)));
continue; }
rules_out.push(rule_instance.clone());
}
rules_out }
#[test]
fn test_apply_rule_configs() {
let initial_rules = rumdl_lib::rules::all_rules(&rumdl_lib::config::Config::default());
let config = create_test_config();
let configured_rules = apply_rule_configs(&initial_rules, &config);
let test_content = r#"# Heading
This is a line that exceeds the default 80 characters but is less than the configured 120 characters.
* Item 1
- Item 2
+ Item 3
"#;
let warnings = rumdl_lib::lint(
test_content,
&configured_rules,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(md013_warnings, 0, "MD013 should not trigger with line_length 120");
let md004_warnings: Vec<_> = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD004"))
.collect();
assert_eq!(
md004_warnings.len(),
2,
"MD004 should trigger for all unordered list items with non-asterisk markers in explicit style mode, matching markdownlint"
);
let md001_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD001"))
.count();
assert_eq!(
md001_warnings,
0, "MD001 should not trigger on this content"
);
}
#[test]
fn test_config_priority() {
let initial_rules = rumdl_lib::rules::all_rules(&rumdl_lib::config::Config::default());
let mut config = create_test_config();
let configured_rules_1 = apply_rule_configs(&initial_rules, &config);
let line_100_chars = "# Test
"
.to_owned()
+ &"ab ".repeat(32)
+ "abcd";
let warnings = rumdl_lib::lint(
&line_100_chars,
&configured_rules_1,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(
md013_warnings, 0,
"MD013 should not trigger with configured line_length 120"
);
let mut md013_values = BTreeMap::new();
md013_values.insert(normalize_key("line_length"), toml::Value::Integer(50));
let md013_config = RuleConfig {
severity: None,
values: md013_values,
};
config.rules.insert(normalize_key("MD013"), md013_config);
let configured_rules_2 = apply_rule_configs(&initial_rules, &config);
let warnings = rumdl_lib::lint(
&line_100_chars,
&configured_rules_2,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(md013_warnings, 1, "MD013 should trigger with configured line_length 50");
}
#[test]
fn test_partial_rule_config() {
let initial_rules = rumdl_lib::rules::all_rules(&rumdl_lib::config::Config::default());
let mut rules_map = BTreeMap::new();
let mut md013_values = BTreeMap::new();
md013_values.insert(normalize_key("line_length"), toml::Value::Integer(100));
let md013_config = RuleConfig {
severity: None,
values: md013_values,
};
rules_map.insert(normalize_key("MD013"), md013_config);
let mut config = Config::default();
config.rules = rules_map;
let configured_rules_1 = apply_rule_configs(&initial_rules, &config);
let test_content =
"This is a regular line that is longer than 80 characters but shorter than 100 characters in length.";
let warnings = rumdl_lib::lint(
test_content,
&configured_rules_1,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(md013_warnings, 0, "MD013 should not trigger with line_length 100");
let mut rules_map = BTreeMap::new();
let mut md013_values = BTreeMap::new();
md013_values.insert(normalize_key("line_length"), toml::Value::Integer(60));
let md013_config = RuleConfig {
severity: None,
values: md013_values,
};
rules_map.insert(normalize_key("MD013"), md013_config);
let mut config = Config::default();
config.rules = rules_map;
let configured_rules_2 = apply_rule_configs(&initial_rules, &config);
let warnings = rumdl_lib::lint(
test_content,
&configured_rules_2,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(md013_warnings, 1, "MD013 should trigger with line_length 60");
}
#[test]
fn test_config_enable_disable() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("enable_disable_config.toml");
let config_content_1 = r#"
[global]
disable = ["md001"] # Use normalized key
[MD013]
line_length = 20
"#;
fs::write(&config_path, config_content_1).expect("Failed to write config 1");
let config_path_str = config_path.to_str().expect("Path is valid UTF-8");
let sourced_config_1 = rumdl_lib::config::SourcedConfig::load_with_discovery(Some(config_path_str), None, true)
.expect("Failed to load config 1");
let config_1: Config = sourced_config_1.into_validated_unchecked().into();
let test_content = r#"
# Heading 1
### Heading 3
This line exceeds 20 characters.
"#;
let initial_rules_1 = rumdl_lib::rules::all_rules(&rumdl_lib::config::Config::default());
let configured_rules_1 = apply_rule_configs(&initial_rules_1, &config_1);
let warnings_1 = rumdl_lib::lint(
test_content,
&configured_rules_1,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md001_warnings_1 = warnings_1
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD001"))
.count();
assert_eq!(
md001_warnings_1, 1,
"MD001 should run and trigger (filtering not tested)"
);
let md013_warnings_1 = warnings_1
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(md013_warnings_1, 1, "MD013 should trigger once with line_length 20");
let config_content_2 = r#"
[global]
enable = ["md013"] # Use normalized key
[MD013]
line_length = 20 # Set a low limit to trigger it
"#;
fs::write(&config_path, config_content_2).expect("Failed to write config 2");
let sourced_config_2 = rumdl_lib::config::SourcedConfig::load_with_discovery(Some(config_path_str), None, true)
.expect("Failed to load config 2");
let config_2: Config = sourced_config_2.into_validated_unchecked().into();
let initial_rules_2 = rumdl_lib::rules::all_rules(&rumdl_lib::config::Config::default());
let configured_rules_2 = apply_rule_configs(&initial_rules_2, &config_2);
let warnings_2 = rumdl_lib::lint(
test_content,
&configured_rules_2,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings_2 = warnings_2
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.count();
assert_eq!(
md013_warnings_2, 1,
"MD013 should trigger once with line_length 20 (enable doesn't affect application)"
);
let md001_warnings_2 = warnings_2
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD001"))
.count();
assert_eq!(
md001_warnings_2, 1,
"MD001 should run and trigger (filtering not tested)"
);
}
#[test]
fn test_disable_all_override() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("disable_all_config.toml");
let config_content = r#"
[global]
disable = ["all"]
[MD013]
line_length = 10
headings = true
"#;
fs::write(&config_path, config_content).expect("Failed to write config");
let config_path_str = config_path.to_str().expect("Path is valid UTF-8");
let sourced_config = rumdl_lib::config::SourcedConfig::load_with_discovery(Some(config_path_str), None, true)
.expect("Failed to load config");
let config: Config = sourced_config.into_validated_unchecked().into();
let initial_rules = rumdl_lib::rules::all_rules(&rumdl_lib::config::Config::default());
let configured_rules = apply_rule_configs(&initial_rules, &config);
let test_content = r#"
# Heading 1
### Heading 3
This line > 10.
"#;
let warnings = rumdl_lib::lint(
test_content,
&configured_rules,
false,
rumdl_lib::config::MarkdownFlavor::Standard,
None,
None,
)
.expect("Linting should succeed");
let md013_warnings = warnings
.iter()
.filter(|w| w.rule_name.as_deref() == Some("MD013"))
.collect::<Vec<_>>();
assert_eq!(
md013_warnings.len(),
3, "MD013 should trigger 3 times with line_length 10 (disable=all doesn't affect application)"
);
}