use mdtablefix::{convert_footnotes, process_stream};
use rstest::rstest;
#[macro_use]
#[path = "common/mod.rs"]
mod common;
#[test]
fn macros_available() {
let _: Vec<String> = lines_vec!("a", "b");
let _: Vec<String> = include_lines!("data/footnotes_input.txt");
}
#[test]
fn test_convert_bare_footnotes() {
let input: Vec<String> = include_lines!("data/footnotes_input.txt");
let expected: Vec<String> = include_lines!("data/footnotes_expected.txt");
let output = convert_footnotes(&input);
assert_eq!(output, expected);
}
#[test]
fn test_idempotent_on_converted() {
let expected: Vec<String> = include_lines!("data/footnotes_expected.txt");
let output = convert_footnotes(&expected);
assert_eq!(output, expected);
}
#[test]
fn test_avoids_false_positives() {
let input = lines_vec!("Plan9 is interesting.", "Call 1-800-555-1234 for help.",);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_ignores_numbers_in_inline_code() {
let input = lines_vec!("Look at `code 1` for details.");
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_ignores_numbers_in_parentheses() {
let input = lines_vec!("Refer to equation (1) for context.");
assert_eq!(convert_footnotes(&input), input);
}
#[rstest]
#[case("### A.2 A Note on This List")]
#[case("### Heading with footnote[1]")]
#[case("> ### A.2 A Note on This List")]
#[case("- ### A.2 A Note on This List")]
#[case("* ### A.2 A Note on This List")]
#[case("+ ### A.2 A Note on This List")]
#[case("1. ### A.2 A Note on This List")]
#[case("1) ### A.2 A Note on This List")]
#[case("- 1. ### A.2 A Note on This List")]
#[case(">> ### A.2 A Note on This List")]
#[case(">>> ### A.2 A Note on This List")]
fn heading_lines_are_left_verbatim(#[case] line: &str) {
let input = lines_vec!(line);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_ignores_numbers_in_fenced_code_block() {
let input = lines_vec!(
"Here is a code block:",
"```",
"let x = 42; // note 1",
"```",
"Done."
);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_ignores_numbers_in_indented_code_block() {
let input = lines_vec!(
" let a = 1;",
" let b = 2; // number 2",
"",
"Outside."
);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_handles_punctuation_inside_bold() {
let input = lines_vec!("It was **scary.**7");
let expected = lines_vec!("It was **scary.**[^1]");
assert_eq!(convert_footnotes(&input), expected);
}
#[rstest]
#[case(
lines_vec!(
"While a full library tutorial is beyond this guide's scope, a brief look at the",
"core API concepts reveals its ergonomic design. The official `docs.rs` page",
"provides several end-to-end examples that revolve around a few key types 7:",
),
lines_vec!(
"While a full library tutorial is beyond this guide's scope, a brief look at the",
"core API concepts reveals its ergonomic design. The official `docs.rs` page",
"provides several end-to-end examples that revolve around a few key types[^1]:",
)
)]
#[case(
lines_vec!(
"This is a reference 12:: to something important.",
"Another example 3::: with more colons.",
),
lines_vec!(
"This is a reference[^1]:: to something important.",
"Another example[^2]::: with more colons.",
)
)]
#[case(
lines_vec!(
"See the details in section 5:, which are crucial.",
"Another case is 8:; for completeness.",
),
lines_vec!(
"See the details in section[^1]:, which are crucial.",
"Another case is[^2]:; for completeness.",
)
)]
#[case(
lines_vec!(
"This is a tricky one 9 : and should be handled.",
"Extra spaces 10 : are also possible.",
),
lines_vec!(
"This is a tricky one[^1]: and should be handled.",
"Extra spaces[^2]: are also possible.",
)
)]
fn test_converts_number_followed_by_colon(
#[case] input: Vec<String>,
#[case] expected: Vec<String>,
) {
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_converts_colon_footnote_definition() {
let input = lines_vec!("## Footnotes", "7: Footnote text");
let expected = lines_vec!("## Footnotes", "[^1]: Footnote text");
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_converts_colon_definition_with_leading_spaces() {
let input = lines_vec!("## Footnotes", " 7: Footnote text");
let expected = lines_vec!("## Footnotes", " [^1]: Footnote text");
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_converts_colon_definition_with_trailing_spaces() {
let input = lines_vec!("## Footnotes", "7: Footnote text");
let expected = lines_vec!("## Footnotes", "[^1]: Footnote text");
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_convert_preserves_headers_with_blank_separators() {
let input: Vec<String> = include_lines!("data/footnotes_regression_input.txt");
let expected: Vec<String> = include_lines!("data/footnotes_regression_expected.txt");
let reflowed = process_stream(&input);
assert_eq!(
reflowed, expected,
"reflowed fixture must match expectation"
);
let output = convert_footnotes(&reflowed);
assert_eq!(output, expected);
}
#[test]
fn test_converts_list_with_blank_lines() {
let input = lines_vec!(
"Text.",
"",
"## Footnotes",
"",
" 1. First",
" ",
" 2. Second",
"",
"10. Tenth",
" ",
"",
);
let expected = lines_vec!(
"Text.",
"",
"## Footnotes",
"",
" [^1]: First",
" ",
" [^2]: Second",
"",
"[^3]: Tenth",
" ",
"",
);
let output = convert_footnotes(&input);
assert_eq!(output, expected);
}
#[test]
fn test_empty_input() {
let input: Vec<String> = Vec::new();
let output = convert_footnotes(&input);
assert!(output.is_empty());
}
#[test]
fn test_whitespace_input() {
let input = lines_vec!(" ", "\t");
let output = convert_footnotes(&input);
assert_eq!(output, input);
}
#[test]
fn test_requires_h2_heading() {
let input = lines_vec!("Text.", " 1. First footnote",);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_skips_when_existing_block_present() {
let input = lines_vec!("[^1]: Old", "## Footnotes", " 2. New",);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_skips_when_list_not_last() {
let input = lines_vec!("## Footnotes", " 1. Note", "", "Tail.",);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_skips_with_h3_heading() {
let input = lines_vec!("Text.", "### Notes", " 1. First");
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_converts_with_non_footnotes_h2() {
let input = lines_vec!("## Notes", " 1. First");
let expected = lines_vec!("## Notes", " [^1]: First");
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_skips_when_existing_block_is_indented_or_quoted() {
let input1 = lines_vec!(" [^1]: Old", "## Footnotes", " 2. New");
let input2 = lines_vec!("> [^1]: Old", "## Footnotes", " 2. New");
let input3 = lines_vec!(">> [^1]: Old", "## Footnotes", " 2. New");
assert_eq!(convert_footnotes(&input1), input1);
assert_eq!(convert_footnotes(&input2), input2);
assert_eq!(convert_footnotes(&input3), input3);
}
#[test]
fn test_converts_after_inline_reference_at_bol() {
let input = lines_vec!("[^1] see note", "## Footnotes", " 1. First");
let expected = lines_vec!("[^1] see note", "## Footnotes", " [^1]: First");
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_ignores_definition_inside_fence() {
let input = lines_vec!("```", "[^1]: Old", "```", "## Footnotes", " 1. First",);
let expected = lines_vec!("```", "[^1]: Old", "```", "## Footnotes", " [^1]: First",);
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_renumbers_numeric_list_without_heading() {
let input = lines_vec!(
"First reference.[^7]",
"Second reference.[^3]",
"",
"1. Legacy footnote",
"3. Third footnote",
"7. Seventh footnote",
);
let expected = lines_vec!(
"First reference.[^1]",
"Second reference.[^2]",
"",
"[^1]: Seventh footnote",
"[^2]: Third footnote",
"[^3]: Legacy footnote",
);
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_renumbers_numeric_list_with_wrapped_items_and_duplicates_without_heading() {
let input = lines_vec!(
"First ref.[^7] and again [^7]",
"",
"1. Legacy footnote",
"3. Third footnote wraps",
" over two lines.",
"7. Seventh footnote",
);
let expected = lines_vec!(
"First ref.[^1] and again [^1]",
"",
"[^1]: Seventh footnote",
"[^2]: Third footnote wraps",
" over two lines.",
"[^3]: Legacy footnote",
);
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_skips_numeric_list_not_last_without_heading() {
let input = lines_vec!(
"Reference.[^2]",
"1. First footnote",
"2. Second footnote",
"",
"Tail.",
);
assert_eq!(convert_footnotes(&input), input);
}
#[test]
fn test_renumbers_reference_followed_by_colons() {
let input = lines_vec!(
"Usage.[^7]:: extra context",
"",
"## Footnotes",
"7. Footnote text",
);
let expected = lines_vec!(
"Usage.[^1]:: extra context",
"",
"## Footnotes",
"[^1]: Footnote text",
);
assert_eq!(convert_footnotes(&input), expected);
}
#[test]
fn test_preserves_numeric_list_without_references() {
let input = lines_vec!("Ordinary list:", "1. Apples", "2. Bananas",);
assert_eq!(convert_footnotes(&input), input);
}