use std::{
borrow::Cow,
fs,
io::{self, Read},
path::{Path, PathBuf},
};
use anyhow::Context;
use clap::Parser;
use mdtablefix::{Options, format_breaks, process_stream_opts, renumber_lists};
use rayon::prelude::*;
#[derive(Parser)]
#[command(version, about = "Reflow broken markdown tables")]
struct Cli {
#[arg(long = "in-place", requires = "files")]
in_place: bool,
#[command(flatten)]
opts: FormatOpts,
files: Vec<PathBuf>,
}
#[derive(clap::Args, Clone, Copy)]
#[expect(
clippy::struct_excessive_bools,
reason = "CLI exposes independent flags via separate switches"
)]
struct FormatOpts {
#[arg(long = "wrap")]
wrap: bool,
#[arg(long = "renumber")]
renumber: bool,
#[arg(long = "breaks")]
breaks: bool,
#[arg(long = "ellipsis")]
ellipsis: bool,
#[arg(long = "fences")]
fences: bool,
#[arg(long = "footnotes")]
footnotes: bool,
#[arg(long = "code-emphasis")]
code_emphasis: bool,
#[arg(long = "headings")]
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,
code_emphasis: opts.code_emphasis,
headings: opts.headings,
}
}
}
fn process_lines(lines: &[String], opts: FormatOpts) -> Vec<String> {
let mut out = process_stream_opts(lines, opts.into());
if opts.renumber {
out = renumber_lists(&out);
}
if opts.breaks {
out = format_breaks(&out)
.into_iter()
.map(Cow::into_owned)
.collect();
}
out
}
fn handle_file(path: &Path, in_place: bool, opts: FormatOpts) -> anyhow::Result<Option<String>> {
let content =
fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let lines: Vec<String> = content.lines().map(str::to_string).collect();
let fixed = process_lines(&lines, opts);
if in_place {
let output = if fixed.is_empty() {
String::new()
} else {
fixed.join("\n") + "\n"
};
fs::write(path, output).with_context(|| format!("writing {}", path.display()))?;
Ok(None)
} else {
Ok(Some(fixed.join("\n")))
}
}
fn report_results<T, F>(results: Vec<anyhow::Result<T>>, mut on_ok: F) -> anyhow::Result<()>
where
F: FnMut(T),
{
let mut first_err: Option<anyhow::Error> = None;
for res in results {
match res {
Ok(val) => on_ok(val),
Err(e) => {
eprintln!("{e}");
if first_err.is_none() {
first_err = Some(e);
}
}
}
}
if let Some(err) = first_err {
Err(err)
} else {
Ok(())
}
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
if cli.files.is_empty() {
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
let lines: Vec<String> = input.lines().map(str::to_string).collect();
let fixed = process_lines(&lines, cli.opts);
println!("{}", fixed.join("\n"));
return Ok(());
}
if cli.in_place {
let results: Vec<anyhow::Result<()>> = cli
.files
.par_iter()
.map(|p| handle_file(p, true, cli.opts).map(|_| ()))
.collect();
report_results(results, |()| {})?;
} else {
let results: Vec<anyhow::Result<Option<String>>> = cli
.files
.par_iter()
.map(|p| handle_file(p, false, cli.opts))
.collect();
report_results(results, |maybe_out| {
if let Some(out) = maybe_out {
println!("{out}");
}
})?;
}
Ok(())
}