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 normalization that must precede layout.

use proptest::{prelude::*, test_runner::Config as ProptestConfig};

use super::idempotence_harness::{flags_for, format_twice};

const WRAP: u16 = 1;
const RENUMBER: u16 = 1 << 1;
const ELLIPSIS: u16 = 1 << 3;
const FOOTNOTES: u16 = 1 << 5;
const CODE_EMPHASIS: u16 = 1 << 6;
const HEADINGS: u16 = 1 << 7;

fn footnote_document_strategy() -> impl Strategy<Value = String> {
    (
        proptest::collection::btree_set(1_u8..=99, 1..=4),
        proptest::collection::vec("[a-z]{2,8}", 4..=8),
    )
        .prop_map(|(labels, words)| {
            let prose = words.join(" ");
            let references = labels
                .iter()
                .map(|label| format!("{prose}.{label}"))
                .collect::<Vec<_>>()
                .join(" ");
            let definitions = labels
                .iter()
                .map(|label| format!("{label}. {prose}"))
                .collect::<Vec<_>>()
                .join("\n");

            format!("{references}\n\n## Footnotes\n\n{definitions}\n")
        })
}

fn wide_ordered_list_strategy() -> impl Strategy<Value = String> {
    proptest::collection::vec(proptest::collection::vec("[a-z]{2,8}", 2..=5), 10..=12).prop_map(
        |items| {
            items
                .into_iter()
                .map(|words| {
                    format!(
                        concat!(
                            "1. {} and a deliberately long continuation that must wrap beneath ",
                            "this ordered list marker for alignment."
                        ),
                        words.join(" ")
                    )
                })
                .collect::<Vec<_>>()
                .join("\n")
                + "\n"
        },
    )
}

#[test]
fn footnote_conversion_precedes_wrapping_at_the_width_boundary() {
    let document = include_str!("../data/idempotence/issue_484_footnotes_wrap.dat");
    let source_line = document.lines().next().expect("fixture has prose");
    assert_eq!(source_line.chars().count(), 78);
    assert!(source_line.ends_with(".2"));

    let flags = flags_for(WRAP | FOOTNOTES);
    let (once, twice) = format_twice(document, &flags);

    assert_eq!(twice, once);
    assert!(
        once.contains("[^"),
        "footnote conversion did not run: {once:?}"
    );
    assert!(
        once.lines().all(|line| line.len() <= 80),
        "wrapping measured pre-conversion content: {once:?}"
    );
}

#[test]
fn footnote_conversion_precedes_list_renumbering() {
    let document = "See.[^7]\n\n7. Seventh\n";
    let flags = flags_for(RENUMBER | FOOTNOTES);
    let (once, twice) = format_twice(document, &flags);

    assert_eq!(twice, once);
    assert_eq!(once, "See.[^1]\n\n[^1]: Seventh\n");
}

#[test]
fn footnote_conversion_preserves_setext_heading_text() {
    let flags = flags_for(FOOTNOTES | HEADINGS);
    let (once, twice) = format_twice("Title.2\n=====\n", &flags);

    assert_eq!(twice, once);
    assert_eq!(once, "# Title.2\n");
}

#[test]
fn ellipsis_before_a_footnote_keeps_its_complete_dot_run() {
    let flags = flags_for(ELLIPSIS | FOOTNOTES);
    let (once, twice) = format_twice("Wait...2\n", &flags);

    assert_eq!(twice, once);
    assert_eq!(once, "Wait…[^1]\n");
}

#[test]
fn table_reflow_measures_footnote_converted_cells() {
    let document = "| Note |\n| ---- |\n| See.2 |\n\n## Footnotes\n\n2. Table note\n";
    let flags = flags_for(FOOTNOTES);
    let (once, twice) = format_twice(document, &flags);

    assert_eq!(twice, once);
    assert_eq!(
        once,
        concat!(
            "| Note     |\n",
            "| -------- |\n",
            "| See.[^1] |\n",
            "\n",
            "## Footnotes\n",
            "\n",
            "[^1]: Table note\n",
        )
    );
}

#[test]
fn list_renumbering_precedes_wrapping_at_the_digit_width_boundary() {
    let document = include_str!("../data/idempotence/issue_484_renumber_wrap.dat");
    let flags = flags_for(WRAP | RENUMBER);
    let (once, twice) = format_twice(document, &flags);

    assert_eq!(twice, once);
    let lines: Vec<&str> = once.lines().collect();
    let tenth = lines
        .iter()
        .position(|line| line.starts_with("10. "))
        .expect("renumbered output has a tenth item");
    let continuations = &lines[tenth + 1..];
    assert!(!continuations.is_empty(), "the tenth item must wrap");
    assert!(continuations.iter().all(|line| {
        line.starts_with("    ")
            && line
                .chars()
                .take_while(|character| *character == ' ')
                .count()
                == 4
    }));
}

#[test]
fn table_reflow_measures_code_emphasis_repaired_cells() {
    let document = include_str!("../data/cli-matrix/table-prose.dat");
    let flags = flags_for(CODE_EMPHASIS);
    let (once, twice) = format_twice(document, &flags);

    assert_eq!(twice, once);
    let table: Vec<&str> = once.lines().take(3).collect();
    assert_eq!(
        table,
        [
            "| Name  | Notes                              |",
            "| ----- | ---------------------------------- |",
            "| alpha | Use `cargo test` before merging... |",
        ]
    );
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(48))]

    #[test]
    fn generated_footnotes_and_lists_reach_a_wrapped_fixed_point(
        document in footnote_document_strategy(),
    ) {
        let flags = flags_for(WRAP | RENUMBER | FOOTNOTES);
        let (once, twice) = format_twice(&document, &flags);

        prop_assert_eq!(&twice, &once);
        prop_assert!(once.lines().all(|line| line.len() <= 80));
    }

    #[test]
    fn generated_wide_ordered_lists_use_final_continuation_width(
        document in wide_ordered_list_strategy(),
    ) {
        let flags = flags_for(WRAP | RENUMBER);
        let (once, twice) = format_twice(&document, &flags);
        let lines = once.lines().collect::<Vec<_>>();
        let tenth = lines
            .iter()
            .position(|line| line.starts_with("10. "))
            .expect("renumbered output has a tenth item");
        let continuation = lines
            .get(tenth + 1)
            .expect("the tenth item has a continuation");

        prop_assert_eq!(&twice, &once);
        prop_assert!(once.lines().all(|line| line.len() <= 80));
        prop_assert!(continuation.starts_with("    "));
    }
}