1use clap::{Parser, ValueEnum};
2use glob::glob;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
7pub enum WrapMode {
8 Always,
10 Never,
12 #[default]
14 Preserve,
15}
16
17impl From<WrapMode> for crate::formatter::WrapMode {
18 fn from(mode: WrapMode) -> Self {
19 match mode {
20 WrapMode::Always => Self::Always,
21 WrapMode::Never => Self::Never,
22 WrapMode::Preserve => Self::Preserve,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
29pub enum OrderedListMode {
30 #[default]
32 Ascending,
33 One,
35}
36
37impl From<OrderedListMode> for crate::formatter::OrderedListMode {
38 fn from(mode: OrderedListMode) -> Self {
39 match mode {
40 OrderedListMode::Ascending => Self::Ascending,
41 OrderedListMode::One => Self::One,
42 }
43 }
44}
45
46const DEFAULT_EXCLUDES: &[&str] = &["node_modules", "target", ".git", "vendor", "dist", "build"];
48
49#[derive(Parser, Debug)]
50#[command(name = "mdfmt")]
51#[command(version = env!("CARGO_PKG_VERSION"))]
52#[command(about = "Fast, opinionated Markdown formatter", long_about = None)]
53pub struct Args {
54 #[arg(value_name = "PATH")]
56 pub paths: Vec<String>,
57
58 #[arg(short, long)]
60 pub write: bool,
61
62 #[arg(long)]
64 pub check: bool,
65
66 #[arg(long)]
68 pub stdin: bool,
69
70 #[arg(long, default_value = "80")]
72 pub width: usize,
73
74 #[arg(long, value_enum, default_value = "preserve")]
76 pub wrap: WrapMode,
77
78 #[arg(long = "ordered-list", value_enum, default_value = "ascending")]
80 pub ordered_list: OrderedListMode,
81
82 #[arg(long = "exclude", value_name = "DIR")]
84 pub excludes: Vec<String>,
85
86 #[arg(long)]
88 pub no_default_excludes: bool,
89}
90
91impl Args {
92 fn get_excludes(&self) -> Vec<String> {
94 let mut excludes: Vec<String> = if self.no_default_excludes {
95 Vec::new()
96 } else {
97 DEFAULT_EXCLUDES.iter().map(|s| s.to_string()).collect()
98 };
99 excludes.extend(self.excludes.clone());
100 excludes
101 }
102
103 fn should_exclude(&self, path: &std::path::Path, excludes: &[String]) -> bool {
105 for component in path.components() {
106 if let std::path::Component::Normal(name) = component {
107 let name_str = name.to_string_lossy();
108 if excludes.iter().any(|e| e == name_str.as_ref()) {
109 return true;
110 }
111 }
112 }
113 false
114 }
115
116 pub fn get_input_sources(&self) -> Result<Vec<InputSource>, String> {
118 if self.stdin || (self.paths.len() == 1 && self.paths[0] == "-") {
119 return Ok(vec![InputSource::Stdin]);
120 }
121
122 if self.paths.is_empty() {
123 return Err("No input provided. Use --stdin or specify file paths.".to_string());
124 }
125
126 let excludes = self.get_excludes();
127 let mut sources = Vec::new();
128
129 for pattern in &self.paths {
130 let path = PathBuf::from(pattern);
131
132 if path.is_dir() {
133 let glob_pattern = format!("{}/**/*.md", pattern);
135 self.collect_markdown_files(&glob_pattern, &mut sources, &excludes)?;
136 } else if path.is_file() {
137 if Self::is_markdown_file(&path) {
139 sources.push(InputSource::File(path));
140 } else {
141 return Err(format!(
142 "File '{}' is not a markdown file (.md)",
143 path.display()
144 ));
145 }
146 } else {
147 self.collect_markdown_files(pattern, &mut sources, &excludes)?;
149 }
150 }
151
152 if sources.is_empty() {
153 return Err("No markdown files found.".to_string());
154 }
155
156 Ok(sources)
157 }
158
159 fn collect_markdown_files(
160 &self,
161 pattern: &str,
162 sources: &mut Vec<InputSource>,
163 excludes: &[String],
164 ) -> Result<(), String> {
165 let entries =
166 glob(pattern).map_err(|e| format!("Invalid glob pattern '{}': {}", pattern, e))?;
167
168 for entry in entries {
169 match entry {
170 Ok(path) => {
171 if path.is_file()
172 && Self::is_markdown_file(&path)
173 && !self.should_exclude(&path, excludes)
174 {
175 sources.push(InputSource::File(path));
176 }
177 }
178 Err(e) => {
179 eprintln!("Warning: Could not read path: {}", e);
180 }
181 }
182 }
183
184 Ok(())
185 }
186
187 fn is_markdown_file(path: &std::path::Path) -> bool {
188 path.extension()
189 .map(|ext| ext.to_string_lossy().to_lowercase() == "md")
190 .unwrap_or(false)
191 }
192}
193
194#[derive(Debug)]
195pub enum InputSource {
196 File(PathBuf),
197 Stdin,
198}