rumdl 0.1.87

A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
use clap::{Args, ValueEnum};
use std::ops::{Deref, DerefMut};

/// Fix mode determines exit code behavior: Check/CheckFix exit 1 on violations, Format exits 0
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FixMode {
    #[default]
    Check,
    CheckFix,
    Format,
}

/// Fail-on mode determines which severity triggers exit code 1
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum FailOn {
    /// Exit 1 on any violation (info, warning, or error)
    #[default]
    Any,
    /// Exit 1 on warning or error severity violations
    Warning,
    /// Exit 1 only on error-severity violations
    Error,
    /// Always exit 0
    Never,
}

#[derive(Args, Debug)]
pub struct SharedCliArgs {
    /// Disable specific rules (comma-separated)
    #[arg(short, long, help = "Disable specific rules (comma-separated)")]
    pub disable: Option<String>,

    /// Enable only specific rules (comma-separated)
    #[arg(
        short,
        long,
        visible_alias = "rules",
        help = "Enable only specific rules (comma-separated)"
    )]
    pub enable: Option<String>,

    /// Extend the list of enabled rules (additive with config)
    #[arg(long, help = "Extend the list of enabled rules (additive with config)")]
    pub extend_enable: Option<String>,

    /// Extend the list of disabled rules (additive with config)
    #[arg(long, help = "Extend the list of disabled rules (additive with config)")]
    pub extend_disable: Option<String>,

    /// Only allow these rules to be fixed (comma-separated)
    #[arg(long, help = "Only allow these rules to be fixed (comma-separated)")]
    pub fixable: Option<String>,

    /// Prevent these rules from being fixed (comma-separated)
    #[arg(long, help = "Prevent these rules from being fixed (comma-separated)")]
    pub unfixable: Option<String>,

    /// Exclude specific files or directories (comma-separated glob patterns)
    #[arg(long, help = "Exclude specific files or directories (comma-separated glob patterns)")]
    pub exclude: Option<String>,

    /// Disable all exclude patterns
    #[arg(long, help = "Disable all exclude patterns")]
    pub no_exclude: bool,

    /// Include only specific files or directories (comma-separated glob patterns)
    #[arg(
        long,
        help = "Include only specific files or directories (comma-separated glob patterns)"
    )]
    pub include: Option<String>,

    /// Respect .gitignore files when scanning directories
    #[arg(
        long,
        num_args(0..=1),
        require_equals(true),
        default_missing_value = "true",
        help = "Respect .gitignore files when scanning directories (does not apply to explicitly provided paths)"
    )]
    pub respect_gitignore: Option<bool>,

    /// Print diagnostics, but suppress summary lines
    #[arg(short, long, help = "Print diagnostics, but suppress summary lines")]
    pub quiet: bool,

    /// Show absolute file paths instead of project-relative paths
    #[arg(long, help = "Show absolute file paths in output instead of relative paths")]
    pub show_full_path: bool,

    /// Filename to use for stdin input (for context and error messages)
    #[arg(long, help = "Filename to use when reading from stdin (e.g., README.md)")]
    pub stdin_filename: Option<String>,

    /// Output diagnostics to stderr instead of stdout
    #[arg(long, help = "Output diagnostics to stderr instead of stdout")]
    pub stderr: bool,

    /// Disable caching (re-check all files)
    #[arg(long, help = "Disable caching (re-check all files)")]
    pub no_cache: bool,

    /// Directory to store cache files
    #[arg(
        long,
        help = "Directory to store cache files (default: .rumdl_cache, or $RUMDL_CACHE_DIR, or cache-dir in config)"
    )]
    pub cache_dir: Option<String>,
}

#[derive(Args, Debug)]
pub struct CheckArgs {
    /// Files or directories to check (use '-' for stdin)
    #[arg(required = false)]
    pub paths: Vec<String>,

    /// Fix issues automatically where possible
    #[arg(short, long, default_value = "false")]
    pub fix: bool,

