#[macro_use]
#[path = "common/mod.rs"]
mod common;
use mdtablefix::{attach_orphan_specifiers, compress_fences};
use rstest::rstest;
fn assert_fence_snapshot(name: &str, input: &[String]) {
insta::with_settings!({
snapshot_path => "snapshots",
prepend_module_to_snapshot => false,
}, {
insta::assert_snapshot!(name, compress_fences(input).join("\n"));
});
}
#[rstest]
#[case(
"fences_compress_overlong_backticks",
lines_vec!["`````rust", "fn main() {}", "`````"]
)]
#[case(
"fences_compress_tilde_fences",
lines_vec!["~~~~~python", "print('hello')", "~~~~~"]
)]
#[case(
"fences_preserve_interior_conflict",
lines_vec!["````markdown", "```rust", "fn main() {}", "```", "````"]
)]
fn fence_normalization_snapshots(#[case] name: &str, #[case] input: Vec<String>) {
assert_fence_snapshot(name, &input);
}
#[test]
fn compresses_backtick_fences() {
let input = lines_vec!["````rust", "code", "````"];
let out = compress_fences(&input);
assert_eq!(out, lines_vec!["```rust", "code", "```"]);
}
#[test]
fn compresses_indented_backticks() {
let input = lines_vec![" `````foo,bar "];
let out = compress_fences(&input);
assert_eq!(out, lines_vec![" ```foo,bar"]);
}
#[test]
fn compresses_tilde_fences() {
let input = lines_vec!["~~~~rust", "code", "~~~~"];
let out = compress_fences(&input);
assert_eq!(out, lines_vec!["```rust", "code", "```"]);
}
#[test]
fn preserves_null_language_during_interior_conflict() {
let input = lines_vec!["````null", "```rust", "fn main() {}", "```", "````"];
let out = compress_fences(&input);
assert_eq!(
out,
lines_vec!["````", "```rust", "fn main() {}", "```", "````"]
);
}
#[rstest]
#[case(
lines_vec!["````markdown", "```rust", "fn main() {}", "```", "````"],
lines_vec!["````markdown", "```rust", "fn main() {}", "```", "````"]
)]
#[case(
lines_vec!["~~~~markdown", "~~~rust", "fn main() {}", "~~~", "~~~~"],
lines_vec!["~~~~markdown", "~~~rust", "fn main() {}", "~~~", "~~~~"]
)]
#[case(
lines_vec!["~~~markdown", "```rust", "fn main() {}", "```", "~~~"],
lines_vec!["~~~markdown", "```rust", "fn main() {}", "```", "~~~"]
)]
#[case(
lines_vec!["````markdown", "~~~rust", "fn main() {}", "~~~", "````"],
lines_vec!["```markdown", "~~~rust", "fn main() {}", "~~~", "```"]
)]
#[case(
lines_vec!["``` rust", "~~~example", "code", "~~~", "```"],
lines_vec!["``` rust", "~~~example", "code", "~~~", "```"]
)]
#[case(
lines_vec!["```` rust", "code", "````"],
lines_vec!["```` rust", "code", "````"]
)]
fn preserves_nested_or_spaced_fence_blocks(
#[case] input: Vec<String>,
#[case] expected: Vec<String>,
) {
let out = compress_fences(&input);
assert_eq!(out, expected);
}
#[test]
fn does_not_compress_mixed_fences() {
let input = lines_vec!["~~~rust", "code", "```"];
let out = compress_fences(&input);
assert_eq!(out, input);
let input2 = lines_vec!["```rust", "code", "~~~"];
let out2 = compress_fences(&input2);
assert_eq!(out2, input2);
}
#[test]
fn leaves_other_lines_untouched() {
let input = lines_vec!["~~", "``text``"];
let out = compress_fences(&input);
assert_eq!(out, input);
}
#[test]
fn fixes_orphaned_specifier() {
let input = lines_vec!["Rust", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["```rust", "fn main() {}", "```"]);
}
#[rstest]
#[case(
lines_vec!["````markdown", "Rust", "```", "fn main() {}", "```", "````"],
lines_vec!["````markdown", "Rust", "```", "fn main() {}", "```", "````"]
)]
#[case(
lines_vec!["````markdown", "```", "Rust", "fn main() {}", "```", "````"],
lines_vec!["````markdown", "```", "Rust", "fn main() {}", "```", "````"]
)]
#[case(
lines_vec!["Rust", "````", "```", "fn main() {}", "```", "````"],
lines_vec!["````rust", "```", "fn main() {}", "```", "````"]
)]
fn handles_orphan_specifiers_around_outer_fences(
#[case] input: Vec<String>,
#[case] expected: Vec<String>,
) {
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, expected);
}
#[test]
fn attaches_orphan_specifier_unit() {
let input = lines_vec!["Rust", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&input);
assert_eq!(out, lines_vec!["```rust", "fn main() {}", "```"]);
}
#[test]
fn attaches_orphan_specifier_with_blank_line_unit() {
let input = lines_vec!["Rust", "", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&input);
assert_eq!(out, lines_vec!["```rust", "fn main() {}", "```"]);
}
#[rstest]
#[case(lines_vec!["Rust", "", "```", "fn main() {}", "```"])]
#[case(lines_vec!["Rust", "", "", "```", "fn main() {}", "```"])]
fn fixes_orphaned_specifier_with_blank_lines(#[case] input: Vec<String>) {
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["```rust", "fn main() {}", "```"]);
}
#[rstest]
#[case(lines_vec!["Rust", "", "", "```python", "print('hi')", "```"])]
#[case(lines_vec!["Rust", "", "not a fence"])]
fn leaves_orphan_specifier_and_blank_lines_unchanged_when_not_attachable(
#[case] input: Vec<String>,
) {
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, input);
}
#[test]
fn fixes_multiple_orphaned_specifiers() {
let input = lines_vec![
"Rust",
"```",
"fn main() {}",
"```",
"",
"Python",
"```",
"print('hi')",
"```",
];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(
out,
lines_vec![
"```rust",
"fn main() {}",
"```",
"",
"```python",
"print('hi')",
"```"
]
);
}
#[test]
fn does_not_attach_non_orphan_lines_before_fences() {
let input = lines_vec![
"Rust code",
"```",
"fn main() {}",
"```",
"rust!",
"```",
"println!(\"hi\");",
"```",
];
let out = attach_orphan_specifiers(&input);
assert_eq!(out, input);
}
#[test]
fn does_not_overwrite_existing_fence() {
let input = lines_vec!["ruby", "```rust", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["ruby", "```rust", "fn main() {}", "```"]);
}
#[test]
fn does_not_attach_specifier_without_preceding_blank_line() {
let input = lines_vec!["intro", "Rust", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(
out,
lines_vec!["intro", "Rust", "```", "fn main() {}", "```"]
);
}
#[test]
fn attaches_orphan_specifier_with_symbols() {
let input = lines_vec!["C++", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["```c++", "fn main() {}", "```"]);
}
#[test]
fn attaches_orphan_specifier_with_hyphen_and_dot() {
let input = lines_vec!["objective-c", "```", "int main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["```objective-c", "int main() {}", "```"]);
}
#[test]
fn does_not_attach_specifier_with_trailing_period() {
let input = lines_vec!["rust.", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&input);
assert_eq!(out, input);
}
#[test]
fn does_not_attach_specifier_with_trailing_question_mark() {
let input = lines_vec!["rust?", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&input);
assert_eq!(out, input);
}
#[test]
fn attaches_orphan_specifier_preserves_indent() {
let input = lines_vec![" Rust", "", " ```", " fn main() {}", " ```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec![" ```rust", " fn main() {}", " ```"]);
}
#[test]
fn attaches_orphan_specifier_preserves_tab_indent() {
let input = lines_vec!["\tRust", "", "\t```", "\tfn main() {}", "\t```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["\t```rust", "\tfn main() {}", "\t```"]);
}
#[test]
fn attaches_orphan_specifier_mixed_indent() {
let input = lines_vec![" \tRust", "", " \t```", " \tfn main() {}", " \t```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec![" \t```rust", " \tfn main() {}", " \t```"]);
}
#[test]
fn attaches_orphan_specifier_uses_candidate_indent_when_fence_unindented() {
let input = lines_vec![" Rust", "", "```", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec![" ```rust", "fn main() {}", "```"]);
}
#[rstest]
#[case(lines_vec![" Rust", "", " ```", " fn main() {}", " ```"], lines_vec![" ```rust", " fn main() {}", " ```"])]
#[case(lines_vec![" Rust", "", " ```", " fn main() {}", " ```"], lines_vec![" ```rust", " fn main() {}", " ```"])]
fn attaches_orphan_specifier_with_mismatched_indent(
#[case] input: Vec<String>,
#[case] expected: Vec<String>,
) {
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, expected);
}
#[test]
fn attaches_orphan_specifier_allows_spaces() {
let input = lines_vec!["TOML, Ini", "```", "a=1", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["```toml,ini", "a=1", "```"]);
}
#[rstest]
#[case("````null", "````")]
#[case("````NULL", "````")]
#[case("````Null", "````")]
#[case("````null ", "````")]
#[case("```` ", "````")]
#[case("````NULL ", "````")]
#[case("````Null ", "````")]
#[case("~~~~null", "~~~~")]
#[case("~~~~NULL", "~~~~")]
#[case("~~~~Null", "~~~~")]
#[case("~~~~null ", "~~~~")]
#[case("~~~~ ", "~~~~")]
#[case("~~~~NULL ", "~~~~")]
#[case("~~~~Null ", "~~~~")]
fn compresses_null_language_to_empty(#[case] open: &str, #[case] close: &str) {
let input = lines_vec![open, "code", close];
let out = compress_fences(&input);
assert_eq!(out, lines_vec!["```", "code", "```"]);
}
#[rstest]
#[case("```null", "```")]
#[case("```NULL", "```")]
#[case("```Null", "```")]
#[case("```null ", "```")]
#[case("```NULL ", "```")]
#[case("```Null ", "```")]
#[case("~~~~null", "~~~~")]
#[case("~~~~NULL", "~~~~")]
#[case("~~~~Null", "~~~~")]
#[case("~~~~null ", "~~~~")]
#[case("~~~~NULL ", "~~~~")]
#[case("~~~~Null ", "~~~~")]
fn attaches_orphan_specifier_when_null_language(#[case] fence: &str, #[case] opener: &str) {
let input = lines_vec!["Rust", fence, "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(
out,
lines_vec![&format!("{opener}rust"), "fn main() {}", "```"]
);
}
#[test]
fn attaches_orphan_specifier_null_language_without_compression() {
let input = lines_vec!["Rust", "```null", "fn main() {}", "```"];
let out = attach_orphan_specifiers(&input);
assert_eq!(out, lines_vec!["```rust", "fn main() {}", "```"]);
}
#[rstest]
#[case("``` ", "```")]
#[case("~~~~ ", "~~~~")]
fn attaches_orphan_specifier_whitespace_language(#[case] fence: &str, #[case] opener: &str) {
let input = lines_vec!["Rust", fence, "fn main() {}", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(
out,
lines_vec![&format!("{opener}rust"), "fn main() {}", "```"]
);
}
#[test]
fn compresses_matched_fence_reusing_cached_opening_and_closing_rewrites() {
let input = lines_vec!["`````rust", "let x = 1;", "let y = 2;", "`````"];
let out = compress_fences(&input);
assert_eq!(
out,
lines_vec!["```rust", "let x = 1;", "let y = 2;", "```"]
);
}
#[test]
fn unclosed_fence_rewrites_only_the_opening_delimiter() {
let input = lines_vec!["``````rust", "plain text", "more text"];
let out = compress_fences(&input);
assert_eq!(out, lines_vec!["```rust", "plain text", "more text"]);
}
#[test]
fn unclosed_fence_keeps_the_opener_reported_for_an_interior_shorter_fence() {
let input = lines_vec!["````", "```", "literal..."];
let out = compress_fences(&input);
assert_eq!(out, input);
}
#[test]
fn unclosed_fence_keeps_the_opener_that_makes_its_interior_literal() {
let input = lines_vec!["``````rust", "````", "~~~"];
let out = compress_fences(&input);
assert_eq!(out, input);
}
#[rstest]
#[case(lines_vec!["> ```rust", "> code", "> ```"])]
#[case(lines_vec!["> > ````toml", "> > data", "> > ````"])]
#[case(lines_vec![">```", ">code", ">```"])]
fn preserves_quoted_and_nested_fences(#[case] input: Vec<String>) {
let out = compress_fences(&input);
assert_eq!(out, input);
}
#[test]
fn compresses_top_level_fence_after_quoted_block() {
let input = lines_vec!["> ````rust", "> code", "> ````", "````rust", "top", "````",];
let out = compress_fences(&input);
assert_eq!(
out,
lines_vec!["> ````rust", "> code", "> ````", "```rust", "top", "```",]
);
}
#[rstest]
#[case::hyphen_run("---")]
#[case::asterisk_run("***")]
#[case::underscore_run("___")]
#[case::spaced_hyphen_run("- - -")]
#[case::indented_hyphen_run(" ---")]
#[case::normalised_break(&"_".repeat(mdtablefix::THEMATIC_BREAK_LEN))]
fn does_not_attach_a_thematic_break_as_a_specifier(#[case] break_line: &str) {
let input = lines_vec![break_line, "```", "aa", "```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, input);
}
#[test]
fn does_not_attach_a_thematic_break_through_blank_lines() {
let break_line = "_".repeat(mdtablefix::THEMATIC_BREAK_LEN);
let input = lines_vec![
break_line.as_str(),
"",
"```",
"aa",
"```",
"",
"Rust",
"```",
"bb",
"```",
];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(
out,
lines_vec![
break_line.as_str(),
"",
"```",
"aa",
"```",
"",
"```rust",
"bb",
"```",
]
);
}
#[test]
fn attaches_a_tab_indented_specifier_that_is_not_a_break() {
let input = lines_vec!["\tRust", "", "\t```", "\tfn main() {}", "\t```"];
let out = attach_orphan_specifiers(&compress_fences(&input));
assert_eq!(out, lines_vec!["\t```rust", "\tfn main() {}", "\t```"]);
}
#[test]
fn keeps_info_string_marker_as_literal_content() {
let input = lines_vec![
"```rust",
"let table = build();",
"```rust extra info",
"| tee /tmp/test.log",
"```",
];
let out = compress_fences(&input);
assert_eq!(out, input);
}