mdtablefix 0.6.1

`mdtablefix` unb0rks and reflows Markdown tables so that each column has a uniform width. When the `--wrap` option is used, it also wraps paragraphs and list items to 80 columns.
Documentation
//! Regression tests for Markdown tables that use continuation rows.

use mdtablefix::{Options, process_stream, process_stream_opts, reflow_table};
use proptest::prelude::*;
use rstest::rstest;
use unicode_width::UnicodeWidthStr;

#[macro_use]
mod common;

fn assert_uniform_display_widths(output: &[String]) {
    assert!(!output.is_empty());

    let widths: Vec<usize> = output[0]
        .trim()
        .trim_matches('|')
        .split('|')
        .map(UnicodeWidthStr::width)
        .collect();

    for row in output {
        let cells: Vec<&str> = row.trim().trim_matches('|').split('|').collect();
        assert_eq!(cells.len(), widths.len());
        for (idx, cell) in cells.iter().enumerate() {
            assert_eq!(UnicodeWidthStr::width(*cell), widths[idx]);
        }
    }
}

#[test]
fn preserves_four_space_indented_table_verbatim() {
    // A four-space indent makes this a Markdown indented code block, not a
    // table: it must pass through untouched rather than being reflowed.
    let input = lines_vec!["    | a | b |", "    | --- | --- |", "    | 1 | 2 |",];
    assert_eq!(process_stream(&input), input);
}

#[rstest(
    block_line,
    case::blockquote("> note | with pipe"),
    case::bullet("- item | with pipe"),
    case::footnote("[^a]: note | with pipe")
)]
fn reflows_table_before_block_line_containing_pipe(block_line: &str) {
    // A block marker (`>`, `-`, `[^id]:`) may carry its own `|`. The buffer
    // must recognise such a line as a new block rather than absorbing it into
    // the active table run; otherwise the stray row makes `reflow_table` bail
    // and the preceding table is left un-reflowed.
    let input = lines_vec!["| a | b |", "| 1 | 22 |", block_line];
    let expected = lines_vec!["| a | b  |", "| 1 | 22 |", block_line];
    assert_eq!(process_stream(&input), expected);
}

#[test]
fn reflow_table_preserves_leading_empty_continuation_cells() {
    let input = lines_vec![
        "| A | B | C |",
        "| - | - | - |",
        "| x | y | one, |",
        "|   |   | two |",
    ];
    let expected = lines_vec![
        "| A   | B   | C    |",
        "| --- | --- | ---- |",
        "| x   | y   | one, |",
        "|     |     | two  |",
    ];
    assert_eq!(reflow_table(&input), expected);
}

#[test]
fn process_stream_preserves_continuation_row_columns() {
    let input = lines_vec![
        "| Module     | Stability | Types and functions                     |",
        "| ---------- | --------- | --------------------------------------- |",
        "| `api`      | Stable    | `CommandOutcome`, `ExecParams`, `exec`, |",
        "|            |           | `run_agent`, `list_containers`,         |",
        "|            |           | `stop_container`, `run_token_daemon`    |",
        "| `github`   | Internal  | Subject to change; not part of the      |",
        "|            |           | stable integration contract             |",
    ];
    let expected = lines_vec![
        "| Module   | Stability | Types and functions                     |",
        "| -------- | --------- | --------------------------------------- |",
        "| `api`    | Stable    | `CommandOutcome`, `ExecParams`, `exec`, |",
        "|          |           | `run_agent`, `list_containers`,         |",
        "|          |           | `stop_container`, `run_token_daemon`    |",
        "| `github` | Internal  | Subject to change; not part of the      |",
        "|          |           | stable integration contract             |",
    ];
    assert_eq!(process_stream(&input), expected);
}

#[test]
fn process_stream_preserves_literal_ellipsis_in_table_cells_when_disabled() {
    let input = lines_vec![
        "| Module | Notes                       |",
        "| ------ | --------------------------- |",
        "| `foo`  | preserves literal ... here  |",
        "| `bar`  | and also ... in this cell   |",
    ];
    let expected = lines_vec![
        "| Module | Notes                      |",
        "| ------ | -------------------------- |",
        "| `foo`  | preserves literal ... here |",
        "| `bar`  | and also ... in this cell  |",
    ];

    assert_eq!(
        process_stream_opts(
            &input,
            Options {
                ellipsis: false,
                ..Default::default()
            },
        ),
        expected
    );
}

