use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD034NoBareUrls;
use std::fs::write;
#[test]
fn test_valid_urls() {
let rule = MD034NoBareUrls;
let content = "[Link](https://example.com)\n<https://example.com>";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_bare_urls() {
let rule = MD034NoBareUrls;
let content = "This is a bare URL: https://example.com/foobar";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URLs should be flagged");
assert_eq!(result[0].line, 1);
assert_eq!(result[0].rule_name.as_deref(), Some("MD034"));
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "This is a bare URL: <https://example.com/foobar>");
}
#[test]
fn test_multiple_urls() {
let rule = MD034NoBareUrls;
let content = "Visit https://example.com and http://another.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Bare URLs should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Visit <https://example.com> and <http://another.com>");
}
#[test]
fn test_urls_in_code_block() {
let rule = MD034NoBareUrls;
let content = "```
https://example.com
```
https://outside.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URL outside code block should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "```\nhttps://example.com\n```\n<https://outside.com>");
}
#[test]
fn test_urls_in_inline_code() {
let rule = MD034NoBareUrls;
let content = "`https://example.com`\nhttps://outside.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URL outside inline code should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "`https://example.com`\n<https://outside.com>");
}
#[test]
fn test_urls_in_markdown_links() {
let rule = MD034NoBareUrls;
let content = "[Example](https://example.com)\nhttps://bare.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URL should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "[Example](https://example.com)\n<https://bare.com>");
}
#[test]
fn test_ftp_urls() {
let rule = MD034NoBareUrls;
let content = "Download from ftp://example.com/file";
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, "Download from <ftp://example.com/file>");
}
#[test]
fn test_complex_urls() {
let rule = MD034NoBareUrls;
let content = "Visit https://example.com/path?param=value#fragment";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URL should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Visit <https://example.com/path?param=value#fragment>");
}
#[test]
fn test_multiple_protocols() {
let rule = MD034NoBareUrls;
let content = "http://example.com\nhttps://secure.com\nftp://files.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let debug_str = format!("test_multiple_protocols\nMD034 test content: {content}\n");
let _ = write("/tmp/md034_ast_debug.txt", debug_str);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "All bare URLs should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "<http://example.com>\n<https://secure.com>\n<ftp://files.com>");
}
#[test]
fn test_mixed_content() {
let rule = MD034NoBareUrls;
let content = "# Heading\nVisit https://example.com\n> Quote with https://another.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let debug_str = format!("test_mixed_content\nMD034 test content: {content}\n");
let _ = write("/tmp/md034_ast_debug.txt", debug_str);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Bare URLs should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"# Heading\nVisit <https://example.com>\n> Quote with <https://another.com>"
);
}
#[test]
fn test_not_urls() {
let rule = MD034NoBareUrls;
let content = "Text with example.com and just://something";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_badge_links_not_flagged() {
let rule = MD034NoBareUrls;
let content =
"[](https://www.npmjs.com/package/react)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Badge links should not be flagged as bare URLs");
}
#[test]
fn test_multiple_badges_and_links_on_one_line() {
let rule = MD034NoBareUrls;
let content = "# [React](https://react.dev/) \
· [](https://github.com/facebook/react/blob/main/LICENSE) \
[](https://www.npmjs.com/package/react) \
[](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml) \
[](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml) \
[](https://legacy.reactjs.org/docs/how-to-contribute.html#your-first-pull-request)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Multiple badges and links on one line should not be flagged as bare URLs"
);
}
#[test]
fn debug_ast_multiple_urls() {
let content = "Visit https://example.com and http://another.com";
let _ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let debug_str = format!("MD034 test content: {content}\n");
match write("/tmp/md034_ast_debug.txt", debug_str) {
Ok(_) => (),
Err(e) => panic!("Failed to write AST debug file: {e}"),
}
}
#[test]
fn test_md034_edge_cases() {
let rule = MD034NoBareUrls;
let cases = [
("`https://example.com`", 0),
("```\nhttps://example.com\n```", 0),
("This is not a URL: htp://example.com", 0),
("custom://example.com", 0),
("See https://example.com.", 1),
("https://example .com", 1),
("> https://example.com", 1),
("- https://example.com", 1),
("https://exämple.com", 1),
("http://example.com:8080", 1),
("https://example.com/path?query=1#frag", 1),
("www.example.com", 1),
("| https://example.com |", 1),
("# https://example.com", 1),
("[ref]: https://example.com", 0),
("", 0),
("[link](https://example.com)", 0),
("foo://example.com", 0),
("htps://example.com", 0),
("`http://example.com`", 0),
("http://example.com", 1),
];
for (content, expected) in cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected, "Failed for content: {content}");
let fixed = rule.fix(&ctx).unwrap();
if *expected > 0 {
assert_ne!(fixed, *content, "Fix should change content with warnings: {content}");
let ctx_fixed = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result_fixed = rule.check(&ctx_fixed).unwrap();
assert_eq!(result_fixed.len(), 0, "Fixed content should have no warnings: {fixed}");
} else {
assert_eq!(
fixed, *content,
"Fix should not change content without warnings: {content}"
);
}
}
}
#[test]
fn test_bare_email_addresses() {
let rule = MD034NoBareUrls;
let content = "Contact us at support@example.com or admin@test.org";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Bare email addresses should be flagged as bare URLs");
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 1);
assert!(
result[0]
.message
.contains("Email address without angle brackets or link formatting")
);
assert!(
result[1]
.message
.contains("Email address without angle brackets or link formatting")
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Contact us at <support@example.com> or <admin@test.org>");
}
#[test]
fn test_email_addresses_various_formats() {
let rule = MD034NoBareUrls;
let test_cases = [
("Email: user@domain.com", 1, "Email: <user@domain.com>"),
(
"Complex email: user.name+tag@sub.domain.co.uk",
1,
"Complex email: <user.name+tag@sub.domain.co.uk>",
),
(
"Email with numbers: user123@example123.com",
1,
"Email with numbers: <user123@example123.com>",
),
(
"Email with hyphens: user-name@sub-domain.example-site.org",
1,
"Email with hyphens: <user-name@sub-domain.example-site.org>",
),
("Short TLD: user@example.co", 1, "Short TLD: <user@example.co>"),
("Long TLD: user@example.museum", 1, "Long TLD: <user@example.museum>"),
];
for (content, expected_count, expected_fix) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for content: {content}");
if *expected_count > 0 {
assert!(
result.iter().any(|w| w
.message
.contains("Email address without angle brackets or link formatting")),
"Email detection failed for: {content}"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, *expected_fix, "Fix failed for: {content}");
}
}
}
#[test]
fn test_email_exclusions() {
let rule = MD034NoBareUrls;
let test_cases = [
("[Contact](mailto:user@example.com)", 0),
("<user@example.com>", 0),
("`user@example.com`", 0),
("```\nuser@example.com\n```", 0),
("<a href=\"mailto:user@example.com\">Contact</a>", 0),
];
for (content, expected_count) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for content: {content}");
}
}
#[test]
fn test_localhost_urls() {
let rule = MD034NoBareUrls;
let content = "Visit http://localhost:3000 and https://localhost:8080/api";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Localhost URLs should be flagged as bare URLs");
assert!(
result
.iter()
.any(|w| w.message.contains("URL without angle brackets or link formatting")),
"Localhost URL detection failed"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Visit <http://localhost:3000> and <https://localhost:8080/api>");
}
#[test]
fn test_localhost_variations() {
let rule = MD034NoBareUrls;
let test_cases = [
("http://localhost", 1, "<http://localhost>"),
("https://localhost", 1, "<https://localhost>"),
("http://localhost:8080", 1, "<http://localhost:8080>"),
("https://localhost:3000", 1, "<https://localhost:3000>"),
("http://localhost/path", 1, "<http://localhost/path>"),
("https://localhost:9090/api/v1", 1, "<https://localhost:9090/api/v1>"),
("ftp://localhost", 1, "<ftp://localhost>"), ];
for (content, expected_count, expected_fix) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for content: {content}");
if *expected_count > 0 {
assert!(
result
.iter()
.any(|w| w.message.contains("URL without angle brackets or link formatting")),
"Localhost/protocol detection failed for: {content}"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, *expected_fix, "Fix failed for: {content}");
}
}
}
#[test]
fn test_ip_address_urls() {
let rule = MD034NoBareUrls;
let content = "Connect to http://127.0.0.1:8080 or https://192.168.1.100";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "IP address URLs should be flagged as bare URLs");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Connect to <http://127.0.0.1:8080> or <https://192.168.1.100>");
}
#[test]
fn test_combined_emails_and_localhost() {
let rule = MD034NoBareUrls;
let content = "Contact admin@localhost.com or visit http://localhost:9090\nAlso try user@example.org and https://192.168.1.1:3000";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 4, "Should detect both emails and localhost URLs");
let fixed = rule.fix(&ctx).unwrap();
let expected = "Contact <admin@localhost.com> or visit <http://localhost:9090>\nAlso try <user@example.org> and <https://192.168.1.1:3000>";
assert_eq!(fixed, expected);
}
#[test]
fn test_multiline_markdown_links_not_flagged() {
let rule = MD034NoBareUrls;
let content = "Details about each issue type and the issue lifecycle are discussed in the [MLflow Issue\nPolicy](https://github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md).\n\nAfter you have agreed upon an implementation strategy for your feature\nor patch with an MLflow committer, the next step is to introduce your\nchanges (see [developing\nchanges](https://github.com/mlflow/mlflow/blob/master/CONTRIBUTING.md#developing-and-testing-mlflow))\nas a pull request against the MLflow Repository.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Multi-line markdown links should not be flagged as bare URLs. Found {} warnings: {:#?}",
result.len(),
result
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, content,
"Fix should not change content with properly formatted multi-line markdown links"
);
}
#[test]
fn test_issue_48_url_in_link_text() {
let rule = MD034NoBareUrls;
let content = "Also don't forget that the next time you need to figure out which `datetime` format you need, **[use the strptime tool at https://pym.dev/strptime](https://www.pythonmorsels.com/strptime/)**!";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty() || result.iter().all(|w| !w.message.contains("URL")),
"URL within link text should not be flagged as bare URL. Found {} warnings: {:#?}",
result.len(),
result
);
}
#[test]
fn test_issue_47_urls_emails_in_html_attributes() {
let rule = MD034NoBareUrls;
let content = r#"# Example
This is **some text**.
<input type="email" name="fields[email]" id="drip-email" placeholder="email@domain.com">
<input name="fields[url]" value="https://www.example.com">"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Emails and URLs within HTML attributes should not be flagged. Found {} warnings: {:#?}",
result.len(),
result
);
}
#[test]
fn test_mixed_multiline_links_and_bare_urls() {
let rule = MD034NoBareUrls;
let content = "This has a [multi-line\nlink](https://github.com/example/repo) which should not be flagged.\n\nBut this bare URL should be flagged: https://bare-url.com\n\nAnd this [another multi-line\nlink with long URL](https://github.com/very/long/repository/path/that/spans/multiple/lines) should also not be flagged.";
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 bare URL, not the multi-line markdown links. Found {} warnings: {:#?}",
result.len(),
result
);
assert!(
result[0]
.message
.contains("URL without angle brackets or link formatting"),
"Should flag bare URL with correct message"
);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("<https://bare-url.com>"),
"Should wrap the bare URL in angle brackets"
);
assert!(
fixed.contains("[multi-line\nlink](https://github.com/example/repo)"),
"Should not modify the multi-line markdown link"
);
assert!(
fixed.contains("[another multi-line\nlink with long URL](https://github.com/very/long/repository/path/that/spans/multiple/lines)"),
"Should not modify the second multi-line markdown link"
);
}
#[test]
fn test_issue_104_url_in_empty_link() {
let rule = MD034NoBareUrls;
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(),
0,
"URL in [url]() link text should not be flagged as bare URL. This is MD042 territory. Found {} warnings: {:#?}",
result.len(),
result
);
}
#[test]
fn test_issue_104_url_in_empty_bracket_link() {
let rule = MD034NoBareUrls;
let content = "Visit [https://www.google.com][] for more info.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"URL in [url][] should not be flagged as bare URL. Found {} warnings: {:#?}",
result.len(),
result
);
}
#[test]
fn test_issue_104_full_paragraph_not_corrupted() {
let rule = MD034NoBareUrls;
let content_after_md042 = "I've never been one to implement hacky solutions because life is just easier\nwhen everything gets done \"by the book.\" So, if you're reading this and want to\nsee the code that creates this extension and prevents me from pouring needless\nhours into meticulously maintaining the files by hand, I welcome you to check it\nout in its new repository at [https://github.com/pfeif/hx-complete-generator](https://github.com/pfeif/hx-complete-generator).";
let ctx = LintContext::new(content_after_md042, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"After MD042 fixes empty link, MD034 should not flag the URL. Found {} warnings: {:#?}",
result.len(),
result
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, content_after_md042,
"MD034 should not modify content that has properly formatted links"
);
}
#[test]
fn test_urls_in_yaml_front_matter() {
let rule = MD034NoBareUrls;
let content = "---\nurl: http://example.com\ntitle: Test\n---\n\n# Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "URLs in YAML front matter should not be flagged");
}
#[test]
fn test_urls_in_toml_front_matter() {
let rule = MD034NoBareUrls;
let content = "+++\nurl = \"http://example.com\"\ntitle = \"Test\"\n+++\n\n# Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "URLs in TOML front matter should not be flagged");
}
#[test]
fn test_urls_in_json_front_matter() {
let rule = MD034NoBareUrls;
let content = "{\n\"url\": \"http://example.com\",\n\"title\": \"Test\"\n}\n\n# Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "URLs in JSON front matter should not be flagged");
}
#[test]
fn test_bare_url_after_front_matter() {
let rule = MD034NoBareUrls;
let content = "---\nurl: http://example.com\n---\n\nVisit http://bare-url.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URL after front matter should be flagged");
assert!(result[0].message.contains("http://bare-url.com"));
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("<http://bare-url.com>"),
"Bare URL should be wrapped in angle brackets"
);
assert!(
fixed.contains("url: http://example.com"),
"URL in front matter should remain unchanged"
);
}
#[test]
fn test_email_in_front_matter() {
let rule = MD034NoBareUrls;
let content = "---\nauthor_email: user@example.com\ncontact: admin@test.org\n---\n\n# Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Emails in front matter should not be flagged");
}
#[test]
fn test_multiple_urls_in_front_matter() {
let rule = MD034NoBareUrls;
let content = "---\nurl: http://example.com\nrepository: https://github.com/user/repo\nwebsite: ftp://files.example.org\n---\n\n# Content";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Multiple URLs in front matter should not be flagged");
}
#[test]
fn test_issue_116_exact_reproduction() {
let rule = MD034NoBareUrls;
let content = "---\nurl: http://example.com\n---\n\n# Repro";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Issue #116: URL in front matter should not be flagged"
);
}
#[test]
fn test_issue_151_urls_in_html_block_attributes() {
let rule = MD034NoBareUrls;
let content = r#"<figure>
<img
src="https://example.com/test.html"
/>
</figure>"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Issue #151: URL in HTML block attribute should not be flagged"
);
}
#[test]
fn test_issue_151_single_line_html_tag_with_url() {
let rule = MD034NoBareUrls;
let content = r#"<img src="https://example.com/image.png" alt="test" />"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Single-line HTML tag with URL in attribute should not be flagged"
);
}
#[test]
fn test_issue_151_multiple_urls_in_html_block() {
let rule = MD034NoBareUrls;
let content = r#"<div>
<img src="https://example.com/image1.png" />
<img src="https://example.com/image2.png" />
<a href="https://example.com/page.html">Link</a>
</div>"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Multiple URLs in HTML block attributes should not be flagged"
);
}
#[test]
fn test_issue_151_various_html_tag_types() {
let rule = MD034NoBareUrls;
let content = r#"<section>
<div data-url="https://example.com/api">
<iframe src="https://example.com/embed.html"></iframe>
</div>
</section>"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URLs in various HTML tag types should not be flagged"
);
}
#[test]
fn test_issue_151_nested_html_blocks_with_urls() {
let rule = MD034NoBareUrls;
let content = r#"<article>
<header>
<img src="https://example.com/logo.png" />
</header>
<div class="content">
<a href="https://example.com/link1.html">Link 1</a>
<figure>
<img src="https://example.com/image.png" />
</figure>
</div>
</article>"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Nested HTML blocks with URLs should not be flagged");
}
#[test]
fn test_issue_151_html_block_with_mixed_content() {
let rule = MD034NoBareUrls;
let content = r#"<div>
Some text content
<img src="https://example.com/image.png" />
More text
</div>
Outside HTML: https://example.com/should-flag.html"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Only bare URL outside HTML block should be flagged");
assert_eq!(result[0].line, 7);
}
#[test]
fn test_issue_178_unicode_before_inline_code_url() {
let rule = MD034NoBareUrls;
let content = "- Some code\u{2019}s example `https://example.com` containing a URL";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL in inline code after curly apostrophe should NOT be flagged, got {result:?}"
);
let content2 = "- [Some normal URL](https://example.com)\n- Some code\u{2019}s example `https://example.com` containing an URL\n- Some code\u{2019}s repro example `https://example.com`";
let ctx2 = LintContext::new(content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert!(
result2.is_empty(),
"URLs in inline code should NOT be flagged, got {result2:?}"
);
}
#[test]
fn test_unicode_multibyte_chars_before_inline_code_url() {
let rule = MD034NoBareUrls;
let test_cases = [
("Left curly quote", "Text \u{2018}quoted\u{2019} `https://example.com`"),
("Em dash", "Text\u{2014}dash `https://example.com`"),
("Euro sign", "Price 100\u{20AC} `https://example.com`"),
("Japanese", "\u{3042}\u{3044}\u{3046} `https://example.com`"),
("Emoji", "\u{1F600} happy `https://example.com`"),
("Chinese", "\u{4E2D}\u{6587} `https://example.com`"),
];
for (name, content) in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"{name}: URL in inline code after multi-byte chars should NOT be flagged, got {result:?}"
);
}
}
#[test]
fn test_reference_definitions_with_titles_not_flagged() {
let rule = MD034NoBareUrls;
let test_cases = [
"[example]: https://example.com",
"[example]: https://example.com \"Title here\"",
"[example]: https://example.com 'Title here'",
"[example]: https://example.com (Title here)",
"[`maturin`]: https://github.com/PyO3/maturin \"Build and publish crates\"",
"[example]: <https://example.com> \"Title\"",
"[feature flags]: https://doc.rust-lang.org/cargo/reference/features.html \"Features - The Cargo Book\"",
"[ref1]: https://example.com\n[ref2]: https://test.com \"Test title\"",
" [example]: https://example.com \"Indented\"",
];
for content in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Reference definition should NOT be flagged as bare URL:\n{content}\nGot: {result:?}"
);
}
}
#[test]
fn test_bare_urls_still_flagged_with_reference_definitions() {
let rule = MD034NoBareUrls;
let content = r#"# Test Document
This has a bare URL: https://bare.example.com
[example]: https://example.com "This is fine"
Another bare URL: https://another.bare.url
"#;
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
2,
"Expected 2 bare URLs flagged, got {}:\n{:?}",
result.len(),
result
);
assert!(result[0].message.contains("https://bare.example.com"));
assert!(result[1].message.contains("https://another.bare.url"));
}
#[test]
fn test_www_urls_without_protocol() {
let rule = MD034NoBareUrls;
let content = "# Test\n\nVisit www.example.com for info.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"www URL should be flagged as bare URL. Got: {result:?}"
);
assert!(
result[0].message.contains("www.example.com"),
"Message should contain the www URL"
);
}
#[test]
fn test_url_inside_markdown_link_not_flagged() {
let rule = MD034NoBareUrls;
let content = "[Link 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(),
"URL inside markdown link should NOT be flagged: {result:?}"
);
}
#[test]
fn test_url_inside_markdown_link_with_trailing_text() {
let rule = MD034NoBareUrls;
let content = "See [here](https://example.com) for details.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL inside markdown link should NOT be flagged even with trailing text: {result:?}"
);
}
#[test]
fn test_multiple_markdown_links_same_line() {
let rule = MD034NoBareUrls;
let content = "[Link1](https://example.com) and [Link2](https://test.com) are both valid.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Multiple URLs inside markdown links should NOT be flagged: {result:?}"
);
}
#[test]
fn test_url_inside_image_not_flagged() {
let rule = MD034NoBareUrls;
let content = "";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL inside image syntax should NOT be flagged: {result:?}"
);
}
#[test]
fn test_url_with_nested_parentheses_in_link() {
let rule = MD034NoBareUrls;
let content = "[Rust](https://en.wikipedia.org/wiki/Rust_(programming_language))";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL with nested parens inside markdown link should NOT be flagged: {result:?}"
);
}
#[test]
fn test_bare_url_outside_link_still_flagged() {
let rule = MD034NoBareUrls;
let content = "Visit https://example.com for more info.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare URL outside markdown link SHOULD be flagged");
assert!(result[0].message.contains("https://example.com"));
}
#[test]
fn test_markdown_link_and_bare_url_same_line() {
let rule = MD034NoBareUrls;
let content = "[Good link](https://example.com) but also https://bare.url here";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag only the bare URL, got: {result:?}");
assert!(
result[0].message.contains("https://bare.url"),
"Should flag the bare URL, not the markdown link URL"
);
}
#[test]
fn test_url_starting_inside_link_boundary() {
let rule = MD034NoBareUrls;
let content = "[Link](https://example.com/path)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL starting inside link should NOT be flagged: {result:?}"
);
}
#[test]
fn test_url_in_angle_brackets_not_flagged() {
let rule = MD034NoBareUrls;
let content = "Contact us at <https://example.com>";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL in angle brackets (autolink) should NOT be flagged: {result:?}"
);
}
#[test]
fn test_url_in_reference_definition_boundary() {
let rule = MD034NoBareUrls;
let content = "[ref]: https://example.com\n\nSee [ref] for details.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL in reference definition should NOT be flagged: {result:?}"
);
}
#[test]
fn test_bare_xmpp_uri() {
let rule = MD034NoBareUrls;
let content = "Contact me at xmpp:user@example.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare XMPP URI should be flagged");
assert!(
result[0].message.contains("xmpp:user@example.com"),
"Message should contain the XMPP URI"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Contact me at <xmpp:user@example.com>");
}
#[test]
fn test_xmpp_uri_with_resource() {
let rule = MD034NoBareUrls;
let content = "My chat address: xmpp:foo@bar.baz/txt";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Bare XMPP URI with resource should be flagged");
assert!(
result[0].message.contains("xmpp:foo@bar.baz/txt"),
"Message should contain the XMPP URI with resource"
);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "My chat address: <xmpp:foo@bar.baz/txt>");
}
#[test]
fn test_xmpp_uri_in_angle_brackets() {
let rule = MD034NoBareUrls;
let content = "Contact me at <xmpp:user@example.com>";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"XMPP URI in angle brackets should NOT be flagged: {result:?}"
);
}
#[test]
fn test_xmpp_uri_in_markdown_link() {
let rule = MD034NoBareUrls;
let content = "[Chat with me](xmpp:user@example.com)";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"XMPP URI in markdown link should NOT be flagged: {result:?}"
);
}
#[test]
fn test_multiple_xmpp_uris() {
let rule = MD034NoBareUrls;
let content = "Contact xmpp:alice@example.com or xmpp:bob@example.org/work";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Both bare XMPP URIs should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Contact <xmpp:alice@example.com> or <xmpp:bob@example.org/work>");
}
#[test]
fn test_xmpp_uri_mixed_with_urls_and_emails() {
let rule = MD034NoBareUrls;
let content = "Website: https://example.com\nEmail: user@example.com\nXMPP: xmpp:chat@example.com";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "URL, email, and XMPP URI should all be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"Website: <https://example.com>\nEmail: <user@example.com>\nXMPP: <xmpp:chat@example.com>"
);
}
#[test]
fn test_xmpp_uri_in_code_block() {
let rule = MD034NoBareUrls;
let content = "```\nxmpp:user@example.com\n```";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"XMPP URI in code block should NOT be flagged: {result:?}"
);
}
#[test]
fn test_xmpp_uri_in_inline_code() {
let rule = MD034NoBareUrls;
let content = "Use `xmpp:user@example.com` for chat.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"XMPP URI in inline code should NOT be flagged: {result:?}"
);
}
#[test]
fn test_xmpp_uri_variations() {
let rule = MD034NoBareUrls;
let test_cases = [
("xmpp:user@domain.com", 1, "<xmpp:user@domain.com>"),
("xmpp:chat@chat.example.org", 1, "<xmpp:chat@chat.example.org>"),
("xmpp:user@domain.net/mobile", 1, "<xmpp:user@domain.net/mobile>"),
(
"xmpp:user@domain.com/resource/path",
1,
"<xmpp:user@domain.com/resource/path>",
),
("xmpp:user123@domain456.com", 1, "<xmpp:user123@domain456.com>"),
("xmpp:first.last@domain.com", 1, "<xmpp:first.last@domain.com>"),
];
for (content, expected_count, expected_fix) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for XMPP URI: {content}");
if *expected_count > 0 {
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, *expected_fix, "Fix failed for XMPP URI: {content}");
}
}
}
#[test]
fn test_www_urls_with_query_string() {
let rule = MD034NoBareUrls;
let test_cases = [
(
"www.example.com?param=value",
1,
"<https://www.example.com?param=value>",
),
("www.example.com?a=1&b=2", 1, "<https://www.example.com?a=1&b=2>"),
(
"www.example.com/path?query=test",
1,
"<https://www.example.com/path?query=test>",
),
];
for (content, expected_count, expected_fix) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for www URL: {content}");
if *expected_count > 0 {
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, *expected_fix, "Fix failed for www URL: {content}");
}
}
}
#[test]
fn test_www_urls_with_fragment() {
let rule = MD034NoBareUrls;
let test_cases = [
("www.example.com#section", 1, "<https://www.example.com#section>"),
(
"www.example.com/page#anchor",
1,
"<https://www.example.com/page#anchor>",
),
("www.example.com?q=1#frag", 1, "<https://www.example.com?q=1#frag>"),
];
for (content, expected_count, expected_fix) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for www URL: {content}");
if *expected_count > 0 {
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, *expected_fix, "Fix failed for www URL: {content}");
}
}
}
#[test]
fn test_www_urls_with_port() {
let rule = MD034NoBareUrls;
let test_cases = [
("www.example.com:8080", 1, "<https://www.example.com:8080>"),
("www.example.com:3000/path", 1, "<https://www.example.com:3000/path>"),
("www.example.com:443?q=1", 1, "<https://www.example.com:443?q=1>"),
];
for (content, expected_count, expected_fix) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for www URL: {content}");
if *expected_count > 0 {
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, *expected_fix, "Fix failed for www URL: {content}");
}
}
}
#[test]
fn test_www_urls_in_context() {
let rule = MD034NoBareUrls;
let test_cases = [
("Visit www.example.com for more info.", 1),
("Check out www.example.com/docs#getting-started today!", 1),
("Server at www.internal.example.com:8080/api is ready.", 1),
];
for (content, expected_count) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), *expected_count, "Failed for: {content}");
}
}
#[test]
fn test_www_urls_not_flagged_when_formatted() {
let rule = MD034NoBareUrls;
let test_cases = [
"<https://www.example.com>",
"[link](https://www.example.com)",
"[www.example.com](https://www.example.com)",
"`www.example.com`",
];
for content in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Formatted www URL should NOT be flagged: {content}");
}
}
#[test]
fn test_www_and_protocol_urls_mixed() {
let rule = MD034NoBareUrls;
let content = "Visit www.example.com and https://other.com for info.";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Both www and https URLs should be flagged");
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed,
"Visit <https://www.example.com> and <https://other.com> for info."
);
}
#[test]
fn test_email_detection_with_multibyte_utf8() {
let rule = MD034NoBareUrls;
let content = "কুবারনেটিস কমিউনিটির মধ্যে ঘটে যাওয়া ঘটনাগুলির জন্য, conduct@kubernetes.io মাধ্যমে";
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Email should be detected in Bengali text");
assert!(
result[0].message.contains("Email address without angle brackets"),
"Should flag bare email"
);
}
#[test]
fn test_email_detection_various_scripts() {
let rule = MD034NoBareUrls;
let test_cases = [
("日本語テキスト user@example.com 日本語", 1),
("中文文本 user@example.com 更多中文", 1),
("نص عربي user@example.com نص آخر", 1),
("🎉 email@test.com 🎉", 1),
("日本語 中文 العربية user@example.com more", 1),
];
for (content, expected_count) in test_cases.iter() {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
*expected_count,
"Failed for multi-byte content: {content}"
);
}
}