use std::{borrow::Cow, path::PathBuf};
use clap::{CommandFactory, FromArgMatches, Parser, error::ErrorKind, parser::ValueSource};
use mdtablefix::{
Options,
format_breaks,
io::SourceDocument,
process::{process_stream_inner, process_with_frontmatter},
};
use crate::{
driver::Mode,
select::extensions::{ExtensionFilter, parse_extension},
};
#[derive(Parser)]
#[command(
version,
about = "Reflow broken markdown tables",
long_about = None
)]
#[command(group(
clap::ArgGroup::new("inputs").args(["files", "git"]).multiple(false)
))]
#[command(group(clap::ArgGroup::new("mode").multiple(false).requires("inputs")))]
#[expect(
clippy::struct_excessive_bools,
reason = "CLI exposes independent flags via separate switches"
)]
pub struct Cli {
#[arg(long = "in-place", group = "mode")]
in_place: bool,
#[arg(long = "check", group = "mode")]
check: bool,
#[arg(long = "diff", group = "mode")]
diff: bool,
#[arg(long = "list-files", group = "mode")]
list_files: bool,
#[arg(long = "git")]
git: bool,
#[arg(long = "include-untracked")]
include_untracked: bool,
#[arg(
long = "md-exts",
value_name = "EXT",
value_delimiter = ',',
default_value = "md,mdc,markdown",
value_parser = parse_extension,
)]
md_exts: Vec<String>,
#[arg(long = "allow-conflicted")]
allow_conflicted: bool,
#[command(flatten)]
pub opts: FormatOpts,
pub files: Vec<PathBuf>,
}
fn git_only_flags(cli: &Cli, matches: &clap::ArgMatches) -> [(&'static str, bool); 4] {
let explicit_exts = matches.value_source("md_exts") != Some(ValueSource::DefaultValue);
[
("--include-untracked", cli.include_untracked),
("--allow-conflicted", cli.allow_conflicted),
("--list-files", cli.list_files),
("--md-exts", explicit_exts),
]
}
impl Cli {
pub fn parse_validated() -> Self {
let matches = Self::command().get_matches();
let cli = Self::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
for (name, present) in git_only_flags(&cli, &matches) {
if present && !cli.git {
Self::command()
.error(
ErrorKind::MissingRequiredArgument,
format!("{name} requires --git"),
)
.exit();
}
}
cli
}
#[must_use]
pub fn mode(&self) -> Mode {
if self.in_place {
Mode::InPlace
} else if self.check {
Mode::Check
} else if self.diff {
Mode::Diff
} else if self.list_files {
Mode::ListFiles
} else {
Mode::Print
}
}
pub fn selects_from_git(&self) -> bool { self.git }
pub fn includes_untracked(&self) -> bool { self.include_untracked }
pub fn allows_conflicted(&self) -> bool { self.allow_conflicted }
pub fn extensions(&self) -> ExtensionFilter { self.md_exts.iter().cloned().collect() }
}
#[derive(clap::Args, Clone, Copy)]
#[expect(
clippy::struct_excessive_bools,
reason = "CLI exposes independent flags via separate switches"
)]
pub struct FormatOpts {
#[arg(long = "wrap")]
pub wrap: bool,
#[arg(long = "renumber")]
pub renumber: bool,
#[arg(long = "breaks")]
pub breaks: bool,
#[arg(long = "ellipsis")]
pub ellipsis: bool,
#[arg(long = "fences")]
pub fences: bool,
#[arg(long = "footnotes")]
pub footnotes: bool,
#[arg(long = "code-emphasis")]
pub code_emphasis: bool,
#[arg(long = "headings")]
pub headings: bool,
}
impl From<FormatOpts> for Options {
fn from(opts: FormatOpts) -> Self {
Self {
wrap: opts.wrap,
ellipsis: opts.ellipsis,
fences: opts.fences,
footnotes: opts.footnotes,
renumber: opts.renumber,
code_emphasis: opts.code_emphasis,
headings: opts.headings,
}
}
}
fn process_lines(lines: &[String], opts: FormatOpts) -> Vec<String> {
process_with_frontmatter(lines, |body| {
let mut out = process_stream_inner(body, opts.into());
if opts.breaks {
out = format_breaks(&out)
.into_iter()
.map(Cow::into_owned)
.collect();
}
out
})
}
pub fn format_lines(content: &str, opts: FormatOpts) -> Vec<String> {
let lines: Vec<String> = content.lines().map(str::to_string).collect();
process_lines(&lines, opts)
}
pub fn formatting_closure(opts: FormatOpts) -> impl Fn(&SourceDocument<'_>) -> String + Sync {
move |document| document.render(&format_lines(document.body(), opts))
}