1extern crate pulldown_cmark;
2extern crate regex;
3
4use pulldown_cmark::{CowStr, Event, LinkType, Tag};
5use regex::Regex;
6
7static URL_REGEX: &str = r#"((https?|ftp)://|www.)[^\s/$.?#].[^\s]*[^.^\s]"#;
8
9enum LinkState {
10 Open,
11 Label,
12 Close,
13}
14
15enum AutoLinkerState<'a> {
16 Clear,
17 Link(LinkState, CowStr<'a>, CowStr<'a>),
18 TrailingText(CowStr<'a>),
19}
20
21pub struct AutoLinker<'a, I> {
22 iter: I,
23 state: AutoLinkerState<'a>,
24 regex: Regex,
25}
26
27impl<'a, I> AutoLinker<'a, I> {
28 pub fn new(iter: I) -> Self {
29 Self {
30 iter,
31 state: AutoLinkerState::Clear,
32 regex: Regex::new(URL_REGEX).unwrap(),
33 }
34 }
35}
36
37impl<'a, I> Iterator for AutoLinker<'a, I>
38where
39 I: Iterator<Item = Event<'a>>,
40{
41 type Item = Event<'a>;
42
43 fn next(&mut self) -> Option<Self::Item> {
44 let text = match std::mem::replace(&mut self.state, AutoLinkerState::Clear) {
45 AutoLinkerState::Clear => match self.iter.next() {
46 Some(Event::Text(text)) => text,
47 x => return x,
48 },
49 AutoLinkerState::TrailingText(text) => text,
50 AutoLinkerState::Link(link_state, link_text, trailing_text) => match link_state {
51 LinkState::Open => {
52 self.state = AutoLinkerState::Link(
53 LinkState::Label,
54 link_text.clone(),
55 trailing_text.clone(),
56 );
57 return Some(Event::Start(Tag::Link(
58 LinkType::Inline,
59 link_text,
60 "".into(),
61 )));
62 }
63 LinkState::Label => {
64 self.state = AutoLinkerState::Link(
65 LinkState::Close,
66 link_text.clone(),
67 trailing_text.clone(),
68 );
69 return Some(Event::Text(link_text));
70 }
71 LinkState::Close => {
72 self.state = AutoLinkerState::TrailingText(trailing_text);
73 return Some(Event::End(Tag::Link(
74 LinkType::Inline,
75 link_text,
76 "".into(),
77 )));
78 }
79 },
80 };
81
82 match self.regex.find(&text) {
83 Some(reg_match) => {
84 let link_text = reg_match.as_str();
85 let leading_text = &text.as_ref()[..reg_match.start()];
86 let trailing_text = &text.as_ref()[reg_match.end()..];
87
88 self.state = AutoLinkerState::Link(
89 LinkState::Open,
90 link_text.to_owned().into(),
91 trailing_text.to_owned().into(),
92 );
93
94 Some(Event::Text(leading_text.to_owned().into()))
95 }
96 None => Some(Event::Text(text)),
97 }
98 }
99}