use rumdl_lib::config::MarkdownFlavor;
use rumdl_lib::rule::{LintWarning, Rule};
use rumdl_lib::rules::{MD022BlanksAroundHeadings, MD065BlanksAroundHorizontalRules, MD090NoHrBeforeHeading};
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
fn lint(content: &str, rules: &[Box<dyn Rule>]) -> Vec<LintWarning> {
rumdl_lib::lint(content, rules, false, MarkdownFlavor::Standard, None, None).unwrap()
}
fn rumdl(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(dir)
.args(args)
.output()
.expect("rumdl runs")
}
fn fmt_with(input: &str, rule_args: &[&str]) -> String {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("doc.md");
fs::write(&file, input).unwrap();
let mut args = vec!["fmt", "--color", "never", "--no-cache", "--no-config"];
args.extend_from_slice(rule_args);
args.push("doc.md");
let output = rumdl(dir.path(), &args);
assert!(
output.status.code().is_some(),
"fmt did not exit normally: {}",
String::from_utf8_lossy(&output.stderr)
);
fs::read_to_string(&file).unwrap()
}
fn fmt(input: &str) -> String {
fmt_with(input, &["--enable", "MD022,MD065,MD090"])
}
#[test]
fn pipeline_reports_the_break_with_a_fix() {
let rules: Vec<Box<dyn Rule>> = vec![Box::new(MD090NoHrBeforeHeading::new())];
let warnings = lint("Prose.\n\n---\n\n## Next\n", &rules);
assert_eq!(warnings.len(), 1, "got: {warnings:?}");
assert_eq!(warnings[0].rule_name.as_deref(), Some("MD090"));
assert_eq!(warnings[0].line, 3);
assert!(warnings[0].fix.is_some());
}
#[test]
fn pipeline_with_md022_and_md065_reports_each_rule_once_on_tight_spacing() {
let rules: Vec<Box<dyn Rule>> = vec![
Box::new(MD022BlanksAroundHeadings::default()),
Box::new(MD065BlanksAroundHorizontalRules),
Box::new(MD090NoHrBeforeHeading::new()),
];
let warnings = lint("Prose\n\n---\n## Next\n", &rules);
let mut names: Vec<&str> = warnings.iter().filter_map(|w| w.rule_name.as_deref()).collect();
names.sort_unstable();
assert_eq!(names, ["MD022", "MD065", "MD090"], "got: {warnings:?}");
}
#[test]
fn fmt_must_fix_rows_produce_exact_bytes() {
let rows: &[(&str, &str, &str)] = &[
(
"tight spacing with MD022 and MD065 active",
"# T\n\nProse\n\n---\n## Next\n",
"# T\n\nProse\n\n## Next\n",
),
("break on first line", "---\n\n# Title\n", "# Title\n"),
(
"break after front matter",
"---\ntitle: x\n---\n\n---\n\n# Title\n",
"---\ntitle: x\n---\n\n# Title\n",
),
(
"run of breaks",
"# T\n\nProse\n\n---\n\n---\n\n## H\n",
"# T\n\nProse\n\n## H\n",
),
(
"break before setext heading",
"# T\n\nProse\n\n---\n\nNext topic\n----------\n",
"# T\n\nProse\n\nNext topic\n----------\n",
),
(
"tight break above setext heading keeps separation",
"# T\n\nProse\n***\nNext\n====\n",
"# T\n\nProse\n\nNext\n====\n",
),
(
"break directly after a table",
"# T\n\n| a |\n| - |\n| x |\n---\n\n## H\n",
"# T\n\n| a |\n| - |\n| x |\n\n## H\n",
),
(
"star run under paragraph text is a break, not an underline",
"# T\n\n*Label*\n***\n\n## Next\n",
"# T\n\n*Label*\n\n## Next\n",
),
(
"tight break above atx heading keeps separation",
"# T\n\nProse\n***\n## Next\n",
"# T\n\nProse\n\n## Next\n",
),
(
"tight run of breaks leaves a single blank line",
"# T\n\nProse\n***\n***\n## H\n",
"# T\n\nProse\n\n## H\n",
),
(
"space and tab line between break and heading is blank",
"# T\n\nProse\n\n---\n \t\n## H\n",
"# T\n\nProse\n\n## H\n",
),
(
"crlf document keeps crlf",
"# T\r\n\r\nProse\r\n\r\n---\r\n\r\n## H\r\n",
"# T\r\n\r\nProse\r\n\r\n## H\r\n",
),
(
"crlf run of breaks keeps crlf",
"# T\r\n\r\nProse\r\n\r\n---\r\n\r\n---\r\n\r\n## H\r\n",
"# T\r\n\r\nProse\r\n\r\n## H\r\n",
),
];
for (name, input, expected) in rows {
assert_eq!(&fmt(input), expected, "row {name:?}");
}
for marker in ["***", "___", "- - -", "* * *", " ---", "-----"] {
let input = format!("# T\n\nProse\n\n{marker}\n\n## H\n");
assert_eq!(fmt(&input), "# T\n\nProse\n\n## H\n", "marker {marker:?}");
}
let div = "<div markdown=\"1\">\n\n---\n\n## H\n\n</div>\n";
let div_fixed = "<div markdown=\"1\">\n\n## H\n\n</div>\n";
assert_eq!(&fmt(div), div_fixed, "markdown html block, standard flavor");
assert_eq!(
&fmt_with(div, &["--flavor", "mkdocs", "--enable", "MD022,MD065,MD090"]),
div_fixed,
"markdown html block, mkdocs flavor"
);
let div_break = "::: note\n---\n\n# H\n:::\n";
assert_eq!(
&fmt_with(div_break, &["--flavor", "quarto", "--enable", "MD022,MD065,MD090"]),
"::: note\n\n# H\n:::\n",
"break directly after div opener, quarto flavor"
);
assert_eq!(
&fmt_with(
"***\n::: note\n---\n\n# H\n:::\n",
&["--flavor", "quarto", "--enable", "MD090"]
),
"***\n::: note\n\n# H\n:::\n",
"break above a div opener is kept, break inside it is removed"
);
assert_eq!(
&fmt_with(
":::: outer\n\n***\n::: inner\n---\n\n# H\n:::\n::::\n",
&["--flavor", "quarto", "--enable", "MD090"]
),
":::: outer\n\n***\n::: inner\n\n# H\n:::\n::::\n",
"break above a nested div opener is kept"
);
assert_eq!(
&fmt_with(
"***\n/// note\n---\n\n# H\n///\n",
&["--flavor", "mkdocs", "--enable", "MD090"]
),
"***\n/// note\n\n# H\n///\n",
"break above a pymdown block opener is kept"
);
}
#[test]
fn fmt_must_keep_rows_are_byte_no_ops() {
let rows: &[(&str, &str)] = &[
("setext underline", "# T\n\nProse\n---\n\n## Next\n"),
("emphasis setext underline", "# T\n\n*Label*\n---\n\n## Next\n"),
(
"inline html setext underline",
"# T\n\n<span>Label</span>\n---\n\n## Next\n",
),
("hashtag setext underline", "# T\n\n#hashtag\n---\n\n## Next\n"),
("colon paragraph setext underline", "# T\n\n::: note\n---\n\n## Next\n"),
("pipe line that is not a table", "# T\n\n*Label | x*\n---\n\n## Next\n"),
("front matter only", "---\ntitle: x\n---\n\n# Title\n"),
("comment between", "# T\n\nProse\n\n---\n\n<!-- c -->\n\n## H\n"),
("break after heading", "# T\n\n## H\n\n---\n\nProse\n"),
("blockquote", "# T\n\n> ---\n>\n> ## H\n"),
("empty blockquote line between", "# T\n\nProse\n\n---\n\n>\n\n## H\n"),
("list item", "# T\n\n- item\n\n ---\n\n ## H\n"),
("fenced", "# T\n\n```text\n---\n```\n\n## H\n"),
("fence indented one space", "# T\n\n ```\n---\n```\n\n## H\n"),
("indented code", "# T\n\nProse\n\n ---\n\n## H\n"),
("attribute line between", "# T\n\nProse\n\n---\n\n{#custom}\n## H\n"),
("html comment", "# T\n\n<!--\n---\n-->\n\n## H\n"),
("hashtag is not a heading", "# T\n\nProse\n\n---\n\n#hashtag\n"),
("break before prose", "# T\n\nProse\n\n---\n\nMore prose\n"),
];
for (name, input) in rows {
assert_eq!(&fmt(input), input, "row {name:?}");
}
let math = "# T\n\n$$\n---\n$$\n\n## H\n";
assert_eq!(
&fmt_with(math, &["--enable", "MD065,MD090"]),
math,
"row \"math block\""
);
let nbsp = "# T\n\nProse\n\n***\n\u{00A0}\n## H\n";
assert_eq!(
&fmt_with(nbsp, &["--enable", "MD090"]),
nbsp,
"row \"nbsp line between\""
);
}
#[test]
fn fmt_default_rules_plus_md090_fix_the_reporter_document() {
let input = "# Title\n\n## Topic\n\nProse.\n\n---\n\n## Next Topic\n\nMore.\n";
assert_eq!(
fmt_with(input, &[]),
input,
"default rules alone must leave this document alone"
);
assert_eq!(
fmt_with(input, &["--extend-enable", "MD090"]),
"# Title\n\n## Topic\n\nProse.\n\n## Next Topic\n\nMore.\n"
);
}
#[test]
fn fmt_positive_control_rewrites_with_the_rule_and_not_without_it() {
let input = "# T\n\nProse.\n\n---\n\n## Next\n";
assert_eq!(fmt_with(input, &["--enable", "MD022,MD065"]), input);
assert_eq!(fmt(input), "# T\n\nProse.\n\n## Next\n");
}