use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD022BlanksAroundHeadings;
use rumdl_lib::utils::range_utils::LineIndex;
#[test]
fn test_valid_headings() {
let rule = MD022BlanksAroundHeadings::default();
let content = "Paragraph.\n\n# Heading 1\n\nContent.\n\n## Heading 2\n\nMore content.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_missing_blank_above() {
let rule = MD022BlanksAroundHeadings::default();
let content = "Paragraph.\n# Heading 1\nContent.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2); }
#[test]
fn test_missing_blank_below() {
let rule = MD022BlanksAroundHeadings::default();
let content = "# Heading 1\nContent.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_fix_headings() {
let rule = MD022BlanksAroundHeadings::default();
let content = "Paragraph.\n# Heading 1\nContent.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(fixed.contains("\n\n# Heading 1\n\n"));
}
#[test]
fn test_invalid_headings() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Heading 1\nSome content here.\n## Heading 2\nMore content here.\n### Heading 3\nFinal content.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_first_heading() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# First Heading\n\nSome content.\n\n## Second Heading\n\nMore content.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_code_block() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Heading\n\n```\n# Not a heading\n```";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
println!("Original content:\n{content}");
println!("Fixed content:\n{fixed}");
let warnings = _rule.check(&_fixed_ctx).unwrap();
println!("Warning count: {}", warnings.len());
for (i, warning) in warnings.iter().enumerate() {
println!("Warning {}: line {}, message: {}", i + 1, warning.line, warning.message);
}
assert!(fixed.contains("```"));
assert!(fixed.contains("# Not a heading"));
assert!(warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_front_matter() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "---\ntitle: Test\n---\n\n# First Heading\n\nContent here.\n\n## Second Heading\n\nMore content.";
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_headings() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "Text before.\n# Heading 1\nSome content here.\nText here\n## Heading 2\nMore content here.\nText here\n### Heading 3\nFinal content.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let warnings = _rule.check(&ctx).unwrap();
assert!(!warnings.is_empty());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_ne!(fixed, content);
let fixed_lines: Vec<&str> = fixed.lines().collect();
let original_lines: Vec<&str> = content.lines().collect();
assert!(fixed_lines.len() > original_lines.len());
assert!(fixed.contains("Some content here"));
assert!(fixed.contains("More content here"));
assert!(fixed.contains("Final content"));
assert!(fixed.contains("# Heading 1"));
assert!(fixed.contains("## Heading 2"));
assert!(fixed.contains("### Heading 3"));
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_custom_blank_lines() {
let _rule = MD022BlanksAroundHeadings::with_values(2, 2);
let content = "# Heading 1\nSome content here.\n## Heading 2\nMore content here.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(!result.is_empty());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_blanks_around_setext_headings() {
let _rule = MD022BlanksAroundHeadings::default();
let bad_content = "Some text\nHeading 1\n=========\nContent\nHeading 2\n---------\nMore content.";
let ctx = LintContext::new(bad_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let _bad_result = _rule.check(&ctx).unwrap();
let ctx = LintContext::new(bad_content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed_result = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_result.is_empty(), "Fixed setext headings should have no warnings");
}
#[test]
fn test_empty_content_headings() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "#\nSome content.\n##\nMore content.\n###\nFinal content.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(!result.is_empty());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
assert!(fixed != content);
assert!(fixed.contains('#'));
assert!(fixed.contains("##"));
assert!(fixed.contains("###"));
assert!(fixed.contains("Some content"));
assert!(fixed.contains("More content"));
assert!(fixed.contains("Final content"));
assert_eq!(fixed.matches("content").count(), content.matches("content").count());
}
#[test]
fn test_no_blanks_between_headings() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Heading 1\n## Heading 2\n### Heading 3\nContent here.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(!result.is_empty());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(fixed != content);
assert!(fixed.contains("# Heading 1"));
assert!(fixed.contains("## Heading 2"));
assert!(fixed.contains("### Heading 3"));
assert!(fixed.contains("Content here"));
let fixed_lines: Vec<&str> = fixed.lines().collect();
let original_lines: Vec<&str> = content.lines().collect();
assert!(fixed_lines.len() > original_lines.len());
}
#[test]
fn test_indented_headings() {
let _rule = MD022BlanksAroundHeadings::default();
let content = " # Heading 1\nContent 1.\n ## Heading 2\nContent 2.\n ### Heading 3\nContent 3.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Should detect blank line issues with indented headings"
);
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_ne!(fixed, content, "Fixed content should be different from original");
assert!(fixed.contains(" # Heading 1"));
assert!(fixed.contains(" ## Heading 2"));
assert!(fixed.contains(" ### Heading 3"));
assert!(fixed.contains("Content 1"));
assert!(fixed.contains("Content 2"));
assert!(fixed.contains("Content 3"));
let fixed_lines: Vec<&str> = fixed.lines().collect();
let original_lines: Vec<&str> = content.lines().collect();
assert!(
fixed_lines.len() > original_lines.len(),
"Fixed content should have more lines due to added blank lines"
);
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_code_block_detection() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Real Heading\n\nSome content.\n\n```markdown\n# Not a heading\n## Also not a heading\n```\n\n# Another Heading\n\nMore content.";
let index = LineIndex::new(content);
assert!(!index.is_code_block(0)); assert!(!index.is_code_block(2)); assert!(index.is_code_block(4)); assert!(index.is_code_block(5)); assert!(index.is_code_block(6)); assert!(index.is_code_block(7)); assert!(!index.is_code_block(9)); }
#[test]
fn test_line_index() {
let content = "# Heading 1\n\nSome text\n\n## Heading 2\n";
let index = LineIndex::new(content);
assert_eq!(index.line_col_to_byte_range(1, 1), 0..0);
assert_eq!(index.line_col_to_byte_range(1, 2), 1..1);
assert_eq!(index.line_col_to_byte_range(3, 1), 13..13);
}
#[test]
fn test_preserve_code_blocks() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Real Heading\nSome text\n\n```\n# Fake heading in code block\n```\n\nMore text";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(fixed.contains("```"));
assert!(fixed.contains("# Fake heading in code block"));
assert!(fixed.contains("# Real Heading"));
let fixed_result = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_result.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_fix_missing_blank_line_below() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Heading\nText";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(!result.is_empty());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert_eq!(fixed, "# Heading\n\nText");
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_fix_specific_blank_line_cases() {
let _rule = MD022BlanksAroundHeadings::default();
let simple_case = "# Heading\nContent";
let ctx = LintContext::new(simple_case, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(
fixed.contains("# Heading\n\nContent"),
"Should add blank line after heading"
);
let fixed_result = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_result.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_fix_with_various_content_types() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Heading 1\nParagraph 1\n```\nCode block\n```\n- List item 1\n- List item 2\n## Heading 2\n> Blockquote\n### Heading 3\nFinal paragraph";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(fixed.contains("# Heading 1"));
assert!(fixed.contains("## Heading 2"));
assert!(fixed.contains("### Heading 3"));
assert!(fixed.contains("Paragraph 1"));
assert!(fixed.contains("```\nCode block\n```"));
assert!(fixed.contains("- List item 1"));
assert!(fixed.contains("> Blockquote"));
assert!(fixed.contains("Final paragraph"));
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_regression_fix_works() {
let _rule = MD022BlanksAroundHeadings::default();
let content = "# Heading 1\nSome text\n\n## Heading 2\nMore text";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = _rule.check(&ctx).unwrap();
assert!(!result.is_empty());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let expected = "# Heading 1\n\nSome text\n\n## Heading 2\n\nMore text";
assert_eq!(fixed, expected);
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_multiple_consecutive_headings() {
let _rule = MD022BlanksAroundHeadings::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());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
assert!(fixed.contains("# Heading 1"));
assert!(fixed.contains("## Heading 2"));
assert!(fixed.contains("### Heading 3"));
let lines: Vec<&str> = fixed.lines().collect();
let h1_pos = lines.iter().position(|&l| l == "# Heading 1").unwrap();
let h2_pos = lines.iter().position(|&l| l == "## Heading 2").unwrap();
let h3_pos = lines.iter().position(|&l| l == "### Heading 3").unwrap();
assert!(h2_pos > h1_pos + 1, "Should have blank line(s) between h1 and h2");
assert!(h3_pos > h2_pos + 1, "Should have blank line(s) between h2 and h3");
assert!(
lines[h1_pos + 1].trim().is_empty(),
"Should have at least one blank line after h1"
);
assert!(
lines[h2_pos + 1].trim().is_empty(),
"Should have at least one blank line after h2"
);
let fixed_warnings = _rule.check(&_fixed_ctx).unwrap();
assert!(fixed_warnings.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_consecutive_headings_pattern() {
let _rule = MD022BlanksAroundHeadings::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());
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = _rule.fix(&ctx).unwrap();
let _fixed_ctx = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed_lines: Vec<&str> = fixed.lines().collect();
let h1_pos = fixed_lines.iter().position(|&l| l == "# Heading 1").unwrap();
let h2_pos = fixed_lines.iter().position(|&l| l == "## Heading 2").unwrap();
let h3_pos = fixed_lines.iter().position(|&l| l == "### Heading 3").unwrap();
assert!(
h2_pos > h1_pos + 1,
"Should have at least one blank line after first heading"
);
assert!(
h3_pos > h2_pos + 1,
"Should have at least one blank line after second heading"
);
assert!(
fixed_lines[h1_pos + 1].is_empty(),
"Should have blank line after first heading"
);
assert!(
fixed_lines[h2_pos + 1].is_empty(),
"Should have blank line after second heading"
);
}
fn assert_check_fix_roundtrip(content: &str, rule: &MD022BlanksAroundHeadings) {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fix_result = rule.fix(&ctx).unwrap();
let warnings = rule.check(&ctx).unwrap();
let warnings =
rumdl_lib::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), rule.name());
let apply_result = rumdl_lib::utils::fix_utils::apply_warning_fixes(content, &warnings).unwrap();
assert_eq!(
fix_result, apply_result,
"fix() and apply_warning_fixes(check()) must produce identical output\n\
--- fix() output ---\n{fix_result}\n\
--- apply_warning_fixes output ---\n{apply_result}"
);
let fixed_ctx = LintContext::new(&fix_result, rumdl_lib::config::MarkdownFlavor::Standard, None);
let re_warnings = rule.check(&fixed_ctx).unwrap();
assert!(
re_warnings.is_empty(),
"Fixed content should have no warnings, but got: {re_warnings:?}"
);
}
#[test]
fn test_roundtrip_simple_missing_blank_below() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip("# Heading\nText", &rule);
}
#[test]
fn test_roundtrip_missing_blank_above_and_below() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip("Text\n# Heading\nMore text", &rule);
}
#[test]
fn test_roundtrip_multiple_headings() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip(
"# Heading 1\nSome content.\n## Heading 2\nMore content.\n### Heading 3\nFinal.",
&rule,
);
}
#[test]
fn test_roundtrip_consecutive_headings() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip("# Heading 1\n## Heading 2\n### Heading 3", &rule);
}
#[test]
fn test_roundtrip_setext_headings() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip(
"Heading 1\n=========\nSome content.\nHeading 2\n---------\nMore content.",
&rule,
);
}
#[test]
fn test_roundtrip_already_valid() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip("# Heading 1\n\nSome content.\n\n## Heading 2\n\nMore content.", &rule);
}
#[test]
fn test_roundtrip_with_trailing_newline() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip("# Heading 1\nContent here.\n", &rule);
}
#[test]
fn test_roundtrip_heading_at_start() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip("# First Heading\nContent.\n\n## Second\nMore content.", &rule);
}
#[test]
fn test_roundtrip_custom_blank_lines() {
let rule = MD022BlanksAroundHeadings::with_values(2, 2);
assert_check_fix_roundtrip("# Heading 1\nContent.\n## Heading 2\nMore content.", &rule);
}
#[test]
fn test_roundtrip_code_block_heading() {
let rule = MD022BlanksAroundHeadings::default();
assert_check_fix_roundtrip(
"# Real Heading\nSome text\n\n```\n# Fake heading\n```\n\nMore text",
&rule,
);
}