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..d00064cd 100644 --- a/src/html.rs +++ b/src/html.rs @@ -212,15 +212,38 @@ 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() /// ]; /// let md_lines = html_table_to_markdown(&html_lines); /// assert!(md_lines[0].starts_with("| Header |")); /// ``` -pub(crate) fn html_table_to_markdown(lines: &[String]) -> Vec { +#[doc(hidden)] +/// Converts HTML tables in the provided lines to Markdown table syntax. +/// +/// Scans the input lines for HTML `` blocks, including nested tables, and replaces each with an equivalent Markdown table. Lines outside of table blocks are preserved unchanged. If a table block is incomplete at the end of input, its lines are appended as-is. +/// +/// # Examples +/// +/// ```no_run +/// use mdtablefix::html_table_to_markdown; +/// +/// let lines = vec![ +/// "
".to_string(), +/// " ".to_string(), +/// " ".to_string(), +/// "
Header
Cell
".to_string(), +/// ]; +/// let result = html_table_to_markdown(&lines); +/// assert_eq!(result, vec![ +/// "| Header |", +/// "| ------ |", +/// "| Cell |", +/// ]); +/// ``` +pub fn html_table_to_markdown(lines: &[String]) -> Vec { let mut out = Vec::new(); let mut buf = Vec::new(); let mut depth = 0usize; @@ -260,8 +283,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..b534429e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,9 @@ mod html; +#[doc(hidden)] +pub use html::html_table_to_markdown; + pub use html::convert_html_tables; use regex::Regex; @@ -249,17 +252,28 @@ 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)] +/// Returns `true` if the line is a fenced code block delimiter (e.g., triple backticks or tildes). +/// +/// # Examples +/// +/// ``` +/// assert!(is_fence("```")); +/// assert!(is_fence("~~~")); +/// assert!(!is_fence(" code block")); +/// ``` +pub fn is_fence(line: &str) -> bool { FENCE_RE.is_match(line) } @@ -303,7 +317,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 +336,34 @@ 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)] +/// Wraps markdown text lines to a specified width, preserving markdown structure. +/// +/// This function wraps paragraphs and list items to the given width, while leaving code blocks, tables, headers, and blank lines unchanged. It preserves indentation, bullet or number prefixes for lists, and respects hard line breaks (two spaces or `
` tags). +/// +/// # Parameters +/// - `lines`: The input markdown lines to wrap. +/// - `width`: The maximum line width for wrapping. +/// +/// # Returns +/// A vector of strings containing the wrapped markdown lines, with structure and formatting preserved. +/// +/// # Examples +/// +/// ``` +/// let input = vec![ +/// "This is a long paragraph that should be wrapped to a shorter width.".to_string(), +/// "".to_string(), +/// "```".to_string(), +/// "let x = 42;".to_string(), +/// "```".to_string(), +/// ]; +/// let wrapped = wrap_text(&input, 20); +/// assert!(wrapped[0].len() <= 20); +/// assert_eq!(wrapped[2], "```"); +/// assert_eq!(wrapped[3], "let x = 42;"); +/// ``` +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 +458,7 @@ fn wrap_text(lines: &[String], width: usize) -> Vec { /// /// # Examples /// -/// ``` +/// ```no_run /// use mdtablefix::process_stream; /// let input = vec![ /// "
Header
foobar
".to_string(), @@ -430,8 +471,10 @@ fn wrap_text(lines: &[String], width: usize) -> Vec { /// let output = process_stream(&input); /// assert!(output.iter().any(|line| line.contains("| foo | bar |"))); /// assert!(output.iter().any(|line| line.len() <= 80)); -/// ``` -pub fn process_stream(lines: &[String]) -> Vec { +/// Processes a stream of markdown lines, converting HTML tables to markdown, reflowing markdown tables, and optionally wrapping text. +/// +/// Converts simple HTML tables to markdown, detects and reflows markdown tables for consistent formatting, and preserves code blocks. If `wrap` is true, wraps the output text to 80 columns while maintaining markdown structure. Returns the processed lines as a vector of strings. +fn process_stream_inner(lines: &[String], wrap: bool) -> Vec { let pre = html::convert_html_tables(lines); let mut out = Vec::new(); @@ -493,7 +536,45 @@ pub fn process_stream(lines: &[String]) -> Vec { } } - wrap_text(&out, 80) + if wrap { wrap_text(&out, 80) } else { out } +} + +#[must_use] +/// Processes markdown lines, fixing tables and wrapping text to 80 columns. +/// +/// This function reflows markdown tables, converts HTML tables to markdown, and wraps text while preserving markdown structure such as code blocks and lists. +/// +/// # Examples +/// +/// ``` +/// let input = vec![ +/// "| a | b |".to_string(), +/// "|---|---|".to_string(), +/// "| 1 | 2 |".to_string(), +/// "", +/// "A paragraph that will be wrapped to fit within 80 columns.".to_string(), +/// ]; +/// let output = process_stream(&input); +/// assert!(output.iter().any(|line| line.contains("| a | b |"))); +/// ``` +pub fn process_stream(lines: &[String]) -> Vec { + process_stream_inner(lines, true) +} + +#[must_use] +/// Processes a stream of markdown lines, fixing tables and converting HTML tables, without wrapping text. +/// +/// Returns the processed lines as a vector of strings. +/// +/// # Examples +/// +/// ``` +/// let input = vec![String::from("| a | b |"), String::from("|-|-|"), String::from("| 1 | 2 |")]; +/// let output = process_stream_no_wrap(&input); +/// assert_eq!(output, vec![String::from("| a | b |"), String::from("| - | - |"), String::from("| 1 | 2 |")]); +/// ``` +pub fn process_stream_no_wrap(lines: &[String]) -> Vec { + process_stream_inner(lines, false) } /// Rewrite a file in place with fixed tables. @@ -510,6 +591,15 @@ pub fn process_stream(lines: &[String]) -> Vec { /// use mdtablefix::rewrite; /// let path = Path::new("example.md"); /// rewrite(path).unwrap(); +/// Reads a markdown file, fixes tables and wraps text, then writes the updated content back to the same file. +/// +/// Returns an I/O error if reading or writing the file fails. +/// +/// # Examples +/// +/// ```no_run +/// use std::path::Path; +/// rewrite(Path::new("README.md")).unwrap(); /// ``` pub fn rewrite(path: &Path) -> std::io::Result<()> { let text = fs::read_to_string(path)?; @@ -517,3 +607,16 @@ 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 +/// Processes a markdown file at the given path, fixing tables without wrapping text, and overwrites the file with the updated content. +/// +/// 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..9b2f6756 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ 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; @@ -10,6 +10,9 @@ 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, } @@ -33,6 +36,18 @@ struct Cli { /// /// # Fix tables from standard input /// cat myfile.md | mdtablefix +/// Entry point for the command-line tool that reflows markdown tables and optionally wraps text. +/// +/// Parses command-line arguments to determine input sources, output behaviour, and whether to wrap text. Processes either standard input or specified files, printing results to standard output or rewriting files in place as requested. Propagates errors from argument parsing, file I/O, and markdown processing. +/// +/// # Examples +/// +/// ```no_run +/// // Run with standard input, wrapping enabled +/// // $ echo "|a|b|\n|---|---|\n|1|2|" | mdtablefix --wrap +/// +/// // Run on files, in-place modification +/// // $ mdtablefix --in-place --wrap file.md /// ``` fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -41,18 +56,30 @@ 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 = if cli.wrap { + process_stream(&lines) + } else { + process_stream_no_wrap(&lines) + }; println!("{}", fixed.join("\n")); return Ok(()); } for path in cli.files { if cli.in_place { - rewrite(&path)?; + if cli.wrap { + rewrite(&path)?; + } else { + rewrite_no_wrap(&path)?; + } } 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 = if cli.wrap { + process_stream(&lines) + } else { + process_stream_no_wrap(&lines) + }; println!("{}", fixed.join("\n")); } } diff --git a/tests/integration.rs b/tests/integration.rs index b54b8edb..9d9c08fe 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -279,6 +279,19 @@ fn test_cli_in_place_requires_file() { /// "| 3 | 4 |".to_string(), /// ]; /// test_cli_process_file(broken_table); +/// Runs the CLI on a file containing a broken Markdown table and asserts the output is the corrected table. +/// +/// This test creates a temporary file with a malformed Markdown table, invokes the `mdtablefix` CLI on it, +/// and verifies that the output matches the expected well-formed table. +/// +/// # Examples +/// +/// ``` +/// test_cli_process_file(vec![ +/// "| A | B |".to_string(), +/// "| 1 | 2 |".to_string(), +/// "| 3 | 4 |".to_string(), +/// ]); /// ``` fn test_cli_process_file(broken_table: Vec) { let dir = tempdir().unwrap(); @@ -298,6 +311,34 @@ fn test_cli_process_file(broken_table: Vec) { } #[test] +/// Tests that the CLI's `--wrap` option wraps long input lines as expected. +/// +/// Runs the `mdtablefix` binary with the `--wrap` flag, provides a long line via stdin, +/// and asserts that the output contains line breaks and the expected text. +/// +/// # Examples +/// +/// ``` +/// test_cli_wrap_option(); +/// ``` +fn test_cli_wrap_option() { + let input = "This line is long enough to require wrapping when the option is enabled."; + 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.contains('\n')); + assert!(text.contains("This line")); +} + +#[test] +/// Verifies that `reflow_table` produces a Markdown table with uniform column widths across all rows. +/// +/// This test checks that each column in the output table has consistent width, ensuring proper alignment after reflow. fn test_uniform_example_one() { let input = vec![ "| Logical type | PostgreSQL | SQLite notes |".to_string(),