1use crate::character_set::character_set;
2use crate::ctype::{isalnum, isalpha, isspace};
3use crate::nodes::{AstNode, NodeLink, NodeValue};
4use crate::parser::inlines::make_inline;
5use std::str;
6use typed_arena::Arena;
7use unicode_categories::UnicodeCategories;
8
9pub(crate) fn process_autolinks<'a>(
12 arena: &'a Arena<AstNode<'a>>,
13 node: &'a AstNode<'a>,
14 contents_str: &mut String,
15 relaxed_autolinks: bool,
16) {
17 let contents = contents_str.as_bytes();
18 let len = contents.len();
19 let mut i = 0;
20
21 while i < len {
22 let mut post_org = None;
23 let mut bracket_opening = 0;
24
25 while i < len {
27 if !relaxed_autolinks {
28 match contents[i] {
29 b'[' => {
30 bracket_opening += 1;
31 }
32 b']' => {
33 bracket_opening -= 1;
34 }
35 _ => (),
36 }
37
38 if bracket_opening > 0 {
39 i += 1;
40 continue;
41 }
42 }
43
44 if contents[i] == b'@' {
45 post_org = email_match(arena, contents, i, relaxed_autolinks);
46 if post_org.is_some() {
47 break;
48 }
49 }
50 i += 1;
51 }
52
53 if let Some((post, reverse, skip)) = post_org {
54 i -= reverse;
55 node.insert_after(post);
56 if i + skip < len {
57 let remain = str::from_utf8(&contents[i + skip..]).unwrap();
58 assert!(!remain.is_empty());
59 post.insert_after(make_inline(
60 arena,
61 NodeValue::Text(remain.to_string()),
62 (0, 1, 0, 1).into(),
63 ));
64 }
65 contents_str.truncate(i);
66 return;
67 }
68 }
69}
70
71pub fn www_match<'a>(
72 arena: &'a Arena<AstNode<'a>>,
73 contents: &[u8],
74 i: usize,
75 relaxed_autolinks: bool,
76) -> Option<(&'a AstNode<'a>, usize, usize)> {
77 const WWW_DELIMS: [bool; 256] = character_set!(b"*_~([");
78
79 if i > 0 && !isspace(contents[i - 1]) && !WWW_DELIMS[contents[i - 1] as usize] {
80 return None;
81 }
82
83 if !contents[i..].starts_with(b"www.") {
84 return None;
85 }
86
87 let mut link_end = match check_domain(&contents[i..], false) {
88 None => return None,
89 Some(link_end) => link_end,
90 };
91
92 while i + link_end < contents.len() && !isspace(contents[i + link_end]) {
93 if relaxed_autolinks && contents[i + link_end - 1] == b']' && contents[i + link_end] == b'('
95 {
96 return None;
97 }
98 link_end += 1;
99 }
100
101 link_end = autolink_delim(&contents[i..], link_end, relaxed_autolinks);
102
103 let mut url = "http://".to_string();
104 url.push_str(str::from_utf8(&contents[i..link_end + i]).unwrap());
105
106 let inl = make_inline(
107 arena,
108 NodeValue::Link(NodeLink {
109 url,
110 title: String::new(),
111 }),
112 (0, 1, 0, 1).into(),
113 );
114
115 inl.append(make_inline(
116 arena,
117 NodeValue::Text(
118 str::from_utf8(&contents[i..link_end + i])
119 .unwrap()
120 .to_string(),
121 ),
122 (0, 1, 0, 1).into(),
123 ));
124 Some((inl, 0, link_end))
125}
126
127fn check_domain(data: &[u8], allow_short: bool) -> Option<usize> {
128 let mut np = 0;
129 let mut uscore1 = 0;
130 let mut uscore2 = 0;
131
132 for (i, c) in unsafe { str::from_utf8_unchecked(data) }.char_indices() {
133 if c == '\\' && i < data.len() - 1 {
134 } else if c == '_' {
137 uscore2 += 1;
138 } else if c == '.' {
139 uscore1 = uscore2;
140 uscore2 = 0;
141 np += 1;
142 } else if !is_valid_hostchar(c) && c != '-' {
143 if uscore1 == 0 && uscore2 == 0 && (allow_short || np > 0) {
144 return Some(i);
145 }
146 return None;
147 }
148 }
149
150 if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
151 None
152 } else if allow_short || np > 0 {
153 Some(data.len())
154 } else {
155 None
156 }
157}
158
159fn is_valid_hostchar(ch: char) -> bool {
160 !(ch.is_whitespace() || ch.is_punctuation() || ch.is_symbol())
161}
162
163fn autolink_delim(data: &[u8], mut link_end: usize, relaxed_autolinks: bool) -> usize {
164 const LINK_END_ASSORTMENT: [bool; 256] = character_set!(b"?!.,:*_~'\"");
165
166 for (i, &b) in data.iter().enumerate().take(link_end) {
167 if b == b'<' {
168 link_end = i;
169 break;
170 }
171 }
172
173 while link_end > 0 {
174 let cclose = data[link_end - 1];
175
176 let mut copen = if cclose == b')' { Some(b'(') } else { None };
181
182 if relaxed_autolinks && copen.is_none() {
183 copen = if cclose == b']' {
185 Some(b'[')
186 } else if cclose == b'}' {
187 Some(b'{')
188 } else {
189 None
190 };
191 }
192
193 if LINK_END_ASSORTMENT[cclose as usize] {
194 link_end -= 1;
195 } else if cclose == b';' {
196 let mut new_end = link_end - 2;
197
198 while new_end > 0 && isalpha(data[new_end]) {
199 new_end -= 1;
200 }
201
202 if new_end < link_end - 2 && data[new_end] == b'&' {
203 link_end = new_end;
204 } else {
205 link_end -= 1;
206 }
207 } else if let Some(copen) = copen {
208 let mut opening = 0;
209 let mut closing = 0;
210 for &b in data.iter().take(link_end) {
211 if b == copen {
212 opening += 1;
213 } else if b == cclose {
214 closing += 1;
215 }
216 }
217
218 if closing <= opening {
219 break;
220 }
221
222 link_end -= 1;
223 } else {
224 break;
225 }
226 }
227
228 link_end
229}
230
231pub fn url_match<'a>(
232 arena: &'a Arena<AstNode<'a>>,
233 contents: &[u8],
234 i: usize,
235 relaxed_autolinks: bool,
236) -> Option<(&'a AstNode<'a>, usize, usize)> {
237 const SCHEMES: [&[u8]; 3] = [b"http", b"https", b"ftp"];
238
239 let size = contents.len();
240
241 if size - i < 4 || contents[i + 1] != b'/' || contents[i + 2] != b'/' {
242 return None;
243 }
244
245 let mut rewind = 0;
246 while rewind < i && isalpha(contents[i - rewind - 1]) {
247 rewind += 1;
248 }
249
250 if !relaxed_autolinks {
251 let cond = |s: &&[u8]| size - i + rewind >= s.len() && &&contents[i - rewind..i] == s;
252 if !SCHEMES.iter().any(cond) {
253 return None;
254 }
255 }
256
257 let mut link_end = match check_domain(&contents[i + 3..], true) {
258 None => return None,
259 Some(link_end) => link_end,
260 };
261
262 while link_end < size - i && !isspace(contents[i + link_end]) {
263 if relaxed_autolinks
265 && link_end > 0
266 && contents[i + link_end - 1] == b']'
267 && contents[i + link_end] == b'('
268 {
269 return None;
270 }
271 link_end += 1;
272 }
273
274 link_end = autolink_delim(&contents[i..], link_end, relaxed_autolinks);
275
276 let url = str::from_utf8(&contents[i - rewind..i + link_end])
277 .unwrap()
278 .to_string();
279 let inl = make_inline(
280 arena,
281 NodeValue::Link(NodeLink {
282 url: url.clone(),
283 title: String::new(),
284 }),
285 (0, 1, 0, 1).into(),
286 );
287
288 inl.append(make_inline(
289 arena,
290 NodeValue::Text(url),
291 (0, 1, 0, 1).into(),
292 ));
293 Some((inl, rewind, rewind + link_end))
294}
295
296fn email_match<'a>(
297 arena: &'a Arena<AstNode<'a>>,
298 contents: &[u8],
299 i: usize,
300 relaxed_autolinks: bool,
301) -> Option<(&'a AstNode<'a>, usize, usize)> {
302 const EMAIL_OK_SET: [bool; 256] = character_set!(b".+-_");
303
304 let size = contents.len();
305
306 let mut auto_mailto = true;
307 let mut is_xmpp = false;
308 let mut rewind = 0;
309
310 while rewind < i {
311 let c = contents[i - rewind - 1];
312
313 if isalnum(c) || EMAIL_OK_SET[c as usize] {
314 rewind += 1;
315 continue;
316 }
317
318 if c == b':' {
319 if validate_protocol("mailto", contents, i - rewind - 1) {
320 auto_mailto = false;
321 rewind += 1;
322 continue;
323 }
324
325 if validate_protocol("xmpp", contents, i - rewind - 1) {
326 is_xmpp = true;
327 auto_mailto = false;
328 rewind += 1;
329 continue;
330 }
331 }
332
333 break;
334 }
335
336 if rewind == 0 {
337 return None;
338 }
339
340 let mut link_end = 1;
341 let mut np = 0;
342
343 while link_end < size - i {
344 let c = contents[i + link_end];
345
346 if isalnum(c) {
347 } else if c == b'@' {
349 return None;
350 } else if c == b'.' && link_end < size - i - 1 && isalnum(contents[i + link_end + 1]) {
351 np += 1;
352 } else if c == b'/' && is_xmpp {
353 } else if c != b'-' && c != b'_' {
355 break;
356 }
357
358 link_end += 1;
359 }
360
361 if link_end < 2
362 || np == 0
363 || (!isalpha(contents[i + link_end - 1]) && contents[i + link_end - 1] != b'.')
364 {
365 return None;
366 }
367
368 link_end = autolink_delim(&contents[i..], link_end, relaxed_autolinks);
369 if link_end == 0 {
370 return None;
371 }
372
373 let mut url = if auto_mailto {
374 "mailto:".to_string()
375 } else {
376 "".to_string()
377 };
378 let text = str::from_utf8(&contents[i - rewind..link_end + i]).unwrap();
379 url.push_str(text);
380
381 let inl = make_inline(
382 arena,
383 NodeValue::Link(NodeLink {
384 url,
385 title: String::new(),
386 }),
387 (0, 1, 0, 1).into(),
388 );
389
390 inl.append(make_inline(
391 arena,
392 NodeValue::Text(text.to_string()),
393 (0, 1, 0, 1).into(),
394 ));
395 Some((inl, rewind, rewind + link_end))
396}
397
398fn validate_protocol(protocol: &str, contents: &[u8], cursor: usize) -> bool {
399 let size = contents.len();
400 let mut rewind = 0;
401
402 while rewind < cursor && isalpha(contents[cursor - rewind - 1]) {
403 rewind += 1;
404 }
405
406 size - cursor + rewind >= protocol.len()
407 && &contents[cursor - rewind..cursor] == protocol.as_bytes()
408}