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
67 changes: 67 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ use std::path::Path;

/// Split a markdown table line into its cells.
#[must_use]
/// Splits a markdown table line into trimmed cell strings.
///
/// Removes leading and trailing pipe characters, splits the line by pipes, trims whitespace from each cell, and returns the resulting cell strings as a vector.
///
/// # Examples
///
/// ```
/// let line = "| cell1 | cell2 | cell3 |";
/// let cells = split_cells(line);
/// assert_eq!(cells, vec!["cell1", "cell2", "cell3"]);
/// ```
fn split_cells(line: &str) -> Vec<String> {
let mut s = line.trim();
if let Some(stripped) = s.strip_prefix('|') {
Expand All @@ -24,6 +35,23 @@ fn split_cells(line: &str) -> Vec<String> {
/// # Panics
/// Panics if the internal regex fails to compile.
#[must_use]
/// Reflows a broken markdown table into properly aligned rows and columns.
///
/// Takes a slice of strings representing lines of a markdown table, reconstructs the table by splitting and aligning cells, and returns the reflowed table as a vector of strings. If the rows have inconsistent numbers of non-empty columns, the original lines are returned unchanged.
///
/// # Examples
///
/// ```
/// let lines = vec![
/// "| a | b |".to_string(),
/// "| c | d |".to_string(),
/// ];
/// let fixed = reflow_table(&lines);
/// assert_eq!(fixed, vec![
/// "| a | b |".to_string(),
/// "| c | d |".to_string(),
/// ]);
/// ```
pub fn reflow_table(lines: &[String]) -> Vec<String> {
let raw = lines.iter().map(|l| l.trim()).collect::<Vec<_>>().join(" ");
let sentinel_re = Regex::new(r"\|\s*\|\s*").unwrap();
Expand Down Expand Up @@ -82,6 +110,35 @@ pub fn reflow_table(lines: &[String]) -> Vec<String> {
/// # Panics
/// Panics if the regex used for code fences fails to compile.
#[must_use]
/// Processes a stream of markdown lines, reflowing tables while preserving code blocks and other content.
///
/// Detects fenced code blocks and avoids modifying their contents. Buffers lines that appear to be part of a markdown table and reflows them when the table ends. Non-table lines and code blocks are output unchanged.
///
/// # Returns
///
/// A vector of strings representing the processed markdown document with tables reflowed.
///
/// # Examples
///
/// ```
/// let input = vec![
/// "| a | b |",
/// "|---|---|",
/// "| 1 | 2 |",
/// "",
/// "```",
/// "code block",
/// "```",
/// ];
/// let output = process_stream(&input);
/// assert_eq!(output[0], "| a | b |");
/// assert_eq!(output[1], "| --- | --- |");
/// assert_eq!(output[2], "| 1 | 2 |");
/// assert_eq!(output[3], "");
/// assert_eq!(output[4], "```");
/// assert_eq!(output[5], "code block");
/// assert_eq!(output[6], "```");
/// ```
pub fn process_stream(lines: &[String]) -> Vec<String> {
let fence_re = Regex::new(r"^(```|~~~)").unwrap();
let mut out = Vec::new();
Expand Down Expand Up @@ -143,7 +200,17 @@ pub fn process_stream(lines: &[String]) -> Vec<String> {
/// Rewrite a file in place with fixed tables.
///
/// # Errors
/// Reads a markdown file, reflows any broken tables within it, and writes the updated content back to the same file.
///
/// Returns an error if the file cannot be read or written.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// let path = Path::new("example.md");
/// rewrite(path).unwrap();
/// ```
pub fn rewrite(path: &Path) -> std::io::Result<()> {
let text = fs::read_to_string(path)?;
let lines: Vec<String> = text.lines().map(str::to_string).collect();
Expand Down
20 changes: 20 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ struct Cli {
files: Vec<PathBuf>,
}

/// 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.
///
/// # Returns
///
/// Returns `Ok(())` if all operations complete successfully; otherwise, returns an error if argument validation or file processing fails.
///
/// # Examples
///
/// ```sh
/// # Fix tables in a file and print to stdout
/// mdtablefix myfile.md
///
/// # Fix tables in place
/// mdtablefix --in-place myfile.md
///
/// # Fix tables from standard input
/// cat myfile.md | mdtablefix
/// ```
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();

Expand Down
59 changes: 59 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ use std::io::Write;
use tempfile::tempdir;

#[fixture]
/// Provides a sample Markdown table with broken rows for testing purposes.
///
/// The returned vector contains lines representing a table with inconsistent columns, useful for validating table reflow logic.
///
/// # Examples
///
/// ```
/// let table = broken_table();
/// assert_eq!(table[0], "| A | B | |");
/// ```
fn broken_table() -> Vec<String> {
vec![
"| A | B | |".to_string(),
Expand All @@ -14,22 +24,47 @@ fn broken_table() -> Vec<String> {
}

#[fixture]
/// Returns a vector of strings representing a malformed Markdown table with inconsistent columns.
///
/// The returned table has rows with differing numbers of columns, making it invalid for standard Markdown table parsing.
///
/// # Examples
///
/// ```
/// let table = malformed_table();
/// assert_eq!(table, vec![String::from("| A | |"), String::from("| 1 | 2 | 3 |")]);
/// ```
fn malformed_table() -> Vec<String> {
vec!["| A | |".to_string(), "| 1 | 2 | 3 |".to_string()]
}

#[rstest]
/// Tests that `reflow_table` correctly restructures a broken Markdown table into a well-formed table.
///
/// # Examples
///
/// ```
/// let broken = vec![String::from("| A | B |"), String::from("| 1 | 2 |"), String::from("| 3 | 4 |")];
/// let expected = vec!["| A | B |", "| 1 | 2 |", "| 3 | 4 |"];
/// assert_eq!(reflow_table(&broken), expected);
/// ```
fn test_reflow_basic(broken_table: Vec<String>) {
let expected = vec!["| A | B |", "| 1 | 2 |", "| 3 | 4 |"];
assert_eq!(reflow_table(&broken_table), expected);
}

#[rstest]
/// Tests that `reflow_table` returns the original input unchanged when given a malformed Markdown table.
///
/// This ensures that the function does not attempt to modify tables with inconsistent columns or structure.
fn test_reflow_malformed_returns_original(malformed_table: Vec<String>) {
assert_eq!(reflow_table(&malformed_table), malformed_table);
}

#[rstest]
/// Tests that `process_stream` leaves lines inside code fences unchanged.
///
/// Verifies that both backtick (```) and tilde (~~~) fenced code blocks are ignored by the table processing logic, ensuring their contents are not altered.
fn test_process_stream_ignores_code_fences() {
let lines = vec![
"```".to_string(),
Expand All @@ -48,6 +83,16 @@ fn test_process_stream_ignores_code_fences() {
}

#[rstest]
/// Verifies that the CLI fails when the `--in-place` flag is used without specifying a file.
///
/// This test ensures that running `mdtablefix --in-place` without a file argument results in a command failure.
///
/// # Examples
///
/// ```
/// test_cli_in_place_requires_file();
/// // The command should fail as no file is provided.
/// ```
fn test_cli_in_place_requires_file() {
Command::cargo_bin("mdtablefix")
.unwrap()
Expand All @@ -57,6 +102,20 @@ fn test_cli_in_place_requires_file() {
}

#[rstest]
/// Tests that the CLI processes a file containing a broken Markdown table and outputs the corrected table to stdout.
///
/// This test creates a temporary file with a malformed table, runs the `mdtablefix` binary on it, and asserts that the output is the expected fixed table.
///
/// # Examples
///
/// ```
/// let broken_table = vec![
/// "| A | B |".to_string(),
/// "| 1 | 2 |".to_string(),
/// "| 3 | 4 |".to_string(),
/// ];
/// test_cli_process_file(broken_table);
/// ```
fn test_cli_process_file(broken_table: Vec<String>) {
let dir = tempdir().unwrap();
let file_path = dir.path().join("sample.md");
Expand Down