mdcheck 0.1.0

A linter/validator for Markdown files that enforces CommonMark specification
Documentation
use clap::{Arg, Command};
use mdcheck::{check_files, CheckConfig, CheckResult, OutputFormat};
use std::path::PathBuf;
use std::process;

fn main() {
    let matches = Command::new("mdcheck")
        .version("0.1.0")
        .about("Linter/Validator for Markdown files that enforces CommonMark specification")
        .arg(
            Arg::new("files")
                .help("Markdown files or directories to check")
                .required(true)
                .num_args(1..),
        )
        .arg(
            Arg::new("recursive")
                .short('r')
                .long("recursive")
                .help("Recursively search directories for markdown files")
                .action(clap::ArgAction::SetTrue),
        )
        .arg(
            Arg::new("output-format")
                .short('o')
                .long("output-format")
                .help("Output format")
                .value_parser(["human", "json"])
                .default_value("human"),
        )
        .arg(
            Arg::new("strict")
                .short('s')
                .long("strict")
                .help("Enable strict mode (treat warnings as errors)")
                .action(clap::ArgAction::SetTrue),
        )
        .arg(
            Arg::new("ignore-warnings")
                .short('w')
                .long("ignore-warnings")
                .help("Ignore warnings, only show errors")
                .action(clap::ArgAction::SetTrue),
        )
        .get_matches();

    let files: Vec<String> = matches
        .get_many::<String>("files")
        .unwrap_or_default()
        .cloned()
        .collect();

    let recursive = matches.get_flag("recursive");
    let output_format = match matches.get_one::<String>("output-format").unwrap().as_str() {
        "json" => OutputFormat::Json,
        _ => OutputFormat::Human,
    };
    let strict = matches.get_flag("strict");
    let ignore_warnings = matches.get_flag("ignore-warnings");

    let config = CheckConfig {
        recursive,
        output_format: output_format.clone(),
        strict,
        ignore_warnings,
    };

    let all_paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();

    let results = check_files(&all_paths, &config);

    if matches!(output_format, OutputFormat::Human) {
        print_human_results(&results, &config);
    }

    // Determine exit code
    let has_errors = results.iter().any(|result| !result.errors.is_empty());
    let has_warnings = results.iter().any(|result| !result.warnings.is_empty());

    let should_fail = has_errors || (strict && has_warnings);

    if should_fail {
        process::exit(1);
    } else {
        process::exit(0);
    }
}

fn print_human_results(results: &[CheckResult], config: &CheckConfig) {
    use colored::*;

    let mut total_errors = 0;
    let mut total_warnings = 0;
    let mut files_with_issues = 0;

    for result in results {
        if result.errors.is_empty() && result.warnings.is_empty() {
            if !config.ignore_warnings {
                println!("{} {}", "".green(), result.file_path.display());
            }
            continue;
        }

        files_with_issues += 1;
        total_errors += result.errors.len();
        total_warnings += result.warnings.len();

        println!("\n{}:", result.file_path.display());

        for error in &result.errors {
            println!("  {} {}: {}", "error".red(), error.line, error.message);
            if let Some(context) = &error.context {
                println!("    {}", context.dimmed());
            }
        }

        if !config.ignore_warnings {
            for warning in &result.warnings {
                println!(
                    "  {} {}: {}",
                    "warning".yellow(),
                    warning.line,
                    warning.message
                );
                if let Some(context) = &warning.context {
                    println!("    {}", context.dimmed());
                }
            }
        }
    }

    if files_with_issues > 0 {
        println!("\nSummary:");
        if total_errors > 0 {
            print!(
                "{} {}",
                total_errors.to_string().red(),
                if total_errors == 1 { "error" } else { "errors" }
            );
        }
        if total_warnings > 0 && !config.ignore_warnings {
            if total_errors > 0 {
                print!(", ");
            }
            print!(
                "{} {}",
                total_warnings.to_string().yellow(),
                if total_warnings == 1 {
                    "warning"
                } else {
                    "warnings"
                }
            );
        }
        println!(
            " in {} file{}",
            files_with_issues,
            if files_with_issues == 1 { "" } else { "s" }
        );
    } else if !config.ignore_warnings || total_errors == 0 {
        println!("\n{} All markdown files are valid!", "".green());
    }
}