use clap::{Parser, ValueEnum};
use glob::glob;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum WrapMode {
Always,
Never,
#[default]
Preserve,
}
impl From<WrapMode> for crate::formatter::WrapMode {
fn from(mode: WrapMode) -> Self {
match mode {
WrapMode::Always => Self::Always,
WrapMode::Never => Self::Never,
WrapMode::Preserve => Self::Preserve,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum OrderedListMode {
#[default]
Ascending,
One,
}
impl From<OrderedListMode> for crate::formatter::OrderedListMode {
fn from(mode: OrderedListMode) -> Self {
match mode {
OrderedListMode::Ascending => Self::Ascending,
OrderedListMode::One => Self::One,
}
}
}
const DEFAULT_EXCLUDES: &[&str] = &["node_modules", "target", ".git", "vendor", "dist", "build"];
#[derive(Parser, Debug)]
#[command(name = "mdfmt")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Fast, opinionated Markdown formatter", long_about = None)]
pub struct Args {
#[arg(value_name = "PATH")]
pub paths: Vec<String>,
#[arg(short, long)]
pub write: bool,
#[arg(long)]
pub check: bool,
#[arg(long)]
pub stdin: bool,
#[arg(long, default_value = "80")]
pub width: usize,
#[arg(long, value_enum, default_value = "preserve")]
pub wrap: WrapMode,
#[arg(long = "ordered-list", value_enum, default_value = "ascending")]
pub ordered_list: OrderedListMode,
#[arg(long = "exclude", value_name = "DIR")]
pub excludes: Vec<String>,
#[arg(long)]
pub no_default_excludes: bool,
}
impl Args {
fn get_excludes(&self) -> Vec<String> {
let mut excludes: Vec<String> = if self.no_default_excludes {
Vec::new()
} else {
DEFAULT_EXCLUDES.iter().map(|s| s.to_string()).collect()
};
excludes.extend(self.excludes.clone());
excludes
}
fn should_exclude(&self, path: &std::path::Path, excludes: &[String]) -> bool {
for component in path.components() {
if let std::path::Component::Normal(name) = component {
let name_str = name.to_string_lossy();
if excludes.iter().any(|e| e == name_str.as_ref()) {
return true;
}
}
}
false
}
pub fn get_input_sources(&self) -> Result<Vec<InputSource>, String> {
if self.stdin || (self.paths.len() == 1 && self.paths[0] == "-") {
return Ok(vec![InputSource::Stdin]);
}
if self.paths.is_empty() {
return Err("No input provided. Use --stdin or specify file paths.".to_string());
}
let excludes = self.get_excludes();
let mut sources = Vec::new();
for pattern in &self.paths {
let path = PathBuf::from(pattern);
if path.is_dir() {
let glob_pattern = format!("{}/**/*.md", pattern);
self.collect_markdown_files(&glob_pattern, &mut sources, &excludes)?;
} else if path.is_file() {
if Self::is_markdown_file(&path) {
sources.push(InputSource::File(path));
} else {
return Err(format!(
"File '{}' is not a markdown file (.md)",
path.display()
));
}
} else {
self.collect_markdown_files(pattern, &mut sources, &excludes)?;
}
}
if sources.is_empty() {
return Err("No markdown files found.".to_string());
}
Ok(sources)
}
fn collect_markdown_files(
&self,
pattern: &str,
sources: &mut Vec<InputSource>,
excludes: &[String],
) -> Result<(), String> {
let entries =
glob(pattern).map_err(|e| format!("Invalid glob pattern '{}': {}", pattern, e))?;
for entry in entries {
match entry {
Ok(path) => {
if path.is_file()
&& Self::is_markdown_file(&path)
&& !self.should_exclude(&path, excludes)
{
sources.push(InputSource::File(path));
}
}
Err(e) => {
eprintln!("Warning: Could not read path: {}", e);
}
}
}
Ok(())
}
fn is_markdown_file(path: &std::path::Path) -> bool {
path.extension()
.map(|ext| ext.to_string_lossy().to_lowercase() == "md")
.unwrap_or(false)
}
}
#[derive(Debug)]
pub enum InputSource {
File(PathBuf),
Stdin,
}