#[test]
fn process_stream_opts_reflows_tables_after_ellipsis_in_table_cells() {
    let input = lines_vec![
        "| Module     | Stability | Types and functions                     |",
        "| ---------- | --------- | --------------------------------------- |",
        "| `config`   | Stable    | `AppConfig`, `ConfigLoadOptions`, ...   |",
        "| `engine`   | Stable    | `EngineConnector`, `ExecRequest`, ...   |",
        "| `error`    | Stable    | `PodbotError`, `ConfigError`, ...       |",
    ];
    let expected = lines_vec![
        "| Module   | Stability | Types and functions                 |",
        "| -------- | --------- | ----------------------------------- |",
        "| `config` | Stable    | `AppConfig`, `ConfigLoadOptions`, … |",
        "| `engine` | Stable    | `EngineConnector`, `ExecRequest`, … |",
        "| `error`  | Stable    | `PodbotError`, `ConfigError`, …     |",
    ];
    let output = process_stream_opts(
        &input,
        Options {
            ellipsis: true,
            ..Default::default()
        },
    );
    assert_eq!(output, expected);
}

#[test]
fn reflow_table_preserves_escaped_pipes_in_continuation_rows() {
    let input = lines_vec![
        "| Key | Notes |",
        "| --- | ----- |",
        "| `api` | first item |",
        "|     | keep \\| literal in continuation |",
    ];
    let expected = lines_vec![
        "| Key   | Notes                           |",
        "| ----- | ------------------------------- |",
        "| `api` | first item                      |",
        "|       | keep \\| literal in continuation |",
    ];

    assert_eq!(reflow_table(&input), expected);
}

#[rstest]
#[case(
    lines_vec![
        "| Name | Notes |",
        "| ---- | ----- |",
        "| café | naïve |",
        "| long | déjà vu |",
    ]
)]
#[case(
    lines_vec![
        "| Name | Notes |",
        "| ---- | ----- |",
        "| 漢字 | wide 🙂 |",
        "| kana | かな |",
    ]
)]
#[case(
    lines_vec![
        "| Name | Notes |",
        "| ---- | ----- |",
        "| 🙂🙂 | emoji |",
        "| text | mixé |",
    ]
)]
fn reflow_table_uses_unicode_display_widths(#[case] input: Vec<String>) {
    let output = reflow_table(&input);

    assert_uniform_display_widths(&output);
}

#[test]
fn reflow_table_passes_single_pipe_line_through_verbatim() {
    // A lone pipe-prefixed content line (for example a shell pipeline
    // continuation) is not a table. It must pass through `reflow_table`
    // unchanged rather than gaining a fabricated trailing pipe.
    let input = lines_vec!["| tee /tmp/test.log"];
    assert_eq!(reflow_table(&input), input);
}

#[test]
fn process_stream_preserves_indented_pipe_continuation() {
    // The reported reproduction: an indented (code block) shell pipeline whose
    // continuation line begins with `|`. The block is literal, so the pipe line
    // must not be reflowed into a table row, and the whole document is
    // idempotent under the full stream pipeline.
    let input = lines_vec![
        "Run the helper:",
        "",
        "    timeout 300 make test 2>&1 \\",
        "        | tee /tmp/test.log",
        "",
        "Done.",
    ];
    assert_eq!(process_stream(&input), input);
}

proptest! {
    /// A single pipe-prefixed line whose sole cell is ordinary content — no
    /// embedded pipe and not a separator run — is not a table and must pass
    /// through `reflow_table` unchanged, never gaining a trailing pipe.
    #[test]
    fn reflow_table_passes_arbitrary_lone_single_cell_through(
        content in "[A-Za-z0-9][A-Za-z0-9 ./_-]{0,30}",
    ) {
        let input = vec![format!("| {content}")];
        let output = reflow_table(&input);
        prop_assert_eq!(output, input);
    }
}

#[test]
fn process_stream_keeps_pipe_line_in_fenced_block_with_info_string() {
    // A same-marker line bearing an info string is literal content, not a
    // closing fence (CommonMark). The fence stays open through the pipe line,
    // which must therefore remain verbatim rather than becoming a table row.
    let input = lines_vec![
        "```rust",
        "let table = build();",
        "```rust extra info",
        "| tee /tmp/test.log",
        "```",
    ];
    assert_eq!(process_stream(&input), input);
}