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