use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD042NoEmptyLinks;
#[test]
fn test_valid_links() {
let rule = MD042NoEmptyLinks::new();
let content = "[Link text](https://example.com)\n[Another link](./local/path)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_empty_link_text() {
let rule = MD042NoEmptyLinks::new();
let content = "[](https://example.com)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Empty text with valid URL should not be flagged");
}
#[test]
fn test_empty_link_url() {
let rule = MD042NoEmptyLinks::new();
let content = "[Link text]()";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].fix.is_none(), "Non-URL text should not have auto-fix");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Should not modify unfixable links");
}
#[test]
fn test_empty_link_both() {
let rule = MD042NoEmptyLinks::new();
let content = "[]()";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].fix.is_none(), "Both empty should not have auto-fix");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Should not modify unfixable links");
}
#[test]
fn test_multiple_empty_links() {
let rule = MD042NoEmptyLinks::new();
let content = "[Link]() and []() and [](url)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Should only flag links with empty URLs");
}
#[test]
fn test_whitespace_only_links() {
let rule = MD042NoEmptyLinks::new();
let content = "[ ]( )";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].fix.is_none(), "Whitespace-only should not have auto-fix");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Should not modify unfixable links");
}
#[test]
fn test_mixed_valid_and_empty_links() {
let rule = MD042NoEmptyLinks::new();
let content = "[Valid](https://example.com) and []() and [Another](./path)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].fix.is_none(), "Both empty should not have auto-fix");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content, "Should not modify unfixable links");
}
#[test]
fn test_md042_ignores_links_in_fenced_code_blocks() {
let content = r#"# Test Document
Regular empty link: [empty text]()
Fenced code block with empty links:
```markdown
[empty link]()
[another empty]()
```
Another regular empty link: [empty text]()"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].line, 3); assert_eq!(warnings[1].line, 12); }
#[test]
fn test_md042_ignores_links_in_indented_code_blocks() {
let content = r#"# Test Document
Regular empty link: [empty text]()
Indented code block with empty links:
[empty link]()
[another empty]()
Another regular empty link: [empty text]()"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].line, 3); assert_eq!(warnings[1].line, 10); }
#[test]
fn test_md042_ignores_links_in_code_spans() {
let content = r#"# Test Document
Regular empty link: [empty text]()
Inline code with empty link: `[empty link]()`
Another regular empty link: [empty text]()
More inline code: `Check this [empty]() link`"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].line, 3); assert_eq!(warnings[1].line, 7); }
#[test]
fn test_md042_complex_nested_scenarios() {
let content = r#"# Test Document
Regular empty link: [empty text]()
## Code blocks in lists
1. First item with code:
```markdown
[empty in fenced]()
```
2. Second item with indented code:
[empty in indented]()
3. Third item with inline code: `[empty in span]()`
## Blockquotes with code
> Regular empty link in quote: [empty]()
>
> Code in quote:
> ```
> [empty in quoted code]()
> ```
Final empty link: []()"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 3);
assert_eq!(warnings[0].line, 3); assert_eq!(warnings[1].line, 21); assert_eq!(warnings[2].line, 28); }
#[test]
fn test_md042_mixed_code_block_types() {
let content = r#"# Test Document
Empty link: [empty text]()
Fenced with language:
```javascript
// [empty link]() in comment
```
Fenced without language:
```
[empty link]()
```
Indented code:
[empty link]()
Tilde fenced:
~~~
[empty link]()
~~~
Final empty: [empty text]()"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].line, 3); assert_eq!(warnings[1].line, 24); }
#[test]
fn test_md042_reference_links_in_code() {
let content = r#"# Test Document
Regular empty reference: [][empty-ref]
Code block with reference:
```
[][empty-ref]
[text][empty-ref]
```
Inline code: `[][empty-ref]`
[empty-ref]: "#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].line, 3); }
#[test]
fn test_md042_edge_cases_with_code() {
let content = r#"# Test Document
Empty link: []()
Mixed content:
```markdown
Valid: [text](url)
Empty: []()
```
Indented empty: []()
Inline: `[]()` and more text
Final: []()"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let rule = MD042NoEmptyLinks::new();
let warnings = rule.check(&ctx).unwrap();
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].line, 3); assert_eq!(warnings[1].line, 15); }
#[test]
fn test_md042_reference_links() {
let rule = MD042NoEmptyLinks::new();
let content = "[text][ref]\n\n[ref]: https://example.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "[][ref]\n\n[ref]: https://example.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Empty text with valid reference should not be flagged"
);
let content = "[text][missing]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Undefined references are handled by MD052, not MD042"
);
let content = "[text][]\n\n[text]: https://example.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "[][]";
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_md042_mkdocs_backtick_wrapped_auto_references() {
let rule = MD042NoEmptyLinks::new();
let content = "[`module.Class`][]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag [`module.Class`][] as empty in MkDocs mode (issue #97). Got: {result:?}"
);
let content = "[`str`][]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag [`str`][] as empty in MkDocs mode (issue #97 example). Got: {result:?}"
);
let content = "See [`str`][], [`int`][], and [`bool`][] for details.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag single-word backtick-wrapped identifiers in MkDocs mode (issue #97). Got: {result:?}"
);
let content = "[str][]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Undefined references are handled by MD052, not MD042. Got: {result:?}"
);
let content = "[`str`][]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Undefined references are handled by MD052, not MD042. Got: {result:?}"
);
let content = "[`module.func`][`module.func`]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag explicit backtick-wrapped reference IDs in MkDocs mode. Got: {result:?}"
);
}
#[test]
fn test_url_in_text_with_empty_destination() {
let rule = MD042NoEmptyLinks::new();
let content = "[https://github.com/user/repo]()";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag empty URL");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, "[https://github.com/user/repo](https://github.com/user/repo)",
"Should use URL text as destination instead of example.com"
);
}
#[test]
fn test_url_variants_in_text_with_empty_destination() {
let rule = MD042NoEmptyLinks::new();
let test_cases = vec![
("[https://example.com]()", "[https://example.com](https://example.com)"),
("[http://example.com]()", "[http://example.com](http://example.com)"),
("[ftp://example.com]()", "[ftp://example.com](ftp://example.com)"),
("[ftps://example.com]()", "[ftps://example.com](ftps://example.com)"),
];
for (input, expected) in test_cases {
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag empty URL in: {input}");
assert!(result[0].fix.is_some(), "URL text should be fixable: {input}");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected, "Failed for input: {input}");
}
let non_url_cases = vec!["[Click here]()", "[Some text]()", "[Learn more]()"];
for input in non_url_cases {
let ctx = LintContext::new(input, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag empty URL in: {input}");
assert!(result[0].fix.is_none(), "Non-URL text should NOT be fixable: {input}");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, input, "Should not modify non-URL text: {input}");
}
}
#[test]
fn test_issue_104_regression() {
let rule = MD042NoEmptyLinks::new();
let content = "check it out in its new repository at [https://github.com/pfeif/hx-complete-generator]().";
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,
"check it out in its new repository at [https://github.com/pfeif/hx-complete-generator](https://github.com/pfeif/hx-complete-generator).",
"Should use the URL from text as the destination"
);
}
#[test]
fn test_autolinks_not_flagged() {
let rule = MD042NoEmptyLinks::new();
let content = "Visit <https://example.com> and <https://github.com/user/repo>.
Also email me at <user@example.com>.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Autolinks should not be flagged as empty links");
}
#[test]
fn test_brackets_in_html_href_not_flagged() {
let rule = MD042NoEmptyLinks::new();
let content = r#"Check this out:
<a href="https://example.com?p[images][0]=test&title=Example">Share on Example</a>
This is a real markdown link that should be flagged (empty URL):
[empty text]()"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should only flag the real markdown empty link, not brackets in HTML href attributes"
);
assert!(
result[0].line > 3,
"Should flag the markdown link on line 5, not the HTML on line 2"
);
}
#[test]
fn test_obsidian_block_references() {
let rule = MD042NoEmptyLinks::new();
let content = "This paragraph has a block reference. ^my-block\n\nLink to it: [[#^my-block]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag Obsidian block references in current file. Got: {result:?}"
);
let content = "Reference block in another file: [[OtherNote#^block-id]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag Obsidian block references in other files. Got: {result:?}"
);
let content = "Reference with path: [[docs/guide#^important-note]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag block references with file paths. Got: {result:?}"
);
let content = "Regular anchor: [[Note#heading]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Wiki-links with heading anchors [[Note#heading]] are valid and should not be flagged. Got: {result:?}"
);
}
#[test]
fn test_wiki_style_links_not_flagged() {
let rule = MD042NoEmptyLinks::new();
let content = "[[Example]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag wiki-style links [[Example]]. Got: {result:?}"
);
let content = "[[Page Name]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag wiki-style links with spaces [[Page Name]]. Got: {result:?}"
);
let content = "[[Folder/Page]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag wiki-style links with paths [[Folder/Page]]. Got: {result:?}"
);
let content = "[[Page|Display Text]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag wiki-style links with display text [[Page|Display Text]]. Got: {result:?}"
);
let content = "Check out [[Page One]] and [[Page Two]] for details.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag multiple wiki-style links. Got: {result:?}"
);
let content = "[[#^block-id]]";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should not flag wiki-style block references. Got: {result:?}"
);
}