#![allow(dead_code)]
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
use regex::Regex;
#[derive(Debug, Clone)]
pub struct Link {
pub start: usize,
pub end: usize,
pub destination: String,
pub text: String,
}
pub fn extract_links(content: &str) -> Vec<Link> {
let mut links = Vec::new();
let parser = Parser::new(content);
let mut current_link: Option<(usize, String)> = None;
let mut link_text = String::new();
for (event, range) in parser.into_offset_iter() {
match event {
Event::Start(Tag::Link { dest_url, .. }) => {
current_link = Some((range.start, dest_url.to_string()));
link_text.clear();
}
Event::Text(text) if current_link.is_some() => {
link_text.push_str(&text);
}
Event::End(TagEnd::Link) => {
if let Some((start, dest)) = current_link.take() {
links.push(Link {
start,
end: range.end,
destination: dest,
text: link_text.clone(),
});
}
}
_ => {}
}
}
let wiki_link_re = Regex::new(r"\[\[([^\]]+)\]\]").unwrap();
for cap in wiki_link_re.captures_iter(content) {
if let Some(matched) = cap.get(0) {
let note = cap.get(1).unwrap().as_str().to_string();
links.push(Link {
start: matched.start(),
end: matched.end(),
destination: note.clone(),
text: note,
});
}
}
links.sort_by_key(|l| l.start);
links
}
pub fn find_link_at_position(links: &[Link], pos: usize) -> Option<&Link> {
links
.iter()
.find(|link| pos >= link.start && pos < link.end)
}
pub fn is_external_link(destination: &str) -> bool {
let dest = destination.trim_start();
if let Some(scheme_end) = dest.find("://") {
let scheme = &dest[..scheme_end];
if !scheme.is_empty()
&& scheme.starts_with(|c: char| c.is_ascii_alphabetic())
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
{
return true;
}
}
let lower = dest.to_ascii_lowercase();
lower.starts_with("mailto:") || lower.starts_with("tel:")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markdown_links() {
let content = "This is a [test link](target.md) in markdown.";
let links = extract_links(content);
assert_eq!(links.len(), 1);
assert_eq!(links[0].destination, "target.md");
assert_eq!(links[0].text, "test link");
}
#[test]
fn test_wiki_links() {
let content = "This is a [[WikiPage]] link.";
let links = extract_links(content);
assert_eq!(links.len(), 1);
assert_eq!(links[0].destination, "WikiPage");
}
#[test]
fn test_mixed_links() {
let content = "A [markdown](note1) and [[wiki]] link.";
let links = extract_links(content);
assert_eq!(links.len(), 2);
}
#[test]
fn test_is_external_link() {
assert!(is_external_link("http://example.com"));
assert!(is_external_link("https://example.com/path?q=1"));
assert!(is_external_link("ftp://files.example.com"));
assert!(is_external_link("file:///etc/hosts"));
assert!(is_external_link("HTTPS://EXAMPLE.COM"));
assert!(is_external_link(" https://example.com"));
assert!(is_external_link("mailto:user@example.com"));
assert!(is_external_link("tel:+1234567890"));
assert!(!is_external_link("frontpage"));
assert!(!is_external_link("some/note.md"));
assert!(!is_external_link("[[WikiPage]]"));
assert!(!is_external_link("Notes: Meeting"));
assert!(!is_external_link("C:\\path\\file"));
}
}