Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.

Expand Down
8 changes: 4 additions & 4 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![
/// "<table><tr><th>Header</th></tr><tr><td>Cell</td></tr></table>".to_string()
/// ];
Expand Down Expand Up @@ -260,8 +260,8 @@ pub(crate) fn html_table_to_markdown(lines: &[String]) -> Vec<String> {
///
/// # Examples
///
/// ```ignore
/// use mdtablefix::html::convert_html_tables;
/// ```no_run
/// use mdtablefix::convert_html_tables;
/// let lines = vec![
/// "<table>".to_string(),
/// " <tr><th>Header</th></tr>".to_string(),
Expand Down
44 changes: 37 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

mod html;

#[doc(hidden)]
#[must_use]
pub fn html_table_to_markdown(lines: &[String]) -> Vec<String> {
html::html_table_to_markdown(lines)
}

pub use html::convert_html_tables;

use regex::Regex;
Expand Down Expand Up @@ -249,17 +255,19 @@ static FENCE_RE: std::sync::LazyLock<Regex> =
static BULLET_RE: std::sync::LazyLock<Regex> =
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)
}

Expand Down Expand Up @@ -303,7 +311,7 @@ fn flush_paragraph(out: &mut Vec<String>, 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(),
Expand All @@ -322,7 +330,8 @@ fn flush_paragraph(out: &mut Vec<String>, buf: &[(String, bool)], indent: &str,
/// assert_eq!(wrapped[6], "let x = 42;");
/// assert_eq!(wrapped[7], "```");
/// ```
fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
#[doc(hidden)]
pub fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
let mut out = Vec::new();
let mut buf: Vec<(String, bool)> = Vec::new();
let mut indent = String::new();
Expand Down Expand Up @@ -417,7 +426,7 @@ fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
///
/// # Examples
///
/// ```
/// ```no_run
/// use mdtablefix::process_stream;
/// let input = vec![
/// "<table><tr><td>foo</td><td>bar</td></tr></table>".to_string(),
Expand All @@ -431,7 +440,7 @@ fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
/// assert!(output.iter().any(|line| line.contains("| foo | bar |")));
/// assert!(output.iter().any(|line| line.len() <= 80));
/// ```
pub fn process_stream(lines: &[String]) -> Vec<String> {
fn process_stream_inner(lines: &[String], wrap: bool) -> Vec<String> {
let pre = html::convert_html_tables(lines);

let mut out = Vec::new();
Expand Down Expand Up @@ -493,7 +502,17 @@ pub fn process_stream(lines: &[String]) -> Vec<String> {
}
}

wrap_text(&out, 80)
if wrap { wrap_text(&out, 80) } else { out }
}

#[must_use]
pub fn process_stream(lines: &[String]) -> Vec<String> {
process_stream_inner(lines, true)
}

#[must_use]
pub fn process_stream_no_wrap(lines: &[String]) -> Vec<String> {
process_stream_inner(lines, false)
}

/// Rewrite a file in place with fixed tables.
Expand All @@ -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<String> = text.lines().map(str::to_string).collect();
let fixed = process_stream_no_wrap(&lines);
fs::write(path, fixed.join("\n") + "\n")
}
29 changes: 24 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
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")]
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<PathBuf>,
}

fn process_lines(lines: &[String], wrap: bool) -> Vec<String> {
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.
Expand Down Expand Up @@ -41,18 +60,18 @@ fn main() -> anyhow::Result<()> {
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_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<String> = content.lines().map(str::to_string).collect();
let fixed = process_stream(&lines);
let fixed = process_lines(&lines, cli.wrap);
println!("{}", fixed.join("\n"));
}
}
Expand Down
17 changes: 17 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,23 @@ fn test_cli_process_file(broken_table: Vec<String>) {
.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![
Expand Down