1#![allow(dead_code)]
2
3use pulldown_cmark::{Event, Parser, Tag, TagEnd};
4use regex::Regex;
5
6#[derive(Debug, Clone)]
7pub struct Link {
8 pub start: usize,
9 pub end: usize,
10 pub destination: String,
11 pub text: String,
12}
13
14pub fn extract_links(content: &str) -> Vec<Link> {
16 let mut links = Vec::new();
17 let parser = Parser::new(content);
18
19 let mut current_link: Option<(usize, String)> = None;
20 let mut link_text = String::new();
21
22 for (event, range) in parser.into_offset_iter() {
23 match event {
24 Event::Start(Tag::Link { dest_url, .. }) => {
25 current_link = Some((range.start, dest_url.to_string()));
26 link_text.clear();
27 }
28 Event::Text(text) if current_link.is_some() => {
29 link_text.push_str(&text);
30 }
31 Event::End(TagEnd::Link) => {
32 if let Some((start, dest)) = current_link.take() {
33 links.push(Link {
34 start,
35 end: range.end,
36 destination: dest,
37 text: link_text.clone(),
38 });
39 }
40 }
41 _ => {}
42 }
43 }
44
45 let wiki_link_re = Regex::new(r"\[\[([^\]]+)\]\]").unwrap();
47 for cap in wiki_link_re.captures_iter(content) {
48 if let Some(matched) = cap.get(0) {
49 let note = cap.get(1).unwrap().as_str().to_string();
50 links.push(Link {
51 start: matched.start(),
52 end: matched.end(),
53 destination: note.clone(),
54 text: note,
55 });
56 }
57 }
58
59 links.sort_by_key(|l| l.start);
60 links
61}
62
63pub fn find_link_at_position(links: &[Link], pos: usize) -> Option<&Link> {
65 links
66 .iter()
67 .find(|link| pos >= link.start && pos < link.end)
68}
69
70pub fn is_external_link(destination: &str) -> bool {
78 let dest = destination.trim_start();
79
80 if let Some(scheme_end) = dest.find("://") {
82 let scheme = &dest[..scheme_end];
83 if !scheme.is_empty()
84 && scheme.starts_with(|c: char| c.is_ascii_alphabetic())
85 && scheme
86 .chars()
87 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
88 {
89 return true;
90 }
91 }
92
93 let lower = dest.to_ascii_lowercase();
95 lower.starts_with("mailto:") || lower.starts_with("tel:")
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_markdown_links() {
104 let content = "This is a [test link](target.md) in markdown.";
105 let links = extract_links(content);
106 assert_eq!(links.len(), 1);
107 assert_eq!(links[0].destination, "target.md");
108 assert_eq!(links[0].text, "test link");
109 }
110
111 #[test]
112 fn test_wiki_links() {
113 let content = "This is a [[WikiPage]] link.";
114 let links = extract_links(content);
115 assert_eq!(links.len(), 1);
116 assert_eq!(links[0].destination, "WikiPage");
117 }
118
119 #[test]
120 fn test_mixed_links() {
121 let content = "A [markdown](note1) and [[wiki]] link.";
122 let links = extract_links(content);
123 assert_eq!(links.len(), 2);
124 }
125
126 #[test]
127 fn test_is_external_link() {
128 assert!(is_external_link("http://example.com"));
130 assert!(is_external_link("https://example.com/path?q=1"));
131 assert!(is_external_link("ftp://files.example.com"));
132 assert!(is_external_link("file:///etc/hosts"));
133 assert!(is_external_link("HTTPS://EXAMPLE.COM"));
134 assert!(is_external_link(" https://example.com"));
135
136 assert!(is_external_link("mailto:user@example.com"));
138 assert!(is_external_link("tel:+1234567890"));
139
140 assert!(!is_external_link("frontpage"));
142 assert!(!is_external_link("some/note.md"));
143 assert!(!is_external_link("[[WikiPage]]"));
144 assert!(!is_external_link("Notes: Meeting"));
145 assert!(!is_external_link("C:\\path\\file"));
146 }
147}