sable-vault/src/link.rs@main
raw
1use std::{ops::Range, sync::LazyLock};
2
3use regex::Regex;
4
5static LINK_REGEX: LazyLock<Regex> =
6 LazyLock::new(|| Regex::new(r"(?:\[(?P<text>.*?)\])\((?P<link>.*?)\)").unwrap());
7static WIKI_REGEX: LazyLock<Regex> =
8 LazyLock::new(|| Regex::new(r"\[\[(?P<link>.+?)(\|(?P<text>.+))?\]\]").unwrap());
9
10#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
15pub enum LinkKind {
16 Internal,
18 External,
20 Wiki,
24}
25
26#[derive(Debug, Clone, Hash, PartialEq, Eq)]
28pub struct Link {
29 pub kind: LinkKind,
31 pub href: String,
36 pub title: Option<String>,
41 pub pos: Range<usize>,
43}
44
45impl Link {
46 pub fn collect(s: &str) -> Vec<Self> {
48 let mut tags = Vec::new();
49
50 for captures in LINK_REGEX.captures_iter(s) {
51 let Some(full) = captures.get(0) else {
52 continue;
53 };
54 let Some(text) = captures.get(1) else {
55 continue;
56 };
57 let Some(url) = captures.get(2) else {
58 continue;
59 };
60
61 tags.push(Self {
62 kind: LinkKind::External,
63 href: url.as_str().to_string(),
64 title: Some(text.as_str().to_string()),
65 pos: full.range(),
66 });
67 }
68
69 for captures in WIKI_REGEX.captures_iter(s) {
70 let Some(full) = captures.get(0) else {
71 continue;
72 };
73 let Some(capture) = captures.get(1) else {
74 continue;
75 };
76 let title = captures.get(3).map(|c| c.as_str().to_string());
77
78 tags.push(Self {
79 kind: LinkKind::Wiki,
80 href: capture.as_str().to_string(),
81 title,
82 pos: full.range(),
83 });
84 }
85
86 tags
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_external() {
96 let text = "[test](https://example.com)";
97
98 let expected = &[Link {
99 kind: LinkKind::External,
100 href: "https://example.com".to_string(),
101 title: Some("test".to_string()),
102 pos: 0..27,
103 }];
104 let got = Link::collect(text);
105
106 assert_eq!(expected, &got[..]);
107 }
108
109 #[test]
110 fn test_wiki_no_title() {
111 let text = "[[test]]";
112
113 let expected = &[Link {
114 kind: LinkKind::Wiki,
115 href: "test".to_string(),
116 title: None,
117 pos: 0..8,
118 }];
119 let got = Link::collect(text);
120
121 assert_eq!(expected, &got[..]);
122 }
123
124 #[test]
125 fn test_wiki_with_title() {
126 let text = "[[test|title]]";
127
128 let expected = &[Link {
129 kind: LinkKind::Wiki,
130 href: "test".to_string(),
131 title: Some("title".to_string()),
132 pos: 0..14,
133 }];
134 let got = Link::collect(text);
135
136 assert_eq!(expected, &got[..]);
137 }
138
139 #[test]
140 fn test_fail_invalid_1() {
141 let text = "[[test]";
142
143 let got = Link::collect(text);
144
145 assert!(got.is_empty());
146 }
147
148 #[test]
149 fn test_fail_invalid_3() {
150 let text = "[test]]";
151
152 let got = Link::collect(text);
153
154 assert!(got.is_empty());
155 }
156
157 #[test]
158 fn test_fail_invalid_4() {
159 let text = "[[test|title]";
160
161 let got = Link::collect(text);
162
163 assert!(got.is_empty());
164 }
165
166 #[test]
167 fn test_fail_invalid_2() {
168 let text = "[test|title]]";
169
170 let got = Link::collect(text);
171
172 assert!(got.is_empty());
173 }
174}
175