use crate::docs::Check;
use crate::docs::tests::{fires_once_at, of_kind, run, silent, wide};
pub(super) fn fence_sensitive() -> Vec<(Check, String)> {
vec),
]
}
pub(super) fn fence_blind() -> Vec<(Check, String)> {
vec![(Check::TrailingWhitespace, "text ".to_owned())]
}
const STRUCTURAL: [Check; 3] = [
Check::MultipleBlankLines,
Check::TrailingBlankLines,
Check::UnclosedCodeFence,
];
#[test]
fn every_check_has_declared_a_fence_position() {
let mut declared: Vec<&str> = fence_sensitive()
.iter()
.chain(fence_blind().iter())
.map(|(c, _)| c.as_str())
.chain(STRUCTURAL.iter().map(|c| c.as_str()))
.collect();
declared.sort_unstable();
let mut all: Vec<&str> = Check::ALL.iter().map(|c| c.as_str()).collect();
all.sort_unstable();
assert_eq!(
declared, all,
"a check must appear in exactly one of fence_sensitive / fence_blind / \
STRUCTURAL - if you added one, decide what it does inside a fence"
);
}
#[test]
fn fence_sensitive_checks_fire_in_prose() {
for (check, snippet) in fence_sensitive() {
assert_eq!(
of_kind(&snippet, check).len(),
1,
"{} must fire on {snippet:?} standing alone",
check.as_str()
);
}
}
#[test]
fn fence_sensitive_checks_are_silent_inside_a_fence() {
for (check, snippet) in fence_sensitive() {
let fenced = format!("```text\n{snippet}\n```\n");
silent(&fenced, check);
}
}
#[test]
fn fence_blind_checks_fire_in_prose_and_inside_a_fence() {
for (check, snippet) in fence_blind() {
assert_eq!(of_kind(&snippet, check).len(), 1, "{}", check.as_str());
let fenced = format!("```text\n{snippet}\n```\n");
fires_once_at(&fenced, check, 2, snippet.chars().count() as u32 - 1);
}
}
#[test]
fn the_delimiter_line_is_itself_inside_the_fence() {
let opener = format!("```{}", wide(130));
let content = format!("{opener}\ncode\n```\n");
silent(&content, Check::LongLine);
assert_eq!(of_kind(&opener.replace('`', "x"), Check::LongLine).len(), 1);
}
#[test]
fn an_indented_fence_still_opens_a_fence() {
let content = "- item:\n\n ```bash\n #!/bin/bash\n ```\n";
silent(content, Check::MissingSpaceAfterHeading);
}
#[test]
fn text_after_a_closing_fence_is_prose_again() {
let content = "```bash\n#!/bin/bash\n```\n\n#Heading\n";
fires_once_at(content, Check::MissingSpaceAfterHeading, 5, 2);
}
#[test]
fn a_file_with_no_fence_at_all_leaves_every_line_in_prose() {
let content = "#Heading\n\nmore\n";
assert!(run(content).iter().any(|f| f.line == 1));
}
fn prose_lines(content: &str) -> Vec<usize> {
let raw: Vec<&str> = content.lines().collect();
let fences = crate::docs::fence::Fences::scan(&raw);
fences
.mask()
.iter()
.enumerate()
.filter(|(_, inside)| !**inside)
.map(|(index, _)| index + 1)
.collect()
}
#[test]
fn a_tilde_fence_is_a_fence() {
let content = "~~~bash\n#!/bin/bash\n#comment\n~~~\n\n#Heading\n";
silent_at_lines(content, Check::MissingSpaceAfterHeading, &[2, 3]);
fires_once_at(content, Check::MissingSpaceAfterHeading, 6, 2);
}
#[track_caller]
fn silent_at_lines(content: &str, check: Check, lines: &[u32]) {
let hits: Vec<u32> = of_kind(content, check)
.into_iter()
.map(|f| f.line)
.filter(|line| lines.contains(line))
.collect();
assert_eq!(
hits,
Vec::<u32>::new(),
"{} over {content:?}",
check.as_str()
);
}
#[test]
fn a_fence_is_closed_only_by_the_character_that_opened_it() {
assert_eq!(prose_lines("```\naaa\n~~~\nstill code\n```\nprose\n"), [6]);
assert_eq!(prose_lines("~~~\naaa\n```\nstill code\n~~~\nprose\n"), [6]);
}
#[test]
fn a_closing_fence_must_be_at_least_as_long_as_the_opening_one() {
assert_eq!(prose_lines("````\naaa\n```\nmore\n``````\nprose\n"), [6]);
assert_eq!(prose_lines("~~~~\naaa\n~~~\nmore\n~~~~\nprose\n"), [6]);
assert_eq!(prose_lines("```\naaa\n```\nprose\n"), [4]);
assert_eq!(prose_lines("```\naaa\n`````\nprose\n"), [4]);
}
#[test]
fn a_closing_fence_carries_nothing_after_it_but_spaces_and_tabs() {
assert_eq!(
prose_lines("```text\n```rust\nfn main() {}\n```\nprose\n"),
[5]
);
assert_eq!(prose_lines("```\ncode\n``` \t\nprose\n"), [4]);
}
#[test]
fn a_backtick_fence_whose_info_string_has_a_backtick_is_not_a_fence() {
assert_eq!(prose_lines("``` aa ```\nfoo\n"), [1, 2]);
assert_eq!(prose_lines("``` ```\naaa\n"), [1, 2]);
}
#[test]
fn a_tilde_fence_may_carry_backticks_in_its_info_string() {
assert_eq!(prose_lines("~~~ aa ``` ~~~\nfoo\n~~~\nprose\n"), [4]);
}
#[test]
fn fewer_than_three_characters_is_not_a_fence() {
assert_eq!(prose_lines("``\nfoo\n``\n"), [1, 2, 3]);
assert_eq!(prose_lines("~~\nfoo\n~~\n"), [1, 2, 3]);
}
#[test]
fn a_fence_marker_line_inside_a_fence_that_cannot_close_it_stays_code() {
assert_eq!(
prose_lines("````\n~~~\n```bash\n~~~~\ntext\n````\nprose\n"),
[7]
);
}
#[test]
fn a_nested_fence_documenting_a_fence_is_one_block() {
let content = "````markdown\n```bash\n#!/bin/bash\n```\n````\n\n#Heading\n";
silent_at_lines(content, Check::MissingSpaceAfterHeading, &[3]);
fires_once_at(content, Check::MissingSpaceAfterHeading, 7, 2);
silent(content, Check::UnclosedCodeFence);
}