use colored::*;
use std::fs;
use std::path::Path;
use clap::ValueEnum;
use rumdl_lib::exit_codes::exit;
#[derive(Clone, Default, ValueEnum)]
pub enum Format {
#[default]
Toml,
Json,
}
pub fn handle_import(file: String, output: Option<String>, format: Format, dry_run: bool) {
use rumdl_lib::markdownlint_config;
let ml_config = match markdownlint_config::load_markdownlint_config(&file) {
Ok(config) => config,
Err(e) => {
eprintln!("{}: {}", "Import error".red().bold(), e);
exit::tool_error();
}
};
let fragment = ml_config.map_to_sourced_rumdl_config_fragment(Some(&file));
let is_pyproject = output
.as_ref()
.is_some_and(|p| p.ends_with("pyproject.toml") || p == "pyproject.toml");
let output_content = match format {
Format::Toml => generate_toml_output(&fragment, is_pyproject),
Format::Json => generate_json_output(&fragment),
};
if dry_run {
print!("{output_content}");
} else {
let output_path = output.as_deref().unwrap_or(match format {
Format::Json => "rumdl-config.json",
Format::Toml => ".rumdl.toml",
});
if Path::new(output_path).exists() {
eprintln!("{}: Output file '{}' already exists", "Error".red().bold(), output_path);
exit::tool_error();
}
match fs::write(output_path, output_content) {
Ok(()) => {
println!("Converted markdownlint config from '{file}' to '{output_path}'");
println!("You can now use: rumdl check --config {output_path} .");
}
Err(e) => {
eprintln!("{}: Failed to write to '{}': {}", "Error".red().bold(), output_path, e);
exit::tool_error();
}
}
}
}
pub(crate) fn generate_toml_output(fragment: &rumdl_lib::config::SourcedConfigFragment, is_pyproject: bool) -> String {
let mut output = String::new();
let section_prefix = if is_pyproject { "tool.rumdl." } else { "" };
if !fragment.global.enable.value.is_empty()
|| !fragment.global.disable.value.is_empty()
|| !fragment.global.exclude.value.is_empty()
|| !fragment.global.include.value.is_empty()
|| fragment.global.line_length.value.get() != 80
{
output.push_str(&format!("[{section_prefix}global]\n"));
if !fragment.global.enable.value.is_empty() {
output.push_str(&format!("enable = {:?}\n", fragment.global.enable.value));
}
if !fragment.global.disable.value.is_empty() {
output.push_str(&format!("disable = {:?}\n", fragment.global.disable.value));
}
if !fragment.global.exclude.value.is_empty() {
output.push_str(&format!("exclude = {:?}\n", fragment.global.exclude.value));
}
if !fragment.global.include.value.is_empty() {
output.push_str(&format!("include = {:?}\n", fragment.global.include.value));
}
if fragment.global.line_length.value.get() != 80 {
output.push_str(&format!("line_length = {}\n", fragment.global.line_length.value.get()));
}
output.push('\n');
}
for (rule_name, rule_config) in &fragment.rules {
if !rule_config.values.is_empty() {
let display = fragment
.rule_display_names
.get(rule_name)
.map(String::as_str)
.unwrap_or(rule_name);
output.push_str(&format!("[{section_prefix}{display}]\n"));
for (key, sourced_value) in &rule_config.values {
if key == "value" && rule_config.values.len() > 1 {
continue;
}
format_toml_value_line(&mut output, key, &sourced_value.value);
}
output.push('\n');
}
}
let trimmed = output.trim_end_matches('\n');
let mut result = trimmed.to_string();
result.push('\n');
result
}
fn format_toml_value_line(output: &mut String, key: &str, value: &toml::Value) {
match value {
toml::Value::String(s) => output.push_str(&format!("{key} = \"{s}\"\n")),
toml::Value::Integer(i) => output.push_str(&format!("{key} = {i}\n")),
toml::Value::Float(f) => output.push_str(&format!("{key} = {f}\n")),
toml::Value::Boolean(b) => output.push_str(&format!("{key} = {b}\n")),
toml::Value::Array(arr) => {
let arr_str = arr
.iter()
.map(|v| match v {
toml::Value::String(s) => format!("\"{s}\""),
_ => format!("{v}"),
})
.collect::<Vec<_>>()
.join(", ");
output.push_str(&format!("{key} = [{arr_str}]\n"));
}
_ => {
if let Ok(toml_str) = toml::to_string_pretty(value) {
let clean_value = toml_str.trim();
if !clean_value.starts_with('[') {
output.push_str(&format!("{key} = {clean_value}"));
} else {
output.push_str(&format!("{key} = {value:?}\n"));
}
} else {
output.push_str(&format!("{key} = {value:?}\n"));
}
}
}
}
pub(crate) fn generate_json_output(fragment: &rumdl_lib::config::SourcedConfigFragment) -> String {
let mut json_config = serde_json::Map::new();
if !fragment.global.enable.value.is_empty()
|| !fragment.global.disable.value.is_empty()
|| !fragment.global.exclude.value.is_empty()
|| !fragment.global.include.value.is_empty()
|| fragment.global.line_length.value.get() != 80
{
let mut global = serde_json::Map::new();
if !fragment.global.enable.value.is_empty() {
global.insert(
"enable".to_string(),
serde_json::Value::Array(
fragment
.global
.enable
.value
.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect(),
),
);
}
if !fragment.global.disable.value.is_empty() {
global.insert(
"disable".to_string(),
serde_json::Value::Array(
fragment
.global
.disable
.value
.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect(),
),
);
}
if !fragment.global.exclude.value.is_empty() {
global.insert(
"exclude".to_string(),
serde_json::Value::Array(
fragment
.global
.exclude
.value
.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect(),
),
);
}
if !fragment.global.include.value.is_empty() {
global.insert(
"include".to_string(),
serde_json::Value::Array(
fragment
.global
.include
.value
.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect(),
),
);
}
if fragment.global.line_length.value.get() != 80 {
global.insert(
"line_length".to_string(),
serde_json::Value::Number(serde_json::Number::from(fragment.global.line_length.value.get())),
);
}
json_config.insert("global".to_string(), serde_json::Value::Object(global));
}
for (rule_name, rule_config) in &fragment.rules {
if !rule_config.values.is_empty() {
let mut rule_obj = serde_json::Map::new();
for (key, sourced_value) in &rule_config.values {
if let Ok(json_value) = serde_json::to_value(&sourced_value.value) {
rule_obj.insert(key.clone(), json_value);
}
}
let display = fragment
.rule_display_names
.get(rule_name)
.map(String::as_str)
.unwrap_or(rule_name);
json_config.insert(display.to_string(), serde_json::Value::Object(rule_obj));
}
}
let mut json = serde_json::to_string_pretty(&json_config).unwrap_or_else(|e| {
eprintln!("{}: Failed to serialize to JSON: {}", "Error".red().bold(), e);
exit::tool_error();
});
json.push('\n');
json
}
#[cfg(test)]
mod tests {
use super::*;
use rumdl_lib::markdownlint_config::MarkdownlintConfig;
use std::collections::HashMap;
#[test]
fn test_generate_toml_output_with_display_names() {
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
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
let toml_output = generate_toml_output(&fragment, false);
assert!(
toml_output.contains("[line-length]"),
"TOML output should use alias 'line-length', got:\n{toml_output}"
);
assert!(
!toml_output.contains("[MD013]"),
"TOML output should NOT contain canonical ID 'MD013', got:\n{toml_output}"
);
}
#[test]
fn test_generate_toml_output_with_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
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
let toml_output = generate_toml_output(&fragment, false);
assert!(
toml_output.contains("[MD013]"),
"TOML output should use canonical ID 'MD013', got:\n{toml_output}"
);
}
#[test]
fn test_generate_toml_output_pyproject_with_display_names() {
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
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
let toml_output = generate_toml_output(&fragment, true);
assert!(
toml_output.contains("[tool.rumdl.line-length]"),
"pyproject TOML output should use alias with prefix, got:\n{toml_output}"
);
}
#[test]
fn test_generate_json_output_with_display_names() {
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
}),
);
let mdl_config = MarkdownlintConfig(config_map);
let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));
let json_output = generate_json_output(&fragment);
assert!(
json_output.contains("\"line-length\""),
"JSON output should use alias 'line-length', got:\n{json_output}"
);
assert!(
!json_output.contains("\"MD013\""),
"JSON output should NOT contain canonical ID 'MD013', got:\n{json_output}"
);
}
#[test]
fn test_generate_toml_output_disable_list_with_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 toml_output = generate_toml_output(&fragment, false);
assert!(
toml_output.contains("line-length") && toml_output.contains("no-bare-urls"),
"TOML disable list should use aliases, got:\n{toml_output}"
);
assert!(
!toml_output.contains("MD013") && !toml_output.contains("MD034"),
"TOML disable list should NOT contain canonical IDs, got:\n{toml_output}"
);
}
#[test]
fn test_generate_json_output_disable_list_with_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 json_output = generate_json_output(&fragment);
assert!(
json_output.contains("line-length") && json_output.contains("no-bare-urls"),
"JSON disable list should use aliases, got:\n{json_output}"
);
assert!(
!json_output.contains("MD013") && !json_output.contains("MD034"),
"JSON disable list should NOT contain canonical IDs, got:\n{json_output}"
);
}
}