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() {
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) {
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() {
let input = lines_vec!["| tee /tmp/test.log"];
assert_eq!(reflow_table(&input), input);
}
#[test]
fn process_stream_preserves_indented_pipe_continuation() {
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! {
#[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() {
let input = lines_vec![
"```rust",
"let table = build();",
"```rust extra info",
"| tee /tmp/test.log",
"```",
];
assert_eq!(process_stream(&input), input);
}