pdcm_linkify/
lib.rs

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}