rumdl 0.2.60

A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)
Documentation
//! Shared CLI utility functions used across command handlers and watch mode.

use colored::*;
use core::error::Error;
use std::fs;
use std::path::Path;

use rumdl_lib::config as rumdl_config;
use rumdl_lib::exit_codes::exit;

use crate::CheckArgs;

/// Apply CLI argument overrides to a sourced config.
/// This centralizes the logic for CLI args overriding config values,
/// ensuring consistency between regular check and watch mode.
pub fn apply_cli_overrides(sourced: &mut rumdl_config::SourcedConfig, args: &CheckArgs) {
    // Apply --flavor override if provided
    if let Some(flavor) = args.flavor {
        sourced.global.flavor = rumdl_config::SourcedValue::new(flavor.into(), rumdl_config::ConfigSource::Cli);
    }

    // Apply --respect-gitignore override if provided
    // This allows CLI to override config file setting
    if let Some(respect_gitignore) = args.respect_gitignore {
        sourced.global.respect_gitignore =
            rumdl_config::SourcedValue::new(respect_gitignore, rumdl_config::ConfigSource::Cli);
    }

    // Apply --fixable override if provided
    if let Some(ref fixable) = args.fixable {
        let rules: Vec<String> = fixable
            .split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        sourced.global.fixable = rumdl_config::SourcedValue::new(rules, rumdl_config::ConfigSource::Cli);
    }

    // Apply --unfixable override if provided
    if let Some(ref unfixable) = args.unfixable {
        let rules: Vec<String> = unfixable
            .split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        sourced.global.unfixable = rumdl_config::SourcedValue::new(rules, rumdl_config::ConfigSource::Cli);
    }
}

/// Resolve the lint output format with the standard precedence:
/// CLI `--output-format` → `RUMDL_OUTPUT_FORMAT` env var → config
/// `output_format` → legacy `--output json` → `text`.
///
/// Returns the parse error message for an unrecognized format name so each
/// caller can report it through its own channel.
pub fn resolve_output_format(
    args: &CheckArgs,
    config: &rumdl_config::Config,
) -> Result<rumdl_lib::output::OutputFormat, String> {
    use std::str::FromStr;

    if let Some(fmt) = args.output_format {
        return Ok(fmt.into());
    }

    let env_output_format = std::env::var("RUMDL_OUTPUT_FORMAT").ok();
    let output_format_str = env_output_format
        .as_deref()
        .or(config.global.output_format.as_deref())
        .or({
            // Legacy support: map --output json to --output-format json
            match args.output {
                crate::cli_types::Output::Json => Some("json"),
                crate::cli_types::Output::Text => None,
            }
        })
        .unwrap_or("text");

    rumdl_lib::output::OutputFormat::from_str(output_format_str).map_err(|e| e.to_string())
}

/// Read file content as a UTF-8 string.
pub fn read_file_efficiently(path: &Path) -> Result<String, Box<dyn Error>> {
    fs::read_to_string(path).map_err(|e| format!("Failed to read file {}: {}", path.display(), e).into())
}

/// Load configuration with standard CLI error handling.
pub fn load_config_with_cli_error_handling(config_path: Option<&str>, isolated: bool) -> rumdl_config::SourcedConfig {
    load_config_with_cli_error_handling_with_dir(config_path, isolated, None)
}

/// Load configuration with standard CLI error handling, optionally using a discovery directory.
pub fn load_config_with_cli_error_handling_with_dir(
    config_path: Option<&str>,
    isolated: bool,
    discovery_dir: Option<&Path>,
) -> rumdl_config::SourcedConfig {
    let result = if let Some(dir) = discovery_dir {
        // Canonicalize config path before changing directory
        // Otherwise relative paths will be resolved from the wrong directory
        let absolute_config_path = config_path.map(|p| {
            let path = Path::new(p);
            if path.is_absolute() {
                p.to_string()
            } else if let Ok(canonical) = std::fs::canonicalize(path) {
                canonical.to_string_lossy().to_string()
            } else {
                // If file doesn't exist yet, make it absolute relative to current dir
                std::env::current_dir()
                    .map(|cwd| cwd.join(p).to_string_lossy().to_string())
                    .unwrap_or_else(|_| p.to_string())
            }
        });

        // Temporarily change working directory for config discovery
        let original_dir = std::env::current_dir().ok();

        // Change to the discovery directory if it exists
        if dir.is_dir() {
            let _ = std::env::set_current_dir(dir);
        } else if let Some(parent) = dir.parent() {
            let _ = std::env::set_current_dir(parent);
        }

        let config_result =
            rumdl_config::SourcedConfig::load_with_discovery(absolute_config_path.as_deref(), None, isolated);

        // Restore original directory
        if let Some(orig) = original_dir {
            let _ = std::env::set_current_dir(orig);
        }

        config_result
    } else {
        rumdl_config::SourcedConfig::load_with_discovery(config_path, None, isolated)
    };

    match result {
        Ok(config) => config,
        Err(e) => {
            eprintln!("{}: {}", "Config error".red().bold(), e);
            exit::tool_error();
        }
    }
}