fn is_virtual_path(target: &str) -> bool {
if !target.starts_with('/') {
return false;
}
let target = target.split(['#', '?']).next().unwrap_or(target);
[".md", ".markdown", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]
.iter()
.any(|ext| target.ends_with(ext))
}
pub fn extract_links(content: &str) -> Vec<String> {
let bytes = content.as_bytes();
let mut links = Vec::new();
let mut at = 0usize;
while let Some(bracket_offset) = content[at..].find('[') {
let bracket = at + bracket_offset;
let Some(close_bracket_offset) = content[bracket..].find(']') else {
break;
};
let close_bracket = bracket + close_bracket_offset;
let paren = close_bracket + 1;
if bytes.get(paren) != Some(&b'(') {
at = close_bracket + 1;
continue;
}
let Some(close_paren_offset) = content[paren..].find(')') else {
break;
};
let close_paren = paren + close_paren_offset;
let target = &content[paren + 1..close_paren];
if is_virtual_path(target) {
let stripped = target.split(['#', '?']).next().unwrap_or(target);
links.push(stripped.to_string());
}
at = close_paren + 1;
}
links
}
pub fn broken_links(content: &str, exists: impl Fn(&str) -> bool) -> Vec<String> {
extract_links(content)
.into_iter()
.filter(|target| !exists(target))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_a_link_and_an_embed_pointing_at_a_virtual_path() {
let content = "See [the guide](/docs/quick-start.md) and .";
assert_eq!(
extract_links(content),
vec!["/docs/quick-start.md".to_string(), "/images/logo.png".to_string()]
);
}
#[test]
fn ignores_a_link_that_is_not_an_absolute_known_extension() {
let content = "[external](https://example.com) and [anchor](#section) and [rel](../x.md)";
assert!(extract_links(content).is_empty());
}
#[test]
fn strips_an_anchor_or_query_before_matching_the_extension() {
let content = "[section](/docs/guide.md#install)";
assert_eq!(extract_links(content), vec!["/docs/guide.md".to_string()]);
}
#[test]
fn reports_only_the_links_that_do_not_exist() {
let content = "[a](/docs/a.md) and [b](/docs/b.md)";
let broken = broken_links(content, |target| target == "/docs/a.md");
assert_eq!(broken, vec!["/docs/b.md".to_string()]);
}
}