Skip to main content

mdlint/
args.rs

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    /// Lint Markdown files and report issues.
89    Check(CheckArgs),
90    /// Format Markdown files with opinionated style.
91    Format(FormatArgs),
92    /// Start an LSP server communicating over stdio.
93    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}