#![allow(clippy::multiple_crate_versions)]
use anyhow::{Context, Result};
use beautiful_md::{format_file, format_markdown, Config};
use std::fs;
use std::io::{self, Write};
use std::path::Path;
mod cli;
use cli::{Cli, Commands};
fn main() -> Result<()> {
let args = Cli::parse_args();
let config = if let Some(config_path) = &args.config {
Config::from_file(config_path).with_context(|| {
format!(
"Failed to load config from {}\n\nHint: If you wanted to generate a config file, use:\n beautiful-md config",
config_path.display()
)
})?
} else {
Config::load_default()
};
if let Some(command) = args.command {
return handle_subcommand(command, &config);
}
if args.files.is_empty() {
anyhow::bail!("No input files specified. Use --help for usage information.");
}
let files = if args.glob {
expand_glob_patterns(&args.files)?
} else {
args.files.clone()
};
if args.check {
return check_files(&files, &config);
}
if args.dry_run {
return dry_run_files(&files, &config);
}
if args.in_place {
format_files_in_place(&files, &config, args.verbose)?;
} else if let Some(output_path) = &args.output {
if files.len() > 1 {
anyhow::bail!("Cannot specify --output with multiple input files");
}
format_to_file(&files[0], output_path, &config)?;
} else {
format_to_stdout(&files, &config)?;
}
Ok(())
}
fn handle_subcommand(command: Commands, config: &Config) -> Result<()> {
match command {
Commands::Format { files, in_place } => {
if in_place {
format_files_in_place(&files, config, false)?;
} else {
format_to_stdout(&files, config)?;
}
}
Commands::Config { output } => {
config
.save(&output)
.with_context(|| format!("Failed to write config to {}", output.display()))?;
println!("Configuration written to {}", output.display());
}
Commands::Check { files } => {
return check_files(&files, config);
}
}
Ok(())
}
fn format_files_in_place(
files: &[std::path::PathBuf],
config: &Config,
verbose: bool,
) -> Result<()> {
for file in files {
if verbose {
println!("Formatting {}", file.display());
}
let diagnostics = format_file(file, config)
.with_context(|| format!("Failed to format {}", file.display()))?;
if !diagnostics.is_empty() {
eprintln!("\n{}:", file.display());
diagnostics.print_to_stderr();
}
}
Ok(())
}
fn format_to_file(input: &Path, output: &Path, config: &Config) -> Result<()> {
let content =
fs::read_to_string(input).with_context(|| format!("Failed to read {}", input.display()))?;
let (formatted, diagnostics) = format_markdown(&content, config)
.with_context(|| format!("Failed to format {}", input.display()))?;
fs::write(output, formatted)
.with_context(|| format!("Failed to write to {}", output.display()))?;
if !diagnostics.is_empty() {
eprintln!("\n{}:", input.display());
diagnostics.print_to_stderr();
}
Ok(())
}
fn format_to_stdout(files: &[std::path::PathBuf], config: &Config) -> Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock();
for file in files {
let content = fs::read_to_string(file)
.with_context(|| format!("Failed to read {}", file.display()))?;
let (formatted, diagnostics) = format_markdown(&content, config)
.with_context(|| format!("Failed to format {}", file.display()))?;
writeln!(handle, "{formatted}").context("Failed to write to stdout")?;
if !diagnostics.is_empty() {
drop(handle); eprintln!("\n{}:", file.display());
diagnostics.print_to_stderr();
handle = stdout.lock(); }
}
Ok(())
}
fn check_files(files: &[std::path::PathBuf], config: &Config) -> Result<()> {
let mut needs_formatting = Vec::new();
for file in files {
let content = fs::read_to_string(file)
.with_context(|| format!("Failed to read {}", file.display()))?;
let (formatted, _diagnostics) = format_markdown(&content, config)
.with_context(|| format!("Failed to format {}", file.display()))?;
if content != formatted {
needs_formatting.push(file.clone());
}
}
if needs_formatting.is_empty() {
println!("All files are properly formatted ✓");
Ok(())
} else {
eprintln!("The following files need formatting:");
for file in &needs_formatting {
eprintln!(" - {}", file.display());
}
anyhow::bail!("{} file(s) need formatting", needs_formatting.len());
}
}
fn dry_run_files(files: &[std::path::PathBuf], config: &Config) -> Result<()> {
let mut total_issues = 0;
for file in files {
let content = fs::read_to_string(file)
.with_context(|| format!("Failed to read {}", file.display()))?;
let (_formatted, diagnostics) = format_markdown(&content, config)
.with_context(|| format!("Failed to analyze {}", file.display()))?;
println!("\n📄 {}", file.display());
if diagnostics.is_empty() {
println!(" ✅ No issues found");
} else {
total_issues += diagnostics.len();
diagnostics.print_to_stderr();
}
}
println!("\n{}", "=".repeat(50));
if total_issues == 0 {
println!("✅ All files are clean! No issues found.");
} else {
println!(
"⚠️ Found {} issue(s) across {} file(s)",
total_issues,
files.len()
);
println!("\nRun without --dry-run to fix these issues automatically.");
}
Ok(())
}
fn expand_glob_patterns(patterns: &[std::path::PathBuf]) -> Result<Vec<std::path::PathBuf>> {
let mut files = Vec::new();
for pattern in patterns {
let pattern_str = pattern.to_str().context("Invalid UTF-8 in glob pattern")?;
for entry in glob::glob(pattern_str)
.with_context(|| format!("Invalid glob pattern: {pattern_str}"))?
{
let path = entry.with_context(|| "Failed to read glob entry".to_string())?;
if path.is_file() {
files.push(path);
}
}
}
if files.is_empty() {
anyhow::bail!("No files matched the specified pattern(s)");
}
Ok(files)
}