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(long, help = "Apply auto-fixes where possible")]
127    pub fix: bool,
128
129    #[arg(
130        long,
131        value_name = "FORMAT",
132        default_value_t = OutputFormat::Default,
133        help = "Output format"
134    )]
135    pub output_format: OutputFormat,
136
137    #[arg(
138        long,
139        help = "Lint files in parallel (experimental)",
140        help_heading = "Experimental",
141        overrides_with = "no_parallel"
142    )]
143    pub parallel: bool,
144
145    #[arg(long, hide = true, overrides_with = "parallel")]
146    pub no_parallel: bool,
147
148    #[arg(
149        long,
150        value_delimiter = ',',
151        value_name = "RULE_CODE",
152        help = "Comma-separated list of rules to enable (or `ALL`)",
153        help_heading = "Rule selection"
154    )]
155    pub select: Vec<String>,
156
157    #[arg(
158        long,
159        value_delimiter = ',',
160        value_name = "RULE_CODE",
161        help = "Comma-separated list of rules to disable",
162        help_heading = "Rule selection"
163    )]
164    pub ignore: Vec<String>,
165}
166
167impl CheckArgs {
168    pub fn files(&self) -> Vec<PathBuf> {
169        if self.files.is_empty() {
170            vec![PathBuf::from(".")]
171        } else {
172            self.files.clone()
173        }
174    }
175
176    pub fn should_respect_ignore(&self) -> bool {
177        !self.no_respect_ignore
178    }
179}
180
181#[derive(Args, Debug)]
182pub struct FormatArgs {
183    #[arg(
184        value_name = "FILES",
185        help = "Files or directories to format (defaults to current directory)"
186    )]
187    pub files: Vec<PathBuf>,
188
189    #[arg(
190        long,
191        help = "Files and directories to exclude from formatting",
192        help_heading = "File selection"
193    )]
194    pub exclude: Vec<PathBuf>,
195
196    #[arg(
197        long,
198        default_value_t = true,
199        help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
200        help_heading = "File selection",
201        conflicts_with = "no_respect_ignore"
202    )]
203    pub respect_ignore: bool,
204
205    #[arg(long, hide = true, conflicts_with = "respect_ignore")]
206    pub no_respect_ignore: bool,
207
208    #[arg(
209        long,
210        help = "Check formatting without modifying files (exits with 1 if any file would change)"
211    )]
212    pub check: bool,
213}
214
215impl FormatArgs {
216    pub fn files(&self) -> Vec<PathBuf> {
217        if self.files.is_empty() {
218            vec![PathBuf::from(".")]
219        } else {
220            self.files.clone()
221        }
222    }
223
224    pub fn should_respect_ignore(&self) -> bool {
225        !self.no_respect_ignore
226    }
227}
228
229impl From<&Cli> for ConfigLoader {
230    fn from(cli: &Cli) -> Self {
231        if cli.no_config {
232            Self::None
233        } else if let Some(config_file) = &cli.config {
234            Self::File(config_file.clone())
235        } else {
236            Self::Detect
237        }
238    }
239}
240
241impl From<&Cli> for LogLevel {
242    fn from(cli: &Cli) -> Self {
243        if cli.silent {
244            Self::Silent
245        } else if cli.quiet {
246            Self::Quiet
247        } else if cli.verbose {
248            Self::Verbose
249        } else {
250            Self::Default
251        }
252    }
253}
254
255#[derive(ValueEnum, Debug, Default, Clone)]
256pub enum OutputFormat {
257    #[default]
258    Default,
259    Json,
260}
261
262impl Display for OutputFormat {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        match self {
265            OutputFormat::Default => write!(f, "default"),
266            OutputFormat::Json => write!(f, "json"),
267        }
268    }
269}
270
271#[derive(ValueEnum, Debug, Default, Clone)]
272pub enum TerminalColor {
273    #[default]
274    Auto,
275    Always,
276    Never,
277}
278
279impl Display for TerminalColor {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        match self {
282            TerminalColor::Auto => write!(f, "auto"),
283            TerminalColor::Always => write!(f, "always"),
284            TerminalColor::Never => write!(f, "never"),
285        }
286    }
287}