use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::heading_utils::HeadingStyle;
use rumdl_lib::rules::{
MD001HeadingIncrement, MD003HeadingStyle, MD022BlanksAroundHeadings, MD023HeadingStartLeft,
MD024NoDuplicateHeading, MD025SingleTitle,
};
#[test]
fn test_md001_edge_cases() {
let rule = MD001HeadingIncrement::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 headings should be valid for MD001");
let content = "\
### Starting at level 3
#### Next level
##### Another level";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Starting at any level is valid for MD001");
let content = "\
# Level 1
##### Level 5 jump";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect large heading jump");
assert!(result[0].message.contains('5'));
let content = "\
# Title
## Section
### Subsection
# New Title
## New Section";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Heading resets to level 1 should be valid");
let content = "\
# ATX Level 1
Setext Level 2
--------------
### ATX Level 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Mixed heading styles should work for MD001");
let content = "\
Setext Level 1
==============
#### ATX Level 4";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect skip from Setext h1 to ATX h4");
let content = "\
# Normal heading
## This is indented 4 spaces (code block)
### Next heading";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should detect h1 to h3 skip (h2 is a code block, not a heading)"
);
let content = "\
# 标题一 🚀
## Título Dos 🎯
### शीर्षक तीन 🌟";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Unicode headings should work correctly");
}
#[test]
fn test_md003_edge_cases() {
let rule = MD003HeadingStyle::new(HeadingStyle::Consistent);
let content = "\
## First heading is ATX
Another heading
===============";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Setext after ATX should be flagged in consistent mode");
let content = "\
Heading 1
=========
Heading 2
---------
### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD003HeadingStyle::new(HeadingStyle::Setext1);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Level 3+ must be ATX even in Setext mode");
let rule = MD003HeadingStyle::new(HeadingStyle::AtxClosed);
let content = "\
# Heading 1 #
## Heading 2 ###
### Heading 3 #";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Any number of closing hashes is valid");
let rule = MD003HeadingStyle::new(HeadingStyle::SetextWithAtx);
let content = "\
Heading 1
=========
Heading 2
---------
### Heading 3
#### Heading 4";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"SetextWithAtx should allow Setext for h1/h2, ATX for h3+"
);
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let content = "\
#
##
###";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Empty ATX headings should be valid");
let content = "\
---
title: Document
---
# First heading after front matter
## Second heading";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD003HeadingStyle::new(HeadingStyle::Consistent);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should handle YAML front matter correctly");
let content = "\
# **Bold** Heading
## *Italic* Heading
### `Code` Heading";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Inline formatting in headings should work");
}
#[test]
fn test_md022_edge_cases() {
let rule = MD022BlanksAroundHeadings::default();
let content = "\
# First heading
Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "First heading with blank line after should pass");
let content = "\
# Heading
```rust
code
```";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Code fence after heading doesn't need blank line");
let content = "\
# Heading
- List item
- Another item";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "List after heading doesn't need blank line");
let content = "# Only heading";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Heading at document end should be valid");
let content = "\
# Heading 1
## Heading 2
### Heading 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
4,
"Should require blanks around all headings (after H1, before H2, after H2, before H3)"
);
let content = "\
Content before
Setext Heading
==============
Content after";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"MD022 should enforce blank lines around Setext headings"
);
let content_ok = "\
Content before
Setext Heading
==============
Content after";
let ctx_ok = LintContext::new(content_ok, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result_ok = rule.check(&ctx_ok).unwrap();
assert!(
result_ok.is_empty(),
"Setext heading with proper blank lines should not warn"
);
let content = "\
---
title: Test
---
# First heading";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Frontmatter is transparent - heading can appear immediately after"
);
let content = "Content\r\n# Heading\r\nMore content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Should handle CRLF line endings correctly");
}
#[test]
fn test_md023_edge_cases() {
let rule = MD023HeadingStartLeft;
let content = "\
# No indent
# One space
## Two spaces
### Three spaces
#### Four spaces (code block)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
3,
"Should flag 3 indented headings (4-space line is a code block per CommonMark)"
);
let content = "\
Setext Heading
==============";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MD023 doesn't flag indented underlines when text is not indented"
);
let content = "\
# Correct
## Indented
### Correct again
#### Code block (ignored)
##### Correct";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should flag 1 indented heading (4-space line is a code block)"
);
let content = "";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Empty document should have no issues");
let content = "\
# Normal
\t# Tab indented";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Tab before heading is MD010's domain, not MD023");
let content = "\
Indented text
==============";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if result.is_empty() {
println!("Note: Indented Setext heading not recognized in test context");
} else {
assert_eq!(result.len(), 1, "Should flag indented Setext text");
}
}
#[test]
fn test_md024_edge_cases() {
let rule = MD024NoDuplicateHeading::default();
let content = "\
# Title
## title
### TITLE";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Different cases should be allowed by default");
let content = "\
# **Bold Title**
## *Italic Title*
### `Code Title`
#### [Link Title](url)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Different formatting should make headings unique");
let content = "\
# Title
## Title!
### Title?
#### Title.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Different punctuation should make headings unique");
let content = "\
#
##
#";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "MD024 doesn't flag empty headings as duplicates");
let rule = MD024NoDuplicateHeading::new(false, false); let content = "\
# 标题 🚀
## 标题 🎯
### Título
#### Título";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect duplicate Unicode headings");
let rule = MD024NoDuplicateHeading::new(true, false);
let content = "\
# Title
## Title
### Title";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Same text at different levels should be allowed");
let rule = MD024NoDuplicateHeading::new(false, false); let content = "\
# Title & More
## Title & More
### Title & More";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect duplicate with HTML entities");
let rule = MD024NoDuplicateHeading::new(false, false); let long_text = "a".repeat(200);
let content = format!("# {long_text}\n## {long_text}");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should handle very long duplicate headings");
let content = "\
# Title With Spaces
## Title With Spaces";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Different whitespace should make headings unique");
}
#[test]
fn test_md025_edge_cases() {
let rule = MD025SingleTitle::strict();
let rule_with_sections = MD025SingleTitle::new(1, "title");
let content = "\
# Main Title
## Content
# Appendix
## More content
# References";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule_with_sections.check(&ctx).unwrap();
assert!(result.is_empty(), "Document sections should be allowed");
let content = "\
---
title: YAML Title
---
# Markdown Title
## Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Body H1 should be flagged when frontmatter has title");
assert_eq!(result[0].line, 4);
let rule_with_separators = MD025SingleTitle::new(1, "title");
let content = "\
# First Title
## Content
---
# Second Title
## More content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule_with_separators.check(&ctx).unwrap();
assert!(result.is_empty(), "H1s with separators should be allowed");
let content = "\
# Title 1
***
# Title 2
___
# Title 3
- - -
# Title 4";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule_with_separators.check(&ctx).unwrap();
assert!(result.is_empty(), "All HR styles should work as separators");
let content = "\
#
## Content
#";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect multiple empty H1s");
let rule_h2 = MD025SingleTitle::new(2, "title");
let content = "\
# Title
## First H2
### Content
## Second H2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule_h2.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect multiple H2s when configured");
let content = "\
Main Title
==========
## Content
Another Title
=============";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect multiple Setext H1s");
let content = "\
# Real Title
```
# This is in a code block
```
## Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should ignore headings in code blocks");
let content = "\
#
## Content
# A";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should handle single-character headings");
}
#[test]
fn test_heading_rules_with_code_blocks() {
let content = "\
# Real Heading
```markdown
# This is in a code block
## Should be ignored
### By all rules
```
# Indented code block
## Also ignored
## Real Heading 2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md001 = MD001HeadingIncrement::default();
let result = md001.check(&ctx).unwrap();
assert!(result.is_empty(), "MD001 should ignore headings in code blocks");
let md003 = MD003HeadingStyle::new(HeadingStyle::Atx);
let result = md003.check(&ctx).unwrap();
assert!(result.is_empty(), "MD003 should ignore headings in code blocks");
let md022 = MD022BlanksAroundHeadings::default();
let result = md022.check(&ctx).unwrap();
assert!(result.is_empty(), "MD022 should ignore headings in code blocks");
let md023 = MD023HeadingStartLeft;
let result = md023.check(&ctx).unwrap();
assert!(result.is_empty(), "MD023 should ignore indented code blocks");
let md024 = MD024NoDuplicateHeading::default();
let result = md024.check(&ctx).unwrap();
assert!(result.is_empty(), "MD024 should not count headings in code blocks");
let md025 = MD025SingleTitle::strict();
let result = md025.check(&ctx).unwrap();
assert!(result.is_empty(), "MD025 should not count headings in code blocks");
}
#[test]
fn test_heading_rules_performance() {
let mut content = String::new();
for i in 1..=500 {
content.push_str(&format!("# Heading {i}\n\n"));
content.push_str(&format!("## Subheading {i}\n\n"));
content.push_str(&format!("### Sub-subheading {i}\n\n"));
content.push_str("Some content between headings.\n\n");
}
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let start = std::time::Instant::now();
let md001 = MD001HeadingIncrement::default();
let _ = md001.check(&ctx).unwrap();
let md001_time = start.elapsed();
let md003 = MD003HeadingStyle::new(HeadingStyle::Atx);
let start = std::time::Instant::now();
let _ = md003.check(&ctx).unwrap();
let md003_time = start.elapsed();
let md022 = MD022BlanksAroundHeadings::default();
let start = std::time::Instant::now();
let _ = md022.check(&ctx).unwrap();
let md022_time = start.elapsed();
let md023 = MD023HeadingStartLeft;
let start = std::time::Instant::now();
let _ = md023.check(&ctx).unwrap();
let md023_time = start.elapsed();
let md024 = MD024NoDuplicateHeading::default();
let start = std::time::Instant::now();
let _ = md024.check(&ctx).unwrap();
let md024_time = start.elapsed();
let md025 = MD025SingleTitle::strict();
let start = std::time::Instant::now();
let _ = md025.check(&ctx).unwrap();
let md025_time = start.elapsed();
assert!(md001_time.as_millis() < 200, "MD001 too slow: {md001_time:?}");
assert!(md003_time.as_millis() < 200, "MD003 too slow: {md003_time:?}");
assert!(md022_time.as_millis() < 200, "MD022 too slow: {md022_time:?}");
assert!(md023_time.as_millis() < 200, "MD023 too slow: {md023_time:?}");
assert!(md024_time.as_millis() < 200, "MD024 too slow: {md024_time:?}");
assert!(md025_time.as_millis() < 200, "MD025 too slow: {md025_time:?}");
}
#[test]
fn test_heading_rules_fix_generation() {
let content = "# Level 1\n### Level 3";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md001 = MD001HeadingIncrement::default();
let fixed = md001.fix(&ctx).unwrap();
assert_eq!(fixed, "# Level 1\n## Level 3", "MD001 should fix heading level");
let content = "# ATX\n\nSetext\n------";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md003 = MD003HeadingStyle::new(HeadingStyle::Atx);
let fixed = md003.fix(&ctx).unwrap();
assert_eq!(
fixed, "# ATX\n\n## Setext\n------",
"MD003 converts heading to ATX but preserves underline (bug?)"
);
let content = "text\n# Heading\nmore text";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md022 = MD022BlanksAroundHeadings::default();
let fixed = md022.fix(&ctx).unwrap();
assert_eq!(fixed, "text\n\n# Heading\n\nmore text", "MD022 should add blank lines");
let content = " # Indented";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md023 = MD023HeadingStartLeft;
let fixed = md023.fix(&ctx).unwrap();
assert_eq!(fixed, "# Indented", "MD023 should remove indentation");
let content = "# Title 1\n## Content\n# Title 2";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md025 = MD025SingleTitle::strict();
let fixed = md025.fix(&ctx).unwrap();
assert_eq!(
fixed, "# Title 1\n## Content\n## Title 2",
"MD025 should demote extra H1s"
);
}
#[test]
fn test_heading_rules_combined_scenarios() {
let content = "\
# My Blog Post
This is the introduction.
## First Section
### Details
Code example:
```
# This is a code comment
```
### More Details
# Conclusion";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md023 = MD023HeadingStartLeft;
let result = md023.check(&ctx).unwrap();
if result.is_empty() {
println!("Warning: MD023 didn't detect indented heading - possible LintContext parsing issue");
} else {
assert_eq!(result.len(), 1, "Should detect indented main heading");
}
let md022 = MD022BlanksAroundHeadings::default();
let result = md022.check(&ctx).unwrap();
assert!(!result.is_empty(), "Should detect missing blank lines");
let md025 = MD025SingleTitle::strict();
let result = md025.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect multiple H1s");
let content = "\
# API Documentation
## Authentication
### OAuth 2.0
#### Grant Types
##### Authorization Code
## Endpoints
### Users
#### GET /users
#### POST /users
### Orders
#### GET /orders";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md001 = MD001HeadingIncrement::default();
let result = md001.check(&ctx).unwrap();
assert!(result.is_empty(), "Proper increment should pass");
let md024 = MD024NoDuplicateHeading::default();
let result = md024.check(&ctx).unwrap();
assert!(result.is_empty(), "No duplicates should pass");
}
#[test]
fn test_heading_rules_unicode_edge_cases() {
let content = "\
# 🚀 Welcome مرحبا 歡迎
## 📝 Notes הערות 筆記
### ⚡ Performance प्रदर्शन 性能
#### 🎯 Goals أهداف 目標
# 🚀 Welcome مرحبا 歡迎";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md024 = MD024NoDuplicateHeading::default();
let result = md024.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should detect duplicate Unicode headings");
let md001 = MD001HeadingIncrement::default();
let result = md001.check(&ctx).unwrap();
assert!(result.is_empty(), "Unicode shouldn't affect heading increment");
let content = "\
# Title\u{200B}with\u{200B}zero\u{200B}width
## Title\u{200C}with\u{200C}zero\u{200C}width";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = md024.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Zero-width characters should make headings different"
);
}
#[test]
fn test_heading_rules_boundary_conditions() {
let content = "";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let md001 = MD001HeadingIncrement::default();
assert!(md001.check(&ctx).unwrap().is_empty());
let md003 = MD003HeadingStyle::new(HeadingStyle::Atx);
assert!(md003.check(&ctx).unwrap().is_empty());
let md022 = MD022BlanksAroundHeadings::default();
assert!(md022.check(&ctx).unwrap().is_empty());
let md023 = MD023HeadingStartLeft;
assert!(md023.check(&ctx).unwrap().is_empty());
let md024 = MD024NoDuplicateHeading::default();
assert!(md024.check(&ctx).unwrap().is_empty());
let md025 = MD025SingleTitle::strict();
assert!(md025.check(&ctx).unwrap().is_empty());
let content = "#";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = md001.check(&ctx).unwrap();
assert!(result.is_empty(), "Single # should be valid");
let content = " \n\n \t\n ";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = md025.check(&ctx).unwrap();
assert!(result.is_empty(), "Whitespace-only document should have no headings");
}
#[test]
fn test_list_items_not_setext_headings() {
let content = r#"- foo
-----"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"List item should not be treated as setext heading (CommonMark Example 99)"
);
let test_cases = vec![
"- Item\n---",
"* Item\n---",
"+ Item\n---",
"- Item\n===",
"* Item\n===",
"+ Item\n===",
];
for content in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"List item should not be treated as setext heading: {content}"
);
}
let test_cases = vec!["1. Item\n---", "2. Item\n===", "10. Item\n---"];
for content in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Numbered list should not be treated as setext heading: {content}"
);
}
let content = r#"- Apple
- Orange
-"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Incomplete list item should not trigger heading detection"
);
let content = r#"Regular paragraph text
----------------------"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let heading_count = ctx.valid_headings().count();
assert_eq!(heading_count, 1, "Valid paragraph setext heading should be detected");
let content = r#"> Quote
---"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Blockquote should not be setext heading content");
let content = r#"```rust
---"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Code fence should not be setext heading content");
let content = r#"<div>
---"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD022BlanksAroundHeadings::default();
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "HTML block should not be setext heading content");
let content = r#"- List item
- Another item
Valid Heading
============="#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let heading_count = ctx.valid_headings().count();
assert_eq!(heading_count, 1, "Should detect exactly 1 heading in mixed content");
}
#[test]
fn test_table_row_not_setext_heading_body_row() {
let content = "# Title\n\n| a | b |\n| - | - |\n| c | d |\n---\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(
headings.len(),
1,
"Only the ATX heading should be detected, not the table row"
);
assert_eq!(headings[0].heading.level, 1);
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"MD003 should not flag table rows as wrong heading style"
);
}
#[test]
fn test_table_row_not_setext_heading_delimiter_row() {
let content = "# Title\n\n| a | b |\n| - | - |\n---\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(headings.len(), 1, "Delimiter row + --- should not create a heading");
}
#[test]
fn test_table_row_not_setext_heading_cjk_content() {
let content = "\
## 📚 文档
| 资源 | 说明 |
| ---- | ---- |
| [用户服务 API 文档](/docs/api-guide.html) | REST API 接口定义及示例 |
| [开源大模型部署选型分析](/docs/analysis.html) | 大模型部署方案对比分析 |
---
> 资源持续补充中";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(headings.len(), 1, "Only the ATX heading should be detected");
assert_eq!(headings[0].heading.text, "📚 文档");
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "MD003 should not fire for table rows");
}
#[test]
fn test_table_row_not_setext_heading_equals_underline() {
let content = "# Title\n\n| a | b |\n| - | - |\n| c | d |\n===\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(headings.len(), 1, "Table row + === should not create a heading");
}
#[test]
fn test_pipe_without_table_context_is_setext_heading() {
let content = "# ATX heading\n\n| some text\n---\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(
headings.len(),
2,
"| some text followed by --- without table context is a Setext heading"
);
let rule = MD003HeadingStyle::new(HeadingStyle::Atx);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "MD003 should flag the Setext heading");
}
#[test]
fn test_multi_pipe_without_table_context_is_setext_heading() {
let content = "# ATX heading\n\n| a | b |\n---\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(
headings.len(),
2,
"Pipe line without delimiter row above is a Setext heading"
);
}
#[test]
fn test_table_in_blockquote_not_setext() {
let content = "# Title\n\n> | a | b |\n> | - | - |\n> | c | d |\n---\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(headings.len(), 1, "Only ATX heading should be detected");
}
#[test]
fn test_large_table_last_row_not_setext() {
let content = "# Title\n\n\
| col1 | col2 |\n\
| ---- | ---- |\n\
| r1 | r1 |\n\
| r2 | r2 |\n\
| r3 | r3 |\n\
| r4 | r4 |\n\
| r5 | r5 |\n\
| r6 | r6 |\n\
| r7 | r7 |\n\
| r8 | r8 |\n\
| r9 | r9 |\n\
| r10 | r10 |\n\
---\n";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings: Vec<_> = ctx.valid_headings().collect();
assert_eq!(
headings.len(),
1,
"Last row of large table + --- should not be a heading"
);
}