1mod ctype;
2pub mod email;
3pub mod url;
4pub(crate) mod utils;
5pub mod www;
6
7pub fn match_start(contents: &str) -> Option<(String, usize)> {
10 let bytes_contents = contents.as_bytes();
11
12 if let Some((url, link_end)) = url::match_http(bytes_contents) {
13 return Some((url, link_end));
14 }
15 if let Some((url, link_end)) = www::match_www(bytes_contents) {
16 return Some((url, link_end));
17 }
18 if let Some((email, link_end)) = email::match_email(bytes_contents) {
19 return Some((email, link_end));
20 }
21 None
22}
23
24pub fn match_index(contents: &str, index: usize) -> Option<(String, usize)> {
28 if index > 0 {
29 let prev_char = contents.chars().nth(index - 1)?;
30
31 if !check_prev(prev_char) {
33 return None;
34 }
35 }
36
37 let start_contents = contents.get(index..)?;
38 let (link, skip_len) = match_start(start_contents)?;
39
40 Some((link, skip_len))
41}
42
43pub fn check_prev(prev: char) -> bool {
45 matches!(prev, ' ' | '\t' | '\r' | '\n' | '*' | '_' | '~' | '(')
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 use rstest::rstest;
53
54 #[rstest]
55 #[case("", None)]
57 #[case(" ", None)]
58 #[case("foo", None)]
59 #[case("example.com", None)]
60 #[case("www.", None)]
61 #[case("@example.com", None)]
62 #[case("http://localhost:3000", Some(("http://localhost:3000", 21)))]
64 #[case("https://localhost:3000", Some(("https://localhost:3000", 22)))]
65 #[case("http://Á.com", Some(("http://Á.com", 12)))]
66 #[case("https://www.wolframalpha.com/input/?i=x^2+(y-(x^2)^(1/3))^2=1", Some(("https://www.wolframalpha.com/input/?i=x^2+(y-(x^2)^(1/3))^2=1", 61)))]
67 #[case("www.example.com", Some(("http://www.example.com", 15)))]
69 #[case("www.Á.com", Some(("http://www.Á.com", 9)))]
70 #[case("john@example.com", Some(("mailto:john@example.com", 16)))]
72 #[case("mailto:@example.com", Some(("mailto:@example.com", 19)))]
73 #[case("xmpp:john@example.com", Some(("xmpp:john@example.com", 21)))]
74 fn test_match_start(#[case] input: &str, #[case] expected: Option<(&str, i32)>) {
75 assert_eq!(
76 match_start(input),
77 expected.and_then(|a| Some((a.0.to_string(), a.1 as usize)))
78 );
79 }
80
81 #[rstest]
82 #[case("www.commonmark.org", Some(("http://www.commonmark.org", 18)))]
84 #[case("www.commonmark.org/help for more information.", Some(("http://www.commonmark.org/help", 23)))]
86 #[case("www.commonmark.org.", Some(("http://www.commonmark.org", 18)))]
88 #[case("www.commonmark.org/a.b.", Some(("http://www.commonmark.org/a.b", 22)))]
89 #[case("www.google.com/search?q=Markup+(business)", Some(("http://www.google.com/search?q=Markup+(business)", 41)))]
91 #[case("www.google.com/search?q=Markup+(business)))", Some(("http://www.google.com/search?q=Markup+(business)", 41)))]
92 #[case("www.google.com/search?q=(business))+ok", Some(("http://www.google.com/search?q=(business))+ok", 38)))]
94 #[case("www.google.com/search?q=commonmark&hl=en", Some(("http://www.google.com/search?q=commonmark&hl=en", 40)))]
96 #[case("www.google.com/search?q=commonmark&hl;", Some(("http://www.google.com/search?q=commonmark", 34)))]
97 #[case("www.commonmark.org/he<lp", Some(("http://www.commonmark.org/he", 21)))]
99 #[case("http://commonmark.org", Some(("http://commonmark.org", 21)))]
101 #[case("https://encrypted.google.com/search?q=Markup+(business))", Some(("https://encrypted.google.com/search?q=Markup+(business)", 55)))]
102 #[case("foo@bar.baz", Some(("mailto:foo@bar.baz", 11)))]
104 #[case("hello@mail+xyz.example", None)]
106 #[case("hello+xyz@mail.example", Some(("mailto:hello+xyz@mail.example", 22)))]
107 #[case("a.b-c_d@a.b", Some(("mailto:a.b-c_d@a.b", 11)))]
109 #[case("a.b-c_d@a.b.", Some(("mailto:a.b-c_d@a.b", 11)))]
110 #[case("a.b-c_d@a.b-", None)]
111 #[case("a.b-c_d@a.b_", None)]
112 #[case("mailto:foo@bar.baz", Some(("mailto:foo@bar.baz", 18)))]
114 #[case("mailto:a.b-c_d@a.b", Some(("mailto:a.b-c_d@a.b", 18)))]
115 #[case("mailto:a.b-c_d@a.b.", Some(("mailto:a.b-c_d@a.b", 18)))]
116 #[case("mailto:a.b-c_d@a.b/", Some(("mailto:a.b-c_d@a.b", 18)))]
117 #[case("mailto:a.b-c_d@a.b-", None)]
118 #[case("mailto:a.b-c_d@a.b_", None)]
119 #[case("xmpp:foo@bar.baz", Some(("xmpp:foo@bar.baz", 16)))]
120 #[case("xmpp:foo@bar.baz.", Some(("xmpp:foo@bar.baz", 16)))]
121 #[case("xmpp:foo@bar.baz/txt", Some(("xmpp:foo@bar.baz/txt", 20)))]
123 #[case("xmpp:foo@bar.baz/txt@bin", Some(("xmpp:foo@bar.baz/txt@bin", 24)))]
124 #[case("xmpp:foo@bar.baz/txt@bin.com", Some(("xmpp:foo@bar.baz/txt@bin.com", 28)))]
125 #[case("xmpp:foo@bar.baz/txt/bin", Some(("xmpp:foo@bar.baz/txt", 20)))]
127 fn test_spec(#[case] input: &str, #[case] expected: Option<(&str, i32)>) {
128 assert_eq!(
129 match_start(input),
130 expected.and_then(|a| Some((a.0.to_string(), a.1 as usize)))
131 );
132 }
133
134 #[rstest]
135 #[case("www.commonmark.org", 0, Some(("http://www.commonmark.org", 18)))]
136 #[case(" www.commonmark.org", 0, None)]
137 #[case("www.commonmark.org", 100, None)]
138 #[case(" www.commonmark.org", 1, Some(("http://www.commonmark.org", 18)))]
139 #[case("[www.commonmark.org", 1, None)]
140 fn test_match_index(
141 #[case] input: &str,
142 #[case] index: usize,
143 #[case] expected: Option<(&str, i32)>,
144 ) {
145 assert_eq!(
146 match_index(input, index),
147 expected.and_then(|a| Some((a.0.to_string(), a.1 as usize)))
148 );
149 }
150}