    /// Show diff of what would be fixed instead of fixing files
    #[arg(
        long,
        alias = "dry-run",
        help = "Show diff of what would be fixed instead of fixing files"
    )]
    pub diff: bool,

    /// Exit with code 1 if any formatting changes would be made (like rustfmt --check)
    #[arg(
        long,
        hide = true,
        help = "Exit with code 1 if any formatting changes would be made (for CI)"
    )]
    pub check: bool,

    /// List all available rules
    #[arg(short, long, default_value = "false")]
    pub list_rules: bool,

    #[command(flatten)]
    pub shared: SharedCliArgs,

    /// Show detailed output
    #[arg(short, long, help = "Show detailed output")]
    pub verbose: bool,

    /// Show profiling information
    #[arg(long, help = "Show profiling information")]
    pub profile: bool,

    /// Show statistics summary of rule violations
    #[arg(long, help = "Show statistics summary of rule violations")]
    pub statistics: bool,

    /// Legacy alias for --output-format: text (default) or json
    #[arg(long, short = 'o', default_value_t, value_enum, hide = true)]
    pub output: Output,

    /// Output format for diagnostics (default: text).
    ///
    /// Precedence: --output-format > $RUMDL_OUTPUT_FORMAT > config file > text
    #[arg(long, value_enum)]
    pub output_format: Option<OutputFormat>,

    /// Markdown flavor to use for linting
    #[arg(
        long,
        value_enum,
        help = "Markdown flavor to use: standard (also accepts gfm/github/commonmark), mkdocs, mdx, quarto, obsidian, or kramdown"
    )]
    pub flavor: Option<Flavor>,

    /// Read from stdin instead of files
    #[arg(long, help = "Read from stdin instead of files")]
    pub stdin: bool,

    /// Suppress diagnostics and summaries
    #[arg(short, long, help = "Suppress diagnostics and summaries")]
    pub silent: bool,

    /// Run in watch mode by re-running whenever files change
    #[arg(short, long, help = "Run in watch mode by re-running whenever files change")]
    pub watch: bool,

    /// Enforce exclude patterns even for paths that are passed explicitly.
    /// By default, rumdl will lint any paths passed in directly, even if they would typically be excluded.
    /// Setting this flag will cause rumdl to respect exclusions unequivocally.
    /// This is useful for pre-commit, which explicitly passes all changed files.
    #[arg(long, help = "Enforce exclude patterns even for explicitly specified files")]
    pub force_exclude: bool,

    /// Control when to exit with code 1: any (default), warning, error, or never
    #[arg(
        long,
        value_enum,
        default_value_t,
        help = "Exit code behavior: 'any' (default) exits 1 on any violation, 'warning' on warning+error, 'error' only on errors, 'never' always exits 0"
    )]
    pub fail_on: FailOn,

    #[arg(skip)]
    pub fix_mode: FixMode,

    #[arg(skip)]
    pub fail_on_mode: FailOn,
}

#[derive(Args, Debug)]
pub struct FmtArgs {
    /// Files or directories to format (use '-' for stdin)
    #[arg(required = false)]
    pub paths: Vec<String>,

    /// Show diff of what would be formatted instead of rewriting files
    #[arg(
        long,
        alias = "dry-run",
        help = "Show diff of what would be formatted instead of rewriting files"
    )]
    pub diff: bool,

    /// Exit with code 1 if any formatting changes would be made (for CI)
    #[arg(long, help = "Exit with code 1 if any formatting changes would be made (for CI)")]
    pub check: bool,

    /// Hidden compatibility flag from check
    #[arg(short, long, hide = true, default_value = "false")]
    pub list_rules: bool,

    #[command(flatten)]
    pub shared: SharedCliArgs,

    /// Show detailed formatter output
    #[arg(short, long, help = "Show detailed formatter output")]
    pub verbose: bool,

    /// Hidden compatibility flag from check
    #[arg(long, hide = true)]
    pub profile: bool,

    /// Hidden compatibility flag from check
    #[arg(long, hide = true)]
    pub statistics: bool,

    /// Hidden legacy alias for --output-format
    #[arg(long, short = 'o', default_value_t, value_enum, hide = true)]
    pub output: Output,

    /// Output format for remaining diagnostics (default: text).
    ///
    /// Precedence: --output-format > $RUMDL_OUTPUT_FORMAT > config file > text
    #[arg(long, value_enum)]
    pub output_format: Option<OutputFormat>,

    /// Markdown flavor to use while formatting
    #[arg(
        long,
        value_enum,
        help = "Markdown flavor to use while formatting: standard (also accepts gfm/github/commonmark), mkdocs, mdx, quarto, obsidian, or kramdown"
    )]
    pub flavor: Option<Flavor>,

    /// Read Markdown from stdin instead of files
    #[arg(long, help = "Read Markdown from stdin instead of files")]
    pub stdin: bool,

    /// Suppress diagnostics and summaries; only formatted content is emitted in stdin/stdout mode
    #[arg(
        short,
        long,
        help = "Suppress diagnostics and summaries; only formatted content is emitted in stdin/stdout mode"
    )]
    pub silent: bool,

    /// Re-run formatting whenever files change
    #[arg(short, long, help = "Re-run formatting whenever files change")]
    pub watch: bool,

    /// Hidden deprecated compatibility flag from check
    #[arg(long, hide = true)]
    pub force_exclude: bool,

    /// Hidden compatibility flag; fmt always exits with formatter-style semantics
    #[arg(long, value_enum, default_value_t, hide = true)]
    pub fail_on: FailOn,
}

impl From<FmtArgs> for CheckArgs {
    fn from(args: FmtArgs) -> Self {
        Self {
            paths: args.paths,
            // `fmt` activates fixing via `FixMode::Format` set in main, not via this flag.
            // The flag is intentionally `false` so the check-dispatch path does not
            // independently enable `FixMode::CheckFix`.
            fix: false,
            diff: args.diff,
            check: args.check,
            list_rules: args.list_rules,
            shared: args.shared,
            verbose: args.verbose,
            profile: args.profile,
            statistics: args.statistics,
            output: args.output,
            output_format: args.output_format,
            flavor: args.flavor,
            stdin: args.stdin,
            silent: args.silent,
            watch: args.watch,
            force_exclude: args.force_exclude,
            fail_on: args.fail_on,
            fix_mode: FixMode::default(),
            fail_on_mode: FailOn::default(),
        }
    }
}

impl Deref for CheckArgs {
    type Target = SharedCliArgs;

    fn deref(&self) -> &Self::Target {
        &self.shared
    }
}

impl DerefMut for CheckArgs {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.shared
    }
}

impl Deref for FmtArgs {
    type Target = SharedCliArgs;

    fn deref(&self) -> &Self::Target {
        &self.shared
    }
}

impl DerefMut for FmtArgs {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.shared
    }
}

#[derive(Clone, Debug, Default, ValueEnum)]
pub enum Output {
    #[default]
    Text,
    Json,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum OutputFormat {
    /// One-line-per-warning with file, line, column, rule, and message (default)
    Text,
    /// Show source lines with caret underlines highlighting the violation
    Full,
    /// Minimal: file:line:col rule message
    Concise,
    /// Warnings grouped by file with a header per file
    Grouped,
    /// JSON array of all warnings (collected across files)
    Json,
    /// One JSON object per warning (streaming)
    JsonLines,
    /// GitHub Actions annotation format (::warning/::error)
    #[value(name = "github")]
    GitHub,
    /// GitLab Code Quality report (JSON)
    #[value(name = "gitlab")]
    GitLab,
    /// Pylint-compatible format
    Pylint,
    /// Azure Pipelines logging commands
    Azure,
    /// SARIF 2.1.0 for static analysis tools
    Sarif,
    /// JUnit XML for CI test reporters
    Junit,
}

impl From<OutputFormat> for rumdl_lib::output::OutputFormat {
    fn from(format: OutputFormat) -> Self {
        match format {
            OutputFormat::Text => Self::Text,
            OutputFormat::Full => Self::Full,
            OutputFormat::Concise => Self::Concise,
            OutputFormat::Grouped => Self::Grouped,
            OutputFormat::Json => Self::Json,
            OutputFormat::JsonLines => Self::JsonLines,
            OutputFormat::GitHub => Self::GitHub,
            OutputFormat::GitLab => Self::GitLab,
            OutputFormat::Pylint => Self::Pylint,
            OutputFormat::Azure => Self::Azure,
            OutputFormat::Sarif => Self::Sarif,
            OutputFormat::Junit => Self::Junit,
        }
    }
}

#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "lower")]
pub enum Flavor {
    #[value(aliases(["gfm", "github", "commonmark"]))]
    Standard,
    MkDocs,
    #[allow(clippy::upper_case_acronyms)]
    MDX,
    #[value(aliases(["qmd", "rmd", "rmarkdown"]))]
    Quarto,
    Obsidian,
    #[value(alias("jekyll"))]
    Kramdown,
}

impl From<Flavor> for rumdl_lib::config::MarkdownFlavor {
    fn from(flavor: Flavor) -> Self {
        match flavor {
            Flavor::Standard => Self::Standard,
            Flavor::MkDocs => Self::MkDocs,
            Flavor::MDX => Self::MDX,
            Flavor::Quarto => Self::Quarto,
            Flavor::Obsidian => Self::Obsidian,
            Flavor::Kramdown => Self::Kramdown,
        }
    }
}