1use crate::config::loader::ConfigLoader;
2use crate::logger::log_level::LogLevel;
3use clap::builder::Styles;
4use clap::builder::styling::{AnsiColor, Effects};
5use clap::{Args, Parser, Subcommand, ValueEnum};
6use std::fmt::Display;
7use std::path::PathBuf;
8
9const STYLES: Styles = Styles::styled()
10 .header(AnsiColor::Green.on_default().effects(Effects::BOLD))
11 .usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
12 .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
13 .placeholder(AnsiColor::Cyan.on_default());
14
15#[derive(Parser, Debug)]
16#[command(
17 author,
18 name = "mdlint",
19 version,
20 about = "An opinionated Markdown formatter and linter",
21 after_help = "For help with a specific command, see: `mdlint help <command>`",
22 styles = STYLES,
23)]
24pub struct Cli {
25 #[command(subcommand)]
26 pub command: Command,
27
28 #[arg(
29 long,
30 global = true,
31 help = "Path to TOML configuration file (`mdlint.toml`)",
32 help_heading = "Configuration",
33 overrides_with = "no_config"
34 )]
35 pub config: Option<PathBuf>,
36
37 #[arg(
38 long,
39 global = true,
40 help = "Ignore all configuration files",
41 help_heading = "Configuration",
42 overrides_with = "config"
43 )]
44 pub no_config: bool,
45
46 #[arg(
47 short,
48 long,
49 global = true,
50 help = "Enable verbose logging",
51 help_heading = "Log levels",
52 conflicts_with_all = ["quiet", "silent"]
53 )]
54 pub verbose: bool,
55
56 #[arg(
57 short,
58 long,
59 global = true,
60 help = "Print diagnostics, nothing else",
61 help_heading = "Log levels",
62 conflicts_with_all = ["verbose", "silent"]
63 )]
64 pub quiet: bool,
65
66 #[arg(
67 short,
68 long,
69 global = true,
70 help = "Disable all logging (exit code still reflects result)",
71 help_heading = "Log levels",
72 conflicts_with_all = ["verbose", "quiet"]
73 )]
74 pub silent: bool,
75
76 #[arg(
77 long,
78 global = true,
79 default_value_t = TerminalColor::Auto,
80 hide_default_value = true,
81 help = "Control colors in output"
82 )]
83 pub color: TerminalColor,
84}
85
86#[derive(Subcommand, Debug)]
87pub enum Command {
88 Check(CheckArgs),
90 Format(FormatArgs),
92 Server(ServerArgs),
94}
95
96#[derive(Args, Debug)]
97pub struct ServerArgs {}
98
99#[derive(Args, Debug)]
100pub struct CheckArgs {
101 #[arg(
102 value_name = "FILES",
103 help = "Files or directories to check (defaults to current directory)"
104 )]
105 pub files: Vec<PathBuf>,
106
107 #[arg(
108 long,
109 help = "Files and directories to exclude from analysis",
110 help_heading = "File selection"
111 )]
112 pub exclude: Vec<PathBuf>,
113
114 #[arg(
115 long,
116 default_value_t = true,
117 help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
118 help_heading = "File selection",
119 conflicts_with = "no_respect_ignore"
120 )]
121 pub respect_ignore: bool,
122
123 #[arg(long, hide = true, conflicts_with = "respect_ignore")]
124 pub no_respect_ignore: bool,
125
126 #[arg(
127 long,
128 help = "Apply auto-fixes where possible",
129 overrides_with = "no_fix"
130 )]
131 pub fix: bool,
132
133 #[arg(long, hide = true, overrides_with = "fix")]
134 pub no_fix: bool,
135
136 #[arg(
137 long,
138 value_name = "FORMAT",
139 default_value_t = OutputFormat::Default,
140 help = "Output format"
141 )]
142 pub output_format: OutputFormat,
143
144 #[arg(
145 long,
146 help = "Lint files in parallel (experimental)",
147 help_heading = "Experimental",
148 overrides_with = "no_parallel"
149 )]
150 pub parallel: bool,
151
152 #[arg(long, hide = true, overrides_with = "parallel")]
153 pub no_parallel: bool,
154
155 #[arg(
156 long,
157 value_delimiter = ',',
158 value_name = "RULE_CODE",
159 help = "Comma-separated list of rules to enable (or `ALL`)",
160 help_heading = "Rule selection"
161 )]
162 pub select: Vec<String>,
163
164 #[arg(
165 long,
166 value_delimiter = ',',
167 value_name = "RULE_CODE",
168 help = "Comma-separated list of rules to disable",
169 help_heading = "Rule selection"
170 )]
171 pub ignore: Vec<String>,
172}
173
174impl CheckArgs {
175 pub fn files(&self) -> Vec<PathBuf> {
176 if self.files.is_empty() {
177 vec![PathBuf::from(".")]
178 } else {
179 self.files.clone()
180 }
181 }
182
183 pub fn should_respect_ignore(&self) -> bool {
184 !self.no_respect_ignore
185 }
186
187 pub fn should_fix(&self) -> Option<bool> {
188 match (self.fix, self.no_fix) {
189 (true, _) => Some(true),
190 (_, true) => Some(false),
191 (false, false) => None,
192 }
193 }
194}
195
196#[derive(Args, Debug)]
197pub struct FormatArgs {
198 #[arg(
199 value_name = "FILES",
200 help = "Files or directories to format (defaults to current directory)"
201 )]
202 pub files: Vec<PathBuf>,
203
204 #[arg(
205 long,
206 help = "Files and directories to exclude from formatting",
207 help_heading = "File selection"
208 )]
209 pub exclude: Vec<PathBuf>,
210
211 #[arg(
212 long,
213 default_value_t = true,
214 help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
215 help_heading = "File selection",
216 conflicts_with = "no_respect_ignore"
217 )]
218 pub respect_ignore: bool,
219
220 #[arg(long, hide = true, conflicts_with = "respect_ignore")]
221 pub no_respect_ignore: bool,
222
223 #[arg(
224 long,
225 help = "Check formatting without modifying files (exits with 1 if any file would change)"
226 )]
227 pub check: bool,
228}
229
230impl FormatArgs {
231 pub fn files(&self) -> Vec<PathBuf> {
232 if self.files.is_empty() {
233 vec![PathBuf::from(".")]
234 } else {
235 self.files.clone()
236 }
237 }
238
239 pub fn should_respect_ignore(&self) -> bool {
240 !self.no_respect_ignore
241 }
242}
243
244impl From<&Cli> for ConfigLoader {
245 fn from(cli: &Cli) -> Self {
246 if cli.no_config {
247 Self::None
248 } else if let Some(config_file) = &cli.config {
249 Self::File(config_file.clone())
250 } else {
251 Self::Detect
252 }
253 }
254}
255
256impl From<&Cli> for LogLevel {
257 fn from(cli: &Cli) -> Self {
258 if cli.silent {
259 Self::Silent
260 } else if cli.quiet {
261 Self::Quiet
262 } else if cli.verbose {
263 Self::Verbose
264 } else {
265 Self::Default
266 }
267 }
268}
269
270#[derive(ValueEnum, Debug, Default, Clone)]
271pub enum OutputFormat {
272 #[default]
273 Default,
274 Json,
275 Gitlab,
276}
277
278impl Display for OutputFormat {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 match self {
281 OutputFormat::Default => write!(f, "default"),
282 OutputFormat::Gitlab => write!(f, "gitlab"),
283 OutputFormat::Json => write!(f, "json"),
284 }
285 }
286}
287
288#[derive(ValueEnum, Debug, Default, Clone)]
289pub enum TerminalColor {
290 #[default]
291 Auto,
292 Always,
293 Never,
294}
295
296impl Display for TerminalColor {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 TerminalColor::Auto => write!(f, "auto"),
300 TerminalColor::Always => write!(f, "always"),
301 TerminalColor::Never => write!(f, "never"),
302 }
303 }
304}