use rumdl_lib::config::MarkdownFlavor;
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::{LintWarning, Rule};
use rumdl_lib::rules::*;
use rumdl_lib::rules::code_fence_utils::CodeFenceStyle;
use rumdl_lib::rules::emphasis_style::EmphasisStyle;
use rumdl_lib::rules::heading_utils::HeadingStyle;
use rumdl_lib::rules::strong_style::StrongStyle;
#[allow(dead_code)]
fn apply_fix(content: &str, fix: &rumdl_lib::rule::Fix) -> String {
let mut result = content.to_string();
result.replace_range(fix.range.clone(), &fix.replacement);
result
}
fn apply_all_fixes(content: &str, warnings: &[LintWarning]) -> String {
let mut fixes: Vec<_> = warnings.iter().filter_map(|w| w.fix.as_ref()).collect();
fixes.sort_by(|a, b| b.range.start.cmp(&a.range.start));
let mut result = content.to_string();
for fix in fixes {
if fix.range.end <= result.len() {
result.replace_range(fix.range.clone(), &fix.replacement);
}
}
result
}
fn assert_fix_idempotent(rule: &dyn Rule, content: &str, rule_name: &str) {
let ctx1 = LintContext::new(content, MarkdownFlavor::Standard, None);
let warnings1 = rule.check(&ctx1).unwrap_or_default();
let content1 = apply_all_fixes(content, &warnings1);
let ctx2 = LintContext::new(&content1, MarkdownFlavor::Standard, None);
let warnings2 = rule.check(&ctx2).unwrap_or_default();
let content2 = apply_all_fixes(&content1, &warnings2);
assert_eq!(
content1, content2,
"{rule_name} fix is not idempotent!\nAfter first fix:\n{content1:?}\nAfter second fix:\n{content2:?}"
);
}
#[allow(dead_code)]
fn assert_fix_idempotent_with_flavor(rule: &dyn Rule, content: &str, rule_name: &str, flavor: MarkdownFlavor) {
let ctx1 = LintContext::new(content, flavor, None);
let warnings1 = rule.check(&ctx1).unwrap_or_default();
let content1 = apply_all_fixes(content, &warnings1);
let ctx2 = LintContext::new(&content1, flavor, None);
let warnings2 = rule.check(&ctx2).unwrap_or_default();
let content2 = apply_all_fixes(&content1, &warnings2);
assert_eq!(
content1, content2,
"{rule_name} fix is not idempotent!\nAfter first fix:\n{content1:?}\nAfter second fix:\n{content2:?}"
);
}
#[test]
fn test_md001_fix_idempotent() {
let rule = MD001HeadingIncrement::default();
let content = "# Title\n\n### Skipped Level\n\n## Back\n";
assert_fix_idempotent(&rule, content, "MD001");
}
#[test]
fn test_md003_fix_idempotent_atx() {
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "Title\n=====\n\nSubtitle\n--------\n";
assert_fix_idempotent(&rule, content, "MD003");
}
#[test]
fn test_md003_fix_idempotent_setext() {
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let content = "# Title\n\n## Subtitle\n";
assert_fix_idempotent(&rule, content, "MD003");
}
#[test]
fn test_md004_fix_idempotent_dash() {
let rule = MD004UnorderedListStyle::new(UnorderedListStyle::Dash);
let content = "* Item 1\n+ Item 2\n* Item 3\n";
assert_fix_idempotent(&rule, content, "MD004");
}
#[test]
fn test_md004_fix_idempotent_asterisk() {
let rule = MD004UnorderedListStyle::new(UnorderedListStyle::Asterisk);
let content = "- Item 1\n+ Item 2\n- Item 3\n";
assert_fix_idempotent(&rule, content, "MD004");
}
#[test]
fn test_md005_fix_idempotent() {
let rule = MD005ListIndent::default();
let content = "- Item 1\n - Nested\n - Deep\n";
assert_fix_idempotent(&rule, content, "MD005");
}
#[test]
fn test_md007_fix_idempotent() {
let rule = MD007ULIndent::default();
let content = "- Item\n - Wrong indent\n";
assert_fix_idempotent(&rule, content, "MD007");
}
#[test]
fn test_md009_fix_idempotent() {
let rule = MD009TrailingSpaces::default();
let content = "Line with trailing spaces \n\nAnother line \n";
assert_fix_idempotent(&rule, content, "MD009");
}
#[test]
fn test_md010_fix_idempotent() {
let rule = MD010NoHardTabs::default();
let content = "Line with\ttab\n\nAnother\t\ttabs\n";
assert_fix_idempotent(&rule, content, "MD010");
}
#[test]
fn test_md012_fix_idempotent() {
let rule = MD012NoMultipleBlanks::default();
let content = "# Title\n\n\n\nParagraph\n\n\n\nEnd\n";
assert_fix_idempotent(&rule, content, "MD012");
}
#[test]
fn test_md014_fix_idempotent() {
let rule = MD014CommandsShowOutput::default();
let content = "```sh\n$ echo hello\n$ ls\n```\n";
assert_fix_idempotent(&rule, content, "MD014");
}
#[test]
fn test_md018_fix_idempotent() {
let rule = MD018NoMissingSpaceAtx::new();
let content = "#Title\n\n##Subtitle\n";
assert_fix_idempotent(&rule, content, "MD018");
}
#[test]
fn test_md019_fix_idempotent() {
let rule = MD019NoMultipleSpaceAtx;
let content = "# Title\n\n## Subtitle\n";
assert_fix_idempotent(&rule, content, "MD019");
}
#[test]
fn test_md020_fix_idempotent() {
let rule = MD020NoMissingSpaceClosedAtx;
let content = "#Title#\n\n##Subtitle##\n";
assert_fix_idempotent(&rule, content, "MD020");
}
#[test]
fn test_md021_fix_idempotent() {
let rule = MD021NoMultipleSpaceClosedAtx;
let content = "# Title #\n\n## Subtitle ##\n";
assert_fix_idempotent(&rule, content, "MD021");
}
#[test]
fn test_md022_fix_idempotent() {
let rule = MD022BlanksAroundHeadings::default();
let content = "# Title\nParagraph\n## Subtitle\nMore text\n";
assert_fix_idempotent(&rule, content, "MD022");
}
#[test]
fn test_md023_fix_idempotent() {
let rule = MD023HeadingStartLeft;
let content = " # Indented Title\n\n ## More Indented\n";
assert_fix_idempotent(&rule, content, "MD023");
}
#[test]
fn test_md026_fix_idempotent() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Title:\n\n## Subtitle!\n";
assert_fix_idempotent(&rule, content, "MD026");
}
#[test]
fn test_md027_fix_idempotent() {
let rule = MD027MultipleSpacesBlockquote::default();
let content = "> Multiple spaces\n> More spaces\n";
assert_fix_idempotent(&rule, content, "MD027");
}
#[test]
fn test_md028_fix_idempotent() {
let rule = MD028NoBlanksBlockquote;
let content = "> First quote\n\n> Second quote\n";
assert_fix_idempotent(&rule, content, "MD028");
}
#[test]
fn test_md029_fix_idempotent_ordered() {
let rule = MD029OrderedListPrefix::new(ListStyle::Ordered);
let content = "1. First\n1. Second\n1. Third\n";
assert_fix_idempotent(&rule, content, "MD029");
}
#[test]
fn test_md029_fix_idempotent_one() {
let rule = MD029OrderedListPrefix::new(ListStyle::One);
let content = "1. First\n2. Second\n3. Third\n";
assert_fix_idempotent(&rule, content, "MD029");
}
#[test]
fn test_md030_fix_idempotent() {
let rule = MD030ListMarkerSpace::default();
let content = "- Two spaces\n- Three spaces\n";
assert_fix_idempotent(&rule, content, "MD030");
}
#[test]
fn test_md031_fix_idempotent() {
let rule = MD031BlanksAroundFences::default();
let content = "Text\n```\ncode\n```\nMore text\n";
assert_fix_idempotent(&rule, content, "MD031");
}
#[test]
fn test_md032_fix_idempotent() {
let rule = MD032BlanksAroundLists::default();
let content = "Text\n- Item 1\n- Item 2\nMore text\n";
assert_fix_idempotent(&rule, content, "MD032");
}
#[test]
fn test_md032_edge_case_proptest_found() {
let rule = MD032BlanksAroundLists::default();
let content = "- \n# \n**\n2. \n# ";
assert_fix_idempotent(&rule, content, "MD032");
}
#[test]
fn test_md032_edge_case_code_fence_after_ordered_non1() {
let rule = MD032BlanksAroundLists::default();
let content = "- \n# \n**\n2. \n```\n\n```";
assert_fix_idempotent(&rule, content, "MD032");
}
#[test]
fn test_md034_fix_idempotent() {
let rule = MD034NoBareUrls;
let content = "Visit https://example.com for more info.\n";
assert_fix_idempotent(&rule, content, "MD034");
}
#[test]
fn test_md035_fix_idempotent() {
let rule = MD035HRStyle::new("---".to_string());
let content = "# Title\n\n***\n\nText\n";
assert_fix_idempotent(&rule, content, "MD035");
}
#[test]
fn test_md037_fix_idempotent() {
let rule = MD037NoSpaceInEmphasis;
let content = "This is * emphasized * text.\n";
assert_fix_idempotent(&rule, content, "MD037");
}
#[test]
fn test_md038_fix_idempotent() {
let rule = MD038NoSpaceInCode::default();
let content = "Use ` code ` here.\n";
assert_fix_idempotent(&rule, content, "MD038");
}
#[test]
fn test_md039_fix_idempotent() {
let rule = MD039NoSpaceInLinks;
let content = "Click [ here ](https://example.com) to continue.\n";
assert_fix_idempotent(&rule, content, "MD039");
}
#[test]
fn test_md044_fix_idempotent() {
let rule = MD044ProperNames::new(
vec!["JavaScript".to_string(), "TypeScript".to_string()],
false, );
let content = "Learn javascript and typescript.\n";
assert_fix_idempotent(&rule, content, "MD044");
}
#[test]
fn test_md047_fix_idempotent_no_newline() {
let rule = MD047SingleTrailingNewline;
let content = "Content without trailing newline";
assert_fix_idempotent(&rule, content, "MD047");
}
#[test]
fn test_md047_fix_idempotent_multiple_newlines() {
let rule = MD047SingleTrailingNewline;
let content = "Content with multiple trailing newlines\n\n\n";
assert_fix_idempotent(&rule, content, "MD047");
}
#[test]
fn test_md048_fix_idempotent_backtick() {
let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Backtick);
let content = "~~~\ncode\n~~~\n";
assert_fix_idempotent(&rule, content, "MD048");
}
#[test]
fn test_md048_fix_idempotent_tilde() {
let rule = MD048CodeFenceStyle::new(CodeFenceStyle::Tilde);
let content = "```\ncode\n```\n";
assert_fix_idempotent(&rule, content, "MD048");
}
#[test]
fn test_md049_fix_idempotent_asterisk() {
let rule = MD049EmphasisStyle::new(EmphasisStyle::Asterisk);
let content = "This is _emphasized_ text.\n";
assert_fix_idempotent(&rule, content, "MD049");
}
#[test]
fn test_md049_fix_idempotent_underscore() {
let rule = MD049EmphasisStyle::new(EmphasisStyle::Underscore);
let content = "This is *emphasized* text.\n";
assert_fix_idempotent(&rule, content, "MD049");
}
#[test]
fn test_md050_fix_idempotent_asterisk() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is __strong__ text.\n";
assert_fix_idempotent(&rule, content, "MD050");
}
#[test]
fn test_md050_fix_idempotent_underscore() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = "This is **strong** text.\n";
assert_fix_idempotent(&rule, content, "MD050");
}
#[test]
fn test_md058_fix_idempotent() {
let rule = MD058BlanksAroundTables::default();
let content = "Text\n| A | B |\n|---|---|\n| 1 | 2 |\nMore text\n";
assert_fix_idempotent(&rule, content, "MD058");
}
#[test]
fn test_md064_fix_idempotent() {
let rule = MD064NoMultipleConsecutiveSpaces::default();
let content = "Text with multiple spaces.\n";
assert_fix_idempotent(&rule, content, "MD064");
}
#[test]
fn test_md065_fix_idempotent() {
let rule = MD065BlanksAroundHorizontalRules;
let content = "Text\n---\nMore text\n";
assert_fix_idempotent(&rule, content, "MD065");
}
#[test]
fn test_md071_fix_idempotent() {
let rule = MD071BlankLineAfterFrontmatter;
let content = "---\ntitle: Test\n---\n# Title\n";
assert_fix_idempotent(&rule, content, "MD071");
}
#[test]
fn test_md072_fix_idempotent() {
let rule = MD072FrontmatterKeySort::default();
let content = "---\nzebra: 1\napple: 2\n---\n\n# Title\n";
assert_fix_idempotent(&rule, content, "MD072");
}
#[test]
fn test_complex_document_idempotent() {
let rules: Vec<Box<dyn Rule>> = vec![
Box::new(MD009TrailingSpaces::default()),
Box::new(MD012NoMultipleBlanks::default()),
Box::new(MD022BlanksAroundHeadings::default()),
Box::new(MD032BlanksAroundLists::default()),
Box::new(MD047SingleTrailingNewline),
];
let content = "# Title \n\n\n\nParagraph\n- Item 1\n- Item 2\nMore text\n\n";
let mut current = content.to_string();
for rule in &rules {
let ctx = LintContext::new(¤t, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap_or_default();
current = apply_all_fixes(¤t, &warnings);
}
let after_first = current.clone();
for rule in &rules {
let ctx = LintContext::new(¤t, MarkdownFlavor::Standard, None);
let warnings = rule.check(&ctx).unwrap_or_default();
current = apply_all_fixes(¤t, &warnings);
}
let after_second = current;
assert_eq!(
after_first, after_second,
"Combined fixes are not idempotent!\nAfter first pass:\n{after_first:?}\nAfter second pass:\n{after_second:?}"
);
}