use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD003HeadingStyle;
use rumdl_lib::rules::heading_utils::HeadingStyle;
#[test]
fn test_consistent_atx() {
let rule = MD003HeadingStyle::default();
let content = "# Heading 1\n## Heading 2\n### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_consistent_atx_closed() {
let rule = MD003HeadingStyle::new(HeadingStyle::AtxClosed);
let content = "# Heading 1 #\n## Heading 2 ##\n### Heading 3 ###";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_mixed_styles() {
let rule = MD003HeadingStyle::default();
let content = "# Heading 1\n## Heading 2 ##\n### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 2);
}
#[test]
fn test_fix_mixed_styles() {
let rule = MD003HeadingStyle::default();
let content = "# Heading 1\n## Heading 2 ##\n### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, "# Heading 1\n## Heading 2\n### Heading 3");
}
#[test]
fn test_fix_to_atx_closed() {
let rule = MD003HeadingStyle::new(HeadingStyle::AtxClosed);
let content = "# Heading 1\n## Heading 2\n### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, "# Heading 1 #\n## Heading 2 ##\n### Heading 3 ###");
}
#[test]
fn test_indented_headings() {
let rule = MD003HeadingStyle::default();
let content = " # Heading 1\n ## Heading 2\n ### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_mixed_indentation() {
let rule = MD003HeadingStyle::default();
let content = "# Heading 1\n ## Heading 2\n ### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_preserve_content() {
let rule = MD003HeadingStyle::default();
let content = "# Heading with *emphasis* and **bold**\n## Another heading with [link](url)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, content);
}
#[test]
fn test_empty_headings() {
let rule = MD003HeadingStyle::default();
let content = "#\n##\n###";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_heading_with_trailing_space() {
let rule = MD003HeadingStyle::default();
let content = "# Heading 1 \n## Heading 2 \n### Heading 3 ";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_consistent_setext() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "Heading 1\n=========\n\nHeading 2\n---------";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_mixed_setext_atx() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "Heading 1\n=========\n\n## Heading 2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 4);
}
#[test]
fn test_fix_to_setext() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "# Heading 1\n## Heading 2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, "Heading 1\n=========\nHeading 2\n---------");
}
#[test]
fn test_setext_with_formatting() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "Heading with *emphasis*\n====================\n\nHeading with **bold**\n--------------------";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_fix_mixed_setext_atx() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "Heading 1\n=========\n\n## Heading 2\n### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, "Heading 1\n=========\n\nHeading 2\n---------\n### Heading 3");
}
#[test]
fn test_fix_setext_to_atx_removes_the_underline() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "Heading 1\n=========\n\nHeading 2\n---------\n\nBody text.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, "# Heading 1\n\n## Heading 2\n\nBody text.\n");
}
#[test]
fn test_fix_setext_to_atx_closed_removes_the_underline() {
let rule = MD003HeadingStyle::new(HeadingStyle::AtxClosed);
let content = "Heading 1\n=========\n\nBody text.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, "# Heading 1 #\n\nBody text.\n");
}
#[test]
fn test_fix_setext_to_atx_preserves_indentation() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = " Heading 1\n =========\n\nBody text.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx).unwrap();
assert_eq!(result, " # Heading 1\n\nBody text.\n");
}
#[test]
fn test_underline_inside_an_html_block_is_html() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
for content in ["# Intro\n\n<span>\nTitle\n===\n", "# Intro\n\n<span>\n===\n"] {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(rule.check(&ctx).unwrap().is_empty(), "{content:?}");
assert_eq!(rule.fix(&ctx).unwrap(), content);
}
let content = "# Intro\n\n<span>\n\nTitle\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(rule.fix(&ctx).unwrap(), "# Intro\n\n<span>\n\n# Title\n");
let content = "---\nhtml: |\n\n <span>\n---\nTitle\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(rule.fix(&ctx).unwrap(), "---\nhtml: |\n\n <span>\n---\n# Title\n");
}
#[test]
fn test_setext_heading_with_a_short_dash_underline_is_checked() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
for (content, fixed) in [
("Title\n-\n", "## Title\n"),
("Title\n--\n", "## Title\n"),
("Title\n- \n\nText\n", "## Title\n\nText\n"),
] {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(!rule.should_skip(&ctx), "{content:?}");
let lines: Vec<_> = rule.check(&ctx).unwrap().iter().map(|warning| warning.line).collect();
assert_eq!(lines, [1], "{content:?}");
assert_eq!(rule.fix(&ctx).unwrap(), fixed, "{content:?}");
}
}
#[test]
fn test_fmt_converts_a_setext_heading_with_a_one_character_underline() {
for (content, fixed) in [
("Title\n-\n", "## Title\n"),
("Title\n=\n", "# Title\n"),
("Title\n---\n", "## Title\n"),
] {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("doc.md");
std::fs::write(&file, content).unwrap();
let config = dir.path().join("rumdl.toml");
std::fs::write(&config, "[global]\nenable = [\"MD003\"]\n\n[MD003]\nstyle = \"atx\"\n").unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(dir.path())
.args(["fmt", "--color", "never", "--no-cache", "--config"])
.arg(&config)
.arg("doc.md")
.output()
.expect("rumdl runs");
assert!(
output.status.code().is_some(),
"fmt did not exit normally: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(std::fs::read_to_string(&file).unwrap(), fixed, "{content:?}");
}
}
#[test]
fn test_fix_is_idempotent_when_style_counts_tie() {
let rule = MD003HeadingStyle::default();
let content = "``\n---\n### #";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let once = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&once, rumdl_lib::config::MarkdownFlavor::Standard, None);
let twice = rule.fix(&ctx2).unwrap();
assert_eq!(once, twice, "MD003 fix is not idempotent");
}
#[test]
fn test_setext_with_indentation() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = " Heading 1\n =========\n\n Heading 2\n ---------";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_with_front_matter() {
let rule = MD003HeadingStyle::default();
let content = "---\ntitle: \"Test Document\"\nauthor: \"Test Author\"\ndate: \"2024-04-03\"\n---\n\n# Heading 1\n## Heading 2\n### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Expected no warnings with front matter followed by ATX headings, but got {} warnings",
result.len()
);
}
#[test]
fn test_yaml_like_content_detected_as_setext_heading() {
let rule = MD003HeadingStyle::default();
let content = "# Real Heading\n\n---\nconfig: value\nsetting: another value\n---\n\nMore content.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Expected warning for Setext heading style mismatch, but got none"
);
}
#[test]
fn test_legitimate_setext_headings_still_work() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "Main Title\n==========\n\nSubtitle\n--------\n\nContent here.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Legitimate Setext headings should still work, but got {} warnings: {:?}",
result.len(),
result
);
}
#[test]
fn test_setext_heading_starting_with_emphasis_is_checked() {
let rule = MD003HeadingStyle::default();
let content = "# Intro\n\n**Practice**\n---\n\nText.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "setext heading must be checked: {result:?}");
assert_eq!(result[0].line, 3);
assert_eq!(rule.fix(&ctx).unwrap(), "# Intro\n\n## **Practice**\n\nText.\n");
}
#[test]
fn test_setext_heading_in_footnote_body_is_checked() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "# Intro\n\nRef[^a].\n\n[^a]: Note.\n\n Heading\n ===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let lines: Vec<_> = rule.check(&ctx).unwrap().iter().map(|warning| warning.line).collect();
assert_eq!(lines, [7]);
assert_eq!(
rule.fix(&ctx).unwrap(),
"# Intro\n\nRef[^a].\n\n[^a]: Note.\n\n # Heading\n"
);
let content = "# Intro\n\nRef[^a].\n\n[^a]: Heading\n ===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(rule.check(&ctx).unwrap().is_empty());
assert_eq!(rule.fix(&ctx).unwrap(), content);
}
#[test]
fn test_mdx_underline_below_a_nested_closing_tag_is_paragraph_text() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "<Outer>\n<Inner>\ntext\n</Inner>\n===\n</Outer>\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MDX, None);
assert!(rule.check(&ctx).unwrap().is_empty());
assert_eq!(rule.fix(&ctx).unwrap(), content);
let content = "<Card>\ntext\n===\n</Card>\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MDX, None);
let lines: Vec<_> = rule.check(&ctx).unwrap().iter().map(|warning| warning.line).collect();
assert_eq!(lines, [2]);
assert_eq!(rule.fix(&ctx).unwrap(), "<Card>\n# text\n</Card>\n");
}
#[test]
fn test_mdx_underline_below_a_flow_expression_is_paragraph_text() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "# Intro\n\n{x}\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MDX, None);
assert!(rule.check(&ctx).unwrap().is_empty());
assert_eq!(rule.fix(&ctx).unwrap(), content);
let content = "# Intro\n\n{x}\ntext\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MDX, None);
let lines: Vec<_> = rule.check(&ctx).unwrap().iter().map(|warning| warning.line).collect();
assert_eq!(lines, [4]);
assert_eq!(rule.fix(&ctx).unwrap(), "# Intro\n\n{x}\n# text\n");
}
#[test]
fn test_fix_multi_line_setext_to_atx_replaces_the_whole_paragraph() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "Intro.\n\nFirst line\nsecond line\n===========\n\nBody.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "{result:?}");
assert_eq!(
(
result[0].line,
result[0].column,
result[0].end_line,
result[0].end_column
),
(3, 1, 4, 12)
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Intro.\n\n# First line second line\n\nBody.\n");
let ctx_fixed = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(rule.fix(&ctx_fixed).unwrap(), fixed, "MD003 fix is not idempotent");
}
#[test]
fn test_fix_multi_line_setext_h2_to_atx_replaces_the_whole_paragraph() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "Intro.\n\nFirst line\nsecond line\n---\n\nBody.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Intro.\n\n## First line second line\n\nBody.\n");
let ctx_fixed = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(rule.fix(&ctx_fixed).unwrap(), fixed, "MD003 fix is not idempotent");
}
#[test]
fn test_fix_multi_line_setext_to_atx_keeps_the_first_line_indent() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = " First\nsecond\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, " # First second\n");
let ctx_fixed = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(rule.fix(&ctx_fixed).unwrap(), fixed, "MD003 fix is not idempotent");
}
#[test]
fn test_fix_setext_with_a_hard_break_to_atx() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "Foo\\\nBar\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "# Foo Bar\n");
let ctx_fixed = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(rule.fix(&ctx_fixed).unwrap(), fixed, "MD003 fix is not idempotent");
}
#[test]
fn test_multi_line_setext_heading_kept_by_a_setext_style() {
let rule = MD003HeadingStyle::new(HeadingStyle::SetextWithAtx);
let content = "Intro.\n\nFirst line\nsecond line\n===========\n\nBody.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(rule.check(&ctx).unwrap().is_empty());
assert_eq!(rule.fix(&ctx).unwrap(), content);
}