use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD051LinkFragments;
fn assert_fragments(test_cases: &[(&str, &str)]) {
let rule = MD051LinkFragments::new();
for (heading, expected_fragment) in test_cases {
let content = format!("# {heading}\n\n[Link](#{expected_fragment})");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Fragment generation failed for heading '{}': expected '{}' but link was flagged as broken. Warnings: {:?}",
heading,
expected_fragment,
result.iter().map(|w| &w.message).collect::<Vec<_>>()
);
}
}
#[test]
fn test_punctuation_removal_order() {
assert_fragments(&[
("A & B", "a--b"), ("A --> B", "a----b"), ("A: B", "a-b"), ("A (B) C", "a-b-c"), ("A!!! B", "a-b"), ("Pre -> Post", "pre---post"), ("Before: After", "before-after"), ]);
}
#[test]
fn test_consecutive_punctuation_handling() {
assert_fragments(&[("cbrown --> sbrown: --unsafe-paths", "cbrown----sbrown---unsafe-paths")]);
assert_fragments(&[("cbrown -> sbrown", "cbrown---sbrown")]);
assert_fragments(&[
("API!!! Methods??? & Properties", "api-methods--properties"), ("Step 1: (Optional) Setup", "step-1-optional-setup"),
("Testing & Coverage & More", "testing--coverage--more"), ("One -> Two -> Three", "one---two---three"), ("A: B: C", "a-b-c"),
]);
}
#[test]
fn test_ampersand_handling_variations() {
assert_fragments(&[
("Testing & Coverage", "testing--coverage"), ("A&B", "ab"), ("A & B & C", "a--b--c"), ("Testing&Development", "testingdevelopment"), ("API & Documentation", "api--documentation"), ]);
}
#[test]
fn test_complex_punctuation_clusters() {
assert_fragments(&[
("Title: (Part 1) - Overview", "title-part-1---overview"), ("FAQ??? What's New!!!", "faq-whats-new"),
("Step 1: Setup (Required)", "step-1-setup-required"),
(
"API Reference: Methods & Properties",
"api-reference-methods--properties", ),
("Version 2.0: New Features & Fixes", "version-20-new-features--fixes"), ("Install: (macOS) & (Windows)", "install-macos--windows"), ]);
}
#[test]
fn test_special_character_edge_cases() {
assert_fragments(&[
("Quote \"Test\" Unquote", "quote-test-unquote"),
("Em—Dash & En–Dash", "emdash--endash"), ("Symbols: @#$%^&*()", "symbols-"), ("Math: x + y = z", "math-x--y--z"), ("Code: foo(bar)", "code-foobar"),
("File.ext: Details", "fileext-details"),
]);
}
#[test]
fn test_unicode_punctuation_edge_cases() {
assert_fragments(&[
("Smart \"Quotes\" Test", "smart-quotes-test"),
("List • Item • Two", "list--item--two"), ("Range … Ellipsis", "range--ellipsis"), ("Math ÷ Division", "math--division"), ("Degree 90° Angle", "degree-90-angle"), ("Currency $100€ Price", "currency-100-price"), ]);
}
#[test]
fn test_mixed_script_edge_cases() {
assert_fragments(&[
("English & 中文", "english--中文"), ("Café & Restaurant", "café--restaurant"), ("Naïve & Smart", "naïve--smart"), ("Русский & English", "русский--english"), ("العربية & English", "العربية--english"), ]);
}
#[test]
fn test_whitespace_punctuation_interaction() {
assert_fragments(&[
("A & B", "a--b"), ("Trailing & ", "trailing-"), (" & Leading", "--leading"), ("Multiple Spaces", "multiple---spaces"), ]);
}
#[test]
fn test_edge_case_heading_structures() {
assert_fragments(&[
("Hash in Heading", "hash-in-heading"),
("Heading with [brackets]", "heading-with-brackets"),
("*Emphasis* in Heading", "emphasis-in-heading"),
("`Code` in Heading", "code-in-heading"),
("**Bold** & *Italic*", "bold--italic"),
("Link [text](url) stripped", "link-text-stripped"),
]);
}
#[test]
fn test_kramdown_vs_github_differences() {
use rumdl_lib::utils::anchor_styles::AnchorStyle;
let github_rule = MD051LinkFragments::with_anchor_style(AnchorStyle::GitHub);
let kramdown_rule = MD051LinkFragments::with_anchor_style(AnchorStyle::Kramdown);
let kramdown_gfm_rule = MD051LinkFragments::with_anchor_style(AnchorStyle::KramdownGfm);
let test_cases = vec![
("test_method", "test_method", "test_method", "testmethod"), ("Café Menu", "café-menu", "café-menu", "caf-menu"), ("über_cool", "über_cool", "über_cool", "bercool"), ("naïve_approach", "naïve_approach", "naïve_approach", "naveapproach"), ];
for (heading, github_expected, kramdown_gfm_expected, kramdown_expected) in test_cases {
let content = format!("# {heading}\n\n[Link](#{github_expected})");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = github_rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"GitHub mode failed for '{heading}' -> '{github_expected}'"
);
let content = format!("# {heading}\n\n[Link](#{kramdown_gfm_expected})");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = kramdown_gfm_rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Kramdown GFM mode failed for '{heading}' -> '{kramdown_gfm_expected}'"
);
let content = format!("# {heading}\n\n[Link](#{kramdown_expected})");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = kramdown_rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Pure Kramdown mode failed for '{heading}' -> '{kramdown_expected}'"
);
}
}
#[test]
fn test_liquid_template_complex_patterns() {
let rule = MD051LinkFragments::new();
let content = r#"# Real Heading
## Another Section
These Liquid patterns should be ignored:
[Complex post]({% post_url 2023-03-25-post param="value" %}#section)
[Variable include]({% include file.html var=site.data %}#anchor)
[Nested tags]({% assign x = "val" %}{% link {{ x }}.md %}#frag)
[Variable path]({{ site.url }}/{{ page.slug }}#section)
[Complex variable]({{ site.posts | where: "slug", "test" | first | url }}#anchor)
Valid internal links:
[Should work](#real-heading)
[Should also work](#another-section)
[Should fail](#missing-section)
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only warn about missing-section");
assert!(result[0].message.contains("missing-section"));
}
#[test]
fn test_cross_file_detection_edge_cases() {
let rule = MD051LinkFragments::new();
let content = r#"# Main Heading
Cross-file patterns (should be ignored):
[Complex path](./docs/api/methods.md#get-user)
[Query params](file.md?version=1.0&format=json#section)
[Encoded chars](file%20name.md#section%20name)
[Multiple dots](config.local.dev.yaml#database-config)
[Archive file](backup.tar.gz#file-list)
[Hidden file](.eslintrc.js#rules)
[Network path](//server.com/file.md#section)
[Absolute path](/usr/local/docs/manual.md#install)
Ambiguous patterns (might be fragment-only):
[No extension](somefile#section)
[Dot only](file.#section)
[Hidden no ext](.hidden#section)
Valid internal:
[Works](#main-heading)
[Fails](#missing)
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Expected 2 warnings (1 ambiguous + 1 missing)");
assert!(result.iter().any(|w| w.message.contains("missing")));
}
#[test]
fn test_performance_with_complex_patterns() {
let mut content = String::from("# Performance Test\n\n");
let complex_headings = [
"Complex: (Pattern) -> Result & More!!!",
"API Reference: Methods & Properties",
"Step 1: Setup (Required) & Configuration",
"FAQ??? What's New & Updated",
"Testing & Coverage & Documentation",
"Unicode: Café & 中文 & Русский",
"Punctuation: @#$%^&*() Symbols",
"Arrows: -> <- <-> <=> --> <--",
"Multiple!!! Exclamations??? & Questions",
"Mixed: A -> B: C & D (E) F",
];
for (i, heading) in complex_headings.iter().enumerate() {
content.push_str(&format!("## {heading}\n\n"));
content.push_str("Some content here.\n\n");
if i % 2 == 0 {
let simple_fragment = heading
.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric() || *c == ' ')
.collect::<String>()
.replace(' ', "-");
content.push_str(&format!("[Link](#{simple_fragment})\n\n"));
}
}
let start = std::time::Instant::now();
let rule = MD051LinkFragments::new();
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let _result = rule.check(&ctx).unwrap();
let duration = start.elapsed();
assert!(
duration.as_millis() < 100,
"Performance regression: took {}ms (threshold: 100ms)",
duration.as_millis()
);
}
#[test]
fn test_regression_prevention_issue_39() {
let rule = MD051LinkFragments::new();
let issue_39_cases = vec![
("Testing & Coverage", "testing--coverage"), (
"API Reference: Methods & Properties",
"api-reference-methods--properties", ),
];
for (heading, expected_fragment) in issue_39_cases {
let content = format!("# {heading}\n\n[Link](#{expected_fragment})");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
if result.is_empty() {
println!("✓ Issue 39 case PASSED: '{heading}' -> '{expected_fragment}'");
} else {
println!("⚠ Issue 39 case NEEDS FIX: '{heading}' -> '{expected_fragment}'");
println!(
" Current behavior produces warnings: {:?}",
result.iter().map(|w| &w.message).collect::<Vec<_>>()
);
}
}
assert_fragments(&[
("Testing & Coverage", "testing--coverage"), (
"API Reference: Methods & Properties",
"api-reference-methods--properties", ),
]);
}
#[test]
fn test_boundary_conditions() {
assert_fragments(&[
("", ""), (" ", ""), ("!!!", ""), ("123", "123"), ("_", "_"), ("-", ""), ("a", "a"), ("A", "a"), ]);
}
#[test]
fn test_markdown_formatting_in_headings() {
assert_fragments(&[
("**Bold** Text", "bold-text"),
("*Italic* Text", "italic-text"),
("`Code` Text", "code-text"),
("~~Strikethrough~~ Text", "strikethrough-text"),
("***Bold Italic*** Text", "bold-italic-text"),
("**_Mixed_** Formatting", "mixed-formatting"),
("[Link](url) Text", "link-text"),
(" Text", "image-text"),
]);
}
#[test]
fn test_zero_width_and_control_characters() {
assert_fragments(&[
("Zero\u{200B}Width", "zerowidth"), ("Soft\u{00AD}Hyphen", "softhyphen"), ("Word\u{2060}Joiner", "wordjoiner"), ]);
}
#[test]
fn test_duplicate_heading_numbering() {
let rule = MD051LinkFragments::new();
let content = r#"# Duplicate
## Section
## Section
## Section
[Link 1](#duplicate)
[Link 2](#section)
[Link 3](#section-1)
[Link 4](#section-2)
[Invalid](#section-3)
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only warn about section-3");
assert!(result[0].message.contains("section-3"));
}
#[test]
fn test_custom_header_id_edge_cases() {
let rule = MD051LinkFragments::new();
let content = r#"# Normal Heading {#custom-id}
## Another {#with-hyphens-in-id}
### Third {:#colon-style}
#### Fourth {: #spaced-colon }
[Link 1](#custom-id)
[Link 2](#with-hyphens-in-id)
[Link 3](#colon-style)
[Link 4](#spaced-colon)
[Link 5](#normal-heading)
[Invalid](#missing-id)
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only warn about missing-id");
assert!(result[0].message.contains("missing-id"));
}