diff --git a/README.md b/README.md index f3d53590..ddecd34f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # mdtablefix `mdtablefix` reflows Markdown tables so that each column has a uniform width. -It also wraps paragraphs and list items to 80 columns. +It can wrap paragraphs and list items to 80 columns when the `--wrap` option is +used. The tool ignores fenced code blocks and respects escaped pipes (`\|`), making it safe for mixed content. @@ -20,10 +21,11 @@ cargo install --path . ## Command-line usage ```bash -mdtablefix [--in-place] [FILE...] +mdtablefix [--wrap] [--in-place] [FILE...] ``` - With file paths provided, the corrected tables are printed to stdout. +- Use `--wrap` to also reflow paragraphs and list items to 80 columns. - Use `--in-place` to overwrite files. - If no files are supplied, input is read from stdin and results are written to stdout. diff --git a/src/html.rs b/src/html.rs index f853ed5b..f85d39fc 100644 --- a/src/html.rs +++ b/src/html.rs @@ -212,8 +212,8 @@ fn push_html_line( /// /// # Examples /// -/// ```ignore -/// use mdtablefix::html::html_table_to_markdown; +/// ```no_run +/// use mdtablefix::html_table_to_markdown; /// let html_lines = vec![ /// "
Header
Cell
".to_string() /// ]; @@ -260,8 +260,8 @@ pub(crate) fn html_table_to_markdown(lines: &[String]) -> Vec { /// /// # Examples /// -/// ```ignore -/// use mdtablefix::html::convert_html_tables; +/// ```no_run +/// use mdtablefix::convert_html_tables; /// let lines = vec![ /// "".to_string(), /// " ".to_string(), diff --git a/src/lib.rs b/src/lib.rs index 42816bf7..70a85cff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,12 @@ mod html; +#[doc(hidden)] +#[must_use] +pub fn html_table_to_markdown(lines: &[String]) -> Vec { + html::html_table_to_markdown(lines) +} + pub use html::convert_html_tables; use regex::Regex; @@ -249,17 +255,19 @@ static FENCE_RE: std::sync::LazyLock = static BULLET_RE: std::sync::LazyLock = std::sync::LazyLock::new(|| Regex::new(r"^(\s*(?:[-*+]|\d+[.)])\s+)(.*)").unwrap()); + /// Returns `true` if the line is a fenced code block delimiter (e.g., three backticks or "~~~"). /// /// # Examples /// -/// ```ignore +/// ```no_run /// use mdtablefix::is_fence; /// assert!(is_fence("```")); /// assert!(is_fence("~~~")); /// assert!(!is_fence("| foo | bar |")); /// ``` -pub(crate) fn is_fence(line: &str) -> bool { +#[doc(hidden)] +pub fn is_fence(line: &str) -> bool { FENCE_RE.is_match(line) } @@ -303,7 +311,7 @@ fn flush_paragraph(out: &mut Vec, buf: &[(String, bool)], indent: &str, /// /// # Examples /// -/// ```ignore +/// ```no_run /// use mdtablefix::wrap_text; /// let input = vec![ /// "This is a long paragraph that should be wrapped to a shorter width.".to_string(), @@ -322,7 +330,8 @@ fn flush_paragraph(out: &mut Vec, buf: &[(String, bool)], indent: &str, /// assert_eq!(wrapped[6], "let x = 42;"); /// assert_eq!(wrapped[7], "```"); /// ``` -fn wrap_text(lines: &[String], width: usize) -> Vec { +#[doc(hidden)] +pub fn wrap_text(lines: &[String], width: usize) -> Vec { let mut out = Vec::new(); let mut buf: Vec<(String, bool)> = Vec::new(); let mut indent = String::new(); @@ -417,7 +426,7 @@ fn wrap_text(lines: &[String], width: usize) -> Vec { /// /// # Examples /// -/// ``` +/// ```no_run /// use mdtablefix::process_stream; /// let input = vec![ /// "
Header
foobar
".to_string(), @@ -431,7 +440,7 @@ fn wrap_text(lines: &[String], width: usize) -> Vec { /// assert!(output.iter().any(|line| line.contains("| foo | bar |"))); /// assert!(output.iter().any(|line| line.len() <= 80)); /// ``` -pub fn process_stream(lines: &[String]) -> Vec { +fn process_stream_inner(lines: &[String], wrap: bool) -> Vec { let pre = html::convert_html_tables(lines); let mut out = Vec::new(); @@ -493,7 +502,17 @@ pub fn process_stream(lines: &[String]) -> Vec { } } - wrap_text(&out, 80) + if wrap { wrap_text(&out, 80) } else { out } +} + +#[must_use] +pub fn process_stream(lines: &[String]) -> Vec { + process_stream_inner(lines, true) +} + +#[must_use] +pub fn process_stream_no_wrap(lines: &[String]) -> Vec { + process_stream_inner(lines, false) } /// Rewrite a file in place with fixed tables. @@ -517,3 +536,14 @@ pub fn rewrite(path: &Path) -> std::io::Result<()> { let fixed = process_stream(&lines); fs::write(path, fixed.join("\n") + "\n") } + +/// Rewrite a file in place with fixed tables without wrapping text. +/// +/// # Errors +/// Returns an error if the file cannot be read or written. +pub fn rewrite_no_wrap(path: &Path) -> std::io::Result<()> { + let text = fs::read_to_string(path)?; + let lines: Vec = text.lines().map(str::to_string).collect(); + let fixed = process_stream_no_wrap(&lines); + fs::write(path, fixed.join("\n") + "\n") +} diff --git a/src/main.rs b/src/main.rs index b9aaf7d1..fb489c14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; -use mdtablefix::{process_stream, rewrite}; +use mdtablefix::{process_stream, process_stream_no_wrap, rewrite, rewrite_no_wrap}; use std::fs; use std::io::{self, Read}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Parser)] #[command(about = "Reflow broken markdown tables")] @@ -10,10 +10,29 @@ struct Cli { /// Rewrite files in place #[arg(long = "in-place", requires = "files")] in_place: bool, + /// Wrap paragraphs and list items to 80 columns + #[arg(long = "wrap")] + wrap: bool, /// Markdown files to fix files: Vec, } +fn process_lines(lines: &[String], wrap: bool) -> Vec { + if wrap { + process_stream(lines) + } else { + process_stream_no_wrap(lines) + } +} + +fn rewrite_path(path: &Path, wrap: bool) -> std::io::Result<()> { + if wrap { + rewrite(path) + } else { + rewrite_no_wrap(path) + } +} + /// Entry point for the command-line tool that reflows broken markdown tables. /// /// Parses command-line arguments to determine whether to process files in place, print fixed output to standard output, or read from standard input. Handles file I/O and error propagation as needed. @@ -41,18 +60,18 @@ fn main() -> anyhow::Result<()> { let mut input = String::new(); io::stdin().read_to_string(&mut input)?; let lines: Vec = input.lines().map(str::to_string).collect(); - let fixed = process_stream(&lines); + let fixed = process_lines(&lines, cli.wrap); println!("{}", fixed.join("\n")); return Ok(()); } for path in cli.files { if cli.in_place { - rewrite(&path)?; + rewrite_path(&path, cli.wrap)?; } else { let content = fs::read_to_string(&path)?; let lines: Vec = content.lines().map(str::to_string).collect(); - let fixed = process_stream(&lines); + let fixed = process_lines(&lines, cli.wrap); println!("{}", fixed.join("\n")); } } diff --git a/tests/integration.rs b/tests/integration.rs index b54b8edb..2eae6262 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -297,6 +297,23 @@ fn test_cli_process_file(broken_table: Vec) { .stdout("| A | B |\n| 1 | 2 |\n| 3 | 4 |\n"); } +#[test] +fn test_cli_wrap_option() { + let input = "This line is deliberately made much longer than eighty columns so that \ + the wrapping algorithm is forced to insert a soft line-break somewhere \ + in the middle of the paragraph when the --wrap flag is supplied."; + let output = Command::cargo_bin("mdtablefix") + .unwrap() + .arg("--wrap") + .write_stdin(format!("{input}\n")) + .output() + .unwrap(); + assert!(output.status.success()); + let text = String::from_utf8_lossy(&output.stdout); + assert!(text.lines().count() > 1, "expected wrapped output on multiple lines"); + assert!(text.lines().all(|l| l.len() <= 80)); +} + #[test] fn test_uniform_example_one() { let input = vec![