Skip to content

Commit e439919

Browse files
committed
Skip HTML/XML tags and non-path targets in link validation
Comrak parses inline HTML tags like <ns:tag> as autolinks. Filter these out by skipping targets containing colons (that aren't known URL schemes) or quotes/commas (JSON attribute garbage).
1 parent 028dd2a commit e439919

3 files changed

Lines changed: 44 additions & 4 deletions

File tree

src/links.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,38 @@ pub struct Link {
2525
pub len: usize,
2626
}
2727

28+
const KNOWN_SCHEMES: &[&str] = &["http://", "https://", "mailto:", "tel:", "ftp://"];
29+
2830
impl Link {
2931
/// Whether this is an external URL (http, https, mailto, etc.)
3032
pub fn is_external(&self) -> bool {
3133
let t = &self.raw_target;
32-
t.starts_with("http://")
33-
|| t.starts_with("https://")
34-
|| t.starts_with("mailto:")
35-
|| t.starts_with("tel:")
34+
KNOWN_SCHEMES.iter().any(|s| t.starts_with(s))
35+
}
36+
37+
/// Whether this link should be skipped entirely during validation.
38+
/// Filters out targets that are clearly not file paths or URLs,
39+
/// such as HTML/XML tag names that comrak misparses as links.
40+
pub fn should_skip(&self) -> bool {
41+
let t = &self.raw_target;
42+
43+
// External URLs are handled separately
44+
if self.is_external() {
45+
return false;
46+
}
47+
48+
// Contains a colon but isn't a known scheme: likely XML namespace,
49+
// custom protocol, or other non-file-path content
50+
if t.contains(':') {
51+
return true;
52+
}
53+
54+
// Targets with quotes/commas are JSON or attribute garbage
55+
if t.contains('"') || t.contains(',') {
56+
return true;
57+
}
58+
59+
false
3660
}
3761

3862
/// Decoded file target path (percent-decoded).

src/rules/broken_links.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ impl WorkspaceRule for BrokenLinks {
1919

2020
for file in &workspace.files {
2121
for link in &file.links {
22+
if link.should_skip() {
23+
continue;
24+
}
2225
if link.is_external() && !config.links.check_external {
2326
continue;
2427
}

tests/integration.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,19 @@ fn test_external_links_skipped_by_default() {
238238
.success();
239239
}
240240

241+
#[test]
242+
fn test_html_xml_tags_not_treated_as_links() {
243+
let vault = create_vault(&[
244+
("doc.md", "# Doc\n\nSome text with <custom:tag>content</custom:tag> inline.\n\n<my-ns:element attr=\"val\">block</my-ns:element>\n"),
245+
]);
246+
247+
cmd()
248+
.arg(vault.path())
249+
.assert()
250+
.success()
251+
.stderr(predicates::str::contains("broken-links").not());
252+
}
253+
241254
#[test]
242255
fn test_links_in_code_blocks_ignored() {
243256
let vault = create_vault(&[

0 commit comments

Comments
 (0)