use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD026NoTrailingPunctuation;
#[test]
fn test_md026_valid() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1\n## Heading 2\n### Heading 3\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_md026_invalid() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1!\n## Heading 2?\n### Heading 3.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 3); }
#[test]
fn test_md026_mixed() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1\n## Heading 2!\n### Heading 3\n";
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_md026_fix() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1!\n## Heading 2?\n### Heading 3.\n";
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\n");
}
#[test]
fn test_md026_custom_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some("!?".to_string()));
let content = "# Heading 1!\n## Heading 2?\n### Heading 3.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 2);
}
#[test]
fn test_md026_setext_headings() {
let rule = MD026NoTrailingPunctuation::default();
let content = "Heading 1!\n=======\nHeading 2?\n-------\n";
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, 1);
}
#[test]
fn test_md026_closed_atx() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1! #\n## Heading 2? ##\n";
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, 1);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "# Heading 1 #\n## Heading 2? ##\n");
}
#[test]
fn test_md026_empty_document() {
let rule = MD026NoTrailingPunctuation::default();
let content = "";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Empty documents should not produce warnings");
}
#[test]
fn test_md026_with_code_blocks() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Valid heading\n\n```\n# This is a code block with heading syntax!\n```\n\n```rust\n# This is another code block with a punctuation mark.\n```";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Content in code blocks should be ignored");
}
#[test]
fn test_md026_with_front_matter() {
let rule = MD026NoTrailingPunctuation::default();
let content = "---\ntitle: This is a title with punctuation!\ndate: 2023-01-01\n---\n\n# Correct heading\n## Heading with punctuation!\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Second heading should be flagged");
assert_eq!(result[0].line, 7);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"---\ntitle: This is a title with punctuation!\ndate: 2023-01-01\n---\n\n# Correct heading\n## Heading with punctuation\n",
"Fix should remove punctuation from heading only"
);
}
#[test]
fn test_md026_multiple_trailing_punctuation() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading with multiple marks!!!???\n## Another heading.....";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "# Heading with multiple marks!!!???\n## Another heading");
}
#[test]
fn test_md026_indented_headings() {
let rule = MD026NoTrailingPunctuation::default();
let content = " # Indented heading!\n ## Deeply indented heading?";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag exclamation mark");
assert_eq!(result[0].line, 1);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, " # Indented heading\n ## Deeply indented heading?");
}
#[test]
fn test_md026_fix_setext_headings() {
let rule = MD026NoTrailingPunctuation::default();
let content = "Heading 1!\n=======\nHeading 2?\n-------";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Heading 1\n=======\nHeading 2?\n-------");
}
#[test]
fn test_md026_performance() {
let rule = MD026NoTrailingPunctuation::default();
let mut content = String::new();
for i in 1..=100 {
content.push_str(&format!(
"# Heading {}{}\n\nSome content paragraph.\n\n",
i,
if i % 3 == 0 { "." } else { "" }
));
}
use std::time::Instant;
let start = Instant::now();
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let duration = start.elapsed();
assert_eq!(result.len(), 33, "Should detect exactly 33 headings with periods");
println!("MD026 performance test completed in {duration:?}");
assert!(
duration.as_millis() < 1000,
"Performance check should complete in under 1000ms"
);
}
#[test]
fn test_md026_non_standard_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some("@$%".to_string()));
let content = "# Heading 1@\n## Heading 2$\n### Heading 3%\n#### Heading 4#\n##### Heading 5!\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 2);
assert_eq!(result[2].line, 3);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"# Heading 1\n## Heading 2\n### Heading 3\n#### Heading 4#\n##### Heading 5!\n"
);
}
#[test]
fn test_md026_inline_code_with_punctuation() {
let rule = MD026NoTrailingPunctuation::default();
let content = r#"# Function `foo()`
## The `bar()` method
### Using `baz.`
#### Variable `x;`
##### Code `y,`"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Punctuation inside inline code should not be flagged");
}
#[test]
fn test_md026_unicode_punctuation() {
let rule = MD026NoTrailingPunctuation::default();
let content = r#"# Heading with ellipsis…
## Chinese full stop。
### Japanese period。
#### Arabic comma،
##### Spanish inverted exclamation¡
###### French guillemets»"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Unicode punctuation should not be flagged by default");
let unicode_rule = MD026NoTrailingPunctuation::new(Some("…。。،¡»".to_string()));
let unicode_result = unicode_rule.check(&ctx).unwrap();
assert_eq!(
unicode_result.len(),
6,
"All Unicode punctuation should be flagged when configured"
);
}
#[test]
fn test_md026_edge_cases() {
let rule = MD026NoTrailingPunctuation::default();
let content = r#"#
## Heading with spaces at end
### Heading with tab at end
#### Heading with newline immediately
#####Heading without space after hashes.
###### Multiple periods...
# Heading with (parentheses).
## Heading with [brackets].
### Heading with {braces}."#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
let violations: Vec<_> = result.iter().map(|w| (w.line, w.message.clone())).collect();
println!("Violations found: {violations:?}");
assert!(
violations.iter().any(|v| v.0 == 5),
"Heading without space should still be checked"
);
assert!(
violations.iter().any(|v| v.0 == 6),
"Multiple periods should be flagged"
);
assert!(
violations.iter().any(|v| v.0 == 7),
"Period after parentheses should be flagged"
);
assert!(
violations.iter().any(|v| v.0 == 8),
"Period after brackets should be flagged"
);
assert!(
violations.iter().any(|v| v.0 == 9),
"Period after braces should be flagged"
);
}
#[test]
fn test_md026_fix_preserves_formatting() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading with period. \n## Another heading with comma,\t\n###No space heading;";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"# Heading with period\n## Another heading with comma\n###No space heading;"
);
}
#[test]
fn test_md026_atx_closed_style_fix() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1. #\n## Heading 2, ##\n### Heading 3; ###";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "# Heading 1 #\n## Heading 2 ##\n### Heading 3 ###");
}
#[test]
fn test_md026_setext_with_various_punctuation() {
let rule = MD026NoTrailingPunctuation::default();
let content = r#"Heading with period.
========
Another with comma,
--------
Yet another with semicolon;
========"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 4);
assert_eq!(result[2].line, 7);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
r#"Heading with period
========
Another with comma
--------
Yet another with semicolon
========"#
);
}
#[test]
fn test_md026_deeply_nested_headings() {
let rule = MD026NoTrailingPunctuation::default();
let content = r#"###### Six level heading.
####### Seven hashes text.
######## Eight hashes text."#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "All lines with periods are flagged");
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 2);
assert_eq!(result[2].line, 3);
}
#[test]
fn test_md026_mixed_content() {
let rule = MD026NoTrailingPunctuation::default();
let content = r#"# Main Title
Some paragraph with punctuation.
## Section.
- List item.
- Another item.
### Subsection,
```
# This is code.
```
#### Final heading;
| Table | Header |
|-------|--------|
| Data. | More. |"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3);
let lines: Vec<_> = result.iter().map(|w| w.line).collect();
assert!(lines.contains(&5)); assert!(lines.contains(&10)); assert!(lines.contains(&16)); }
#[test]
fn test_md026_config_empty_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some("".to_string()));
let content = "# Heading!\n## Heading?\n### Heading.\n#### Heading,\n##### Heading;";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Empty punctuation config should not flag anything");
}
#[test]
fn test_md026_single_character_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some("!".to_string()));
let content = "# Warning!\n## Question?\n### Statement.\n#### List,\n##### Code;";
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, 1);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"# Warning\n## Question?\n### Statement.\n#### List,\n##### Code;"
);
}
#[test]
fn test_md026_special_regex_characters() {
let rule = MD026NoTrailingPunctuation::new(Some(".*+?[]{}()^$|\\".to_string()));
let content = r#"# Heading.
## Heading*
### Heading+
#### Heading?
##### Heading[
###### Heading]"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.len() >= 5,
"Should detect special regex characters as punctuation"
);
}
#[test]
fn test_md026_alphanumeric_as_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some("abc123XYZ".to_string()));
let content = r#"# Heading ending with a
## Heading ending with 1
### Heading ending with Z
#### Normal heading!
##### Heading ending with c
###### Heading ending with 3"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 5, "Should flag a, 1, Z, c, and 3");
assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 2); assert_eq!(result[2].line, 3); assert_eq!(result[3].line, 5); assert_eq!(result[4].line, 6);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"# Heading ending with \n## Heading ending with \n### Heading ending with \n#### Normal heading!\n##### Heading ending with \n###### Heading ending with "
);
}
#[test]
fn test_md026_mixed_alphanumeric_and_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some(".,!a1".to_string()));
let content = r#"# Heading.
## Heading!
### Heading a
#### Heading 1
##### Heading?"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 2); assert_eq!(result[2].line, 3); assert_eq!(result[3].line, 4); }
#[test]
fn test_md026_roundtrip_atx_headings() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Title.\n## Subtitle,\n### Sub-subtitle;\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx2).unwrap();
assert!(
result.is_empty(),
"Roundtrip: fix then re-check should produce 0 violations, got {result:?}"
);
}
#[test]
fn test_md026_roundtrip_setext_headings() {
let rule = MD026NoTrailingPunctuation::default();
let content = "Heading with period.\n========\n\nAnother with comma,\n--------\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx2).unwrap();
assert!(
result.is_empty(),
"Roundtrip: fix then re-check should produce 0 violations, got {result:?}"
);
}
#[test]
fn test_md026_roundtrip_closed_atx() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Heading 1! #\n## Heading 2. ##\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx2).unwrap();
assert!(
result.is_empty(),
"Roundtrip: fix then re-check should produce 0 violations, got {result:?}"
);
}
#[test]
fn test_md026_roundtrip_custom_punctuation() {
let rule = MD026NoTrailingPunctuation::new(Some("!?".to_string()));
let content = "# Warning!\n## Question?\n### Statement.\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx2).unwrap();
assert!(
result.is_empty(),
"Roundtrip: fix then re-check should produce 0 violations, got {result:?}"
);
}
#[test]
fn test_md026_roundtrip_multiple_punctuation() {
let rule = MD026NoTrailingPunctuation::default();
let content = "# Title...\n## Another heading!!\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx2).unwrap();
assert!(
result.is_empty(),
"Roundtrip: fix then re-check should produce 0 violations, got {result:?}"
);
}
#[test]
fn test_md026_roundtrip_mixed_content() {
let rule = MD026NoTrailingPunctuation::default();
let content =
"# Main Title\n\nSome paragraph.\n\n## Section.\n\n- List item.\n\n### Subsection,\n\n#### Final heading;\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx2).unwrap();
assert!(
result.is_empty(),
"Roundtrip: fix then re-check should produce 0 violations, got {result:?}"
);
}