use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
use std::fs;
use std::process::{Command, Output};
use tempfile::tempdir;
const CONFIG: &str =
"[global]\nenable = [\"MD013\"]\n[MD013]\nline-length = 40\nreflow = true\nreflow-mode = \"normalize\"\n";
const LONG: &str = "This is a deliberately long paragraph that exceeds forty columns and should stay on this single line while the rule is disabled.";
fn run(command: &str, extra: &[&str], doc: &str) -> (Output, String) {
let dir = tempdir().unwrap();
let config = dir.path().join("rumdl.toml");
fs::write(&config, CONFIG).unwrap();
let path = dir.path().join("doc.md");
fs::write(&path, doc).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(dir.path())
.arg(command)
.args(["--config", config.to_str().unwrap(), "--no-cache"])
.args(extra)
.arg(&path)
.output()
.expect("run rumdl");
let code = output.status.code();
assert!(
matches!(code, Some(0 | 1)),
"rumdl {command} failed ({code:?}): {}",
String::from_utf8_lossy(&output.stderr)
);
let after = fs::read_to_string(&path).unwrap();
(output, after)
}
fn fmt(doc: &str) -> String {
run("fmt", &[], doc).1
}
fn md013_warnings(doc: &str) -> usize {
let (output, _) = run("check", &[], doc);
String::from_utf8_lossy(&output.stdout).matches("[MD013]").count()
}
fn skeleton(content: &str) -> Vec<String> {
Parser::new_ext(content, Options::empty())
.map(|event| match event {
Event::Html(_) => "html".to_string(),
Event::InlineHtml(_) => "inline-html".to_string(),
Event::Start(Tag::Paragraph) => "p".to_string(),
Event::End(TagEnd::Paragraph) => "/p".to_string(),
other => format!("{other:?}"),
})
.collect()
}
fn spellings(body: &str) -> Vec<String> {
vec![
format!("<!-- {body} -->"),
format!("<!--{body} -->"),
format!("<!-- {body}-->"),
format!("<!--{body}-->"),
format!("<!-- {body} -->"),
format!("<!--\t{body}\t-->"),
]
}
fn region(open: &str, close: &str) -> String {
format!("# Example\n\n{open}\n{LONG}\n{close}\n\n{LONG}\n")
}
#[test]
fn issue_repro_fmt_leaves_paragraph_inside_compact_region() {
let doc = format!("# Example\n\n<!--markdownlint-disable MD013 -->\n{LONG}\n<!--markdownlint-enable MD013 -->\n");
let (_, after) = run("fmt", &[], &doc);
assert_eq!(
after, doc,
"the paragraph inside the disabled region must not be rewrapped"
);
}
#[test]
fn fmt_control_without_directive_rewraps() {
let doc = format!("# Example\n\n{LONG}\n");
assert_ne!(fmt(&doc), doc, "the harness must be able to observe a rewrap");
}
#[test]
fn every_tool_and_spelling_disables_a_block() {
for tool in ["markdownlint", "rumdl"] {
for open in spellings(&format!("{tool}-disable MD013")) {
for close in spellings(&format!("{tool}-enable MD013")) {
let doc = region(&open, &close);
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
}
}
}
#[test]
fn fmt_output_is_identical_for_every_spelling_and_a_fixed_point() {
let reference = fmt(®ion(
"<!-- markdownlint-disable MD013 -->",
"<!-- markdownlint-enable MD013 -->",
));
assert!(reference.contains(&format!("\n{LONG}\n<!--")), "{reference:?}");
for open in spellings("markdownlint-disable MD013") {
let doc = region(&open, "<!--markdownlint-enable MD013-->");
let once = fmt(&doc);
assert_eq!(skeleton(&once), skeleton(&reference), "structure differs for {open:?}");
assert!(once.contains(&format!("\n{LONG}\n")), "{once:?}");
assert_eq!(fmt(&once), once, "fmt is not idempotent for {open:?}");
}
}
#[test]
fn compact_disable_line_and_next_line_and_file() {
for tool in ["markdownlint", "rumdl"] {
for comment in spellings(&format!("{tool}-disable-line MD013")) {
let doc = format!("# T\n\n{LONG} {comment}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
for comment in spellings(&format!("{tool}-disable-next-line MD013")) {
let doc = format!("# T\n\n{comment}\n{LONG}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
for comment in spellings(&format!("{tool}-disable-file MD013")) {
let doc = format!("# T\n\n{comment}\n\n{LONG}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 0, "{doc:?}");
}
for comment in spellings(&format!("{tool}-disable-file")) {
let doc = format!("# T\n\n{comment}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 0, "{doc:?}");
}
}
}
#[test]
fn compact_enable_file_cancels_disable_file() {
let doc = format!("# T\n\n<!-- rumdl-disable-file MD013 -->\n<!--rumdl-enable-file MD013-->\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
#[test]
fn compact_capture_and_restore() {
let doc = format!(
"# T\n\n<!--markdownlint-disable MD013-->\n<!--markdownlint-capture-->\n<!--markdownlint-enable MD013-->\n{LONG}\n<!--markdownlint-restore-->\n\n{LONG}\n"
);
assert_eq!(
md013_warnings(&doc),
1,
"only the paragraph between enable and restore: {doc:?}"
);
}
#[test]
fn compact_prettier_ignore_disables_the_next_line() {
for comment in [
"<!-- prettier-ignore -->",
"<!--prettier-ignore-->",
"<!--prettier-ignore -->",
"<!-- prettier-ignore-->",
] {
let doc = format!("# T\n\n{comment}\n{LONG}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
for comment in ["<!--prettier-ignore-start-->", "<!-- prettier-ignore-end -->"] {
let doc = format!("# T\n\n{comment}\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
}
#[test]
fn compact_configure_file_one_line_and_multiline() {
for comment in [
"<!--markdownlint-configure-file { \"MD013\": { \"line_length\": 200 } }-->",
"<!--rumdl-configure-file { \"MD013\": { \"line-length\": 200 } } -->",
"<!--markdownlint-configure-file\n{\n \"MD013\": { \"line_length\": 200 }\n}\n-->",
"<!--markdownlint-configure-file { \"MD013\": false }-->",
] {
let doc = format!("# T\n\n{comment}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 0, "{doc:?}");
}
}
#[test]
fn compact_directive_inside_blockquote_and_list_item() {
let quote = format!("> <!--markdownlint-disable-next-line MD013-->\n> {LONG}\n\n{LONG}\n");
assert_eq!(md013_warnings("e), 1, "{quote:?}");
let list = format!("- item\n\n <!--markdownlint-disable-next-line MD013-->\n {LONG}\n\n{LONG}\n");
assert_eq!(md013_warnings(&list), 1, "{list:?}");
}
#[test]
fn two_directives_with_mixed_spacing_on_one_line() {
for line in [
"<!-- markdownlint-disable MD013 --> <!--markdownlint-enable MD013-->",
"<!--markdownlint-disable MD013--> <!-- markdownlint-enable MD013 -->",
] {
let doc = format!("# T\n\n{line}\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{doc:?}");
}
}
#[test]
fn near_misses_are_not_directives() {
for comment in [
"<!--markdownlint-disablefoo MD013 -->",
"<!--rumdl-disableMD013-->",
"<!--RUMDL-disable MD013 -->",
"<!--Markdownlint-Disable MD013-->",
"<!--note markdownlint-disable MD013 -->",
"<!---- markdownlint-disable MD013 -->",
] {
let doc = format!("# T\n\n{comment}\n\n{LONG}\n");
assert_eq!(md013_warnings(&doc), 1, "{comment:?} must not disable anything");
assert_ne!(fmt(&doc), doc, "{comment:?} must not protect the paragraph from fmt");
}
}
#[test]
fn directives_in_code_are_not_directives() {
let span = format!("# T\n\n`<!--markdownlint-disable MD013-->`\n\n{LONG}\n");
assert_eq!(md013_warnings(&span), 1, "{span:?}");
let fenced = format!("# T\n\n```markdown\n<!--markdownlint-disable MD013-->\n```\n\n{LONG}\n");
assert_eq!(md013_warnings(&fenced), 1, "{fenced:?}");
let indented = format!("# T\n\n <!--markdownlint-disable MD013-->\n\n{LONG}\n");
assert_eq!(md013_warnings(&indented), 1, "{indented:?}");
}
fn md044_findings(doc: &str) -> String {
let dir = tempdir().unwrap();
let path = dir.path().join("doc.md");
fs::write(&path, doc).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--isolated", "--no-cache", "--enable", "MD044"])
.args(["--config", "MD044.names=[\"Markdownlint\", \"Rumdl\"]"])
.arg(&path)
.output()
.expect("run rumdl");
assert!(
matches!(output.status.code(), Some(0 | 1)),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}
#[test]
fn compact_directive_text_is_not_checked_as_prose_by_md044() {
for line in [
"<!-- markdownlint-disable MD013 -->",
"<!--markdownlint-disable MD013 -->",
"<!--rumdl-disable MD013-->",
"<!--\trumdl-enable MD013-->",
] {
let stdout = md044_findings(&format!("# T\n\n{line}\n\ntext\n"));
assert!(!stdout.contains("[MD044]"), "{line:?}: {stdout}");
}
for line in ["<!-- note markdownlint here -->", "<!--note markdownlint here-->"] {
let stdout = md044_findings(&format!("# T\n\n{line}\n\ntext\n"));
assert!(stdout.contains("[MD044]"), "{line:?} must still be checked: {stdout}");
}
}