markdown_it/plugins/extra/
linkify.rs

1//! Find urls and emails, and turn them into links
2
3use linkify::{LinkFinder, LinkKind};
4use once_cell::sync::Lazy;
5use regex::Regex;
6use std::cmp::Ordering;
7
8use crate::parser::core::{CoreRule, Root};
9use crate::parser::extset::RootExt;
10use crate::parser::inline::builtin::InlineParserRule;
11use crate::parser::inline::{InlineRule, InlineState, TextSpecial};
12use crate::{MarkdownIt, Node, NodeValue, Renderer};
13
14static SCHEME_RE : Lazy<Regex> = Lazy::new(|| {
15    Regex::new(r"(?i)(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$").unwrap()
16});
17
18#[derive(Debug)]
19pub struct Linkified {
20    pub url: String,
21}
22
23impl NodeValue for Linkified {
24    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
25        let mut attrs = node.attrs.clone();
26        attrs.push(("href", self.url.clone()));
27
28        fmt.open("a", &attrs);
29        fmt.contents(&node.children);
30        fmt.close("a");
31    }
32}
33
34pub fn add(md: &mut MarkdownIt) {
35    md.add_rule::<LinkifyPrescan>()
36        .before::<InlineParserRule>();
37
38    md.inline.add_rule::<LinkifyScanner>();
39}
40
41type LinkifyState = Vec<LinkifyPosition>;
42impl RootExt for LinkifyState {}
43
44#[derive(Debug, Clone, Copy)]
45struct LinkifyPosition {
46    start: usize,
47    end:   usize,
48    //email: bool,
49}
50
51#[doc(hidden)]
52pub struct LinkifyPrescan;
53impl CoreRule for LinkifyPrescan {
54    fn run(root: &mut Node, _: &MarkdownIt) {
55        let root_data = root.cast_mut::<Root>().unwrap();
56        let source = root_data.content.as_str();
57        let finder = LinkFinder::new();
58        let positions = finder.links(source).filter_map(|link| {
59            if *link.kind() == LinkKind::Url {
60                Some(LinkifyPosition {
61                    start: link.start(),
62                    end:   link.end(),
63                    //email: *link.kind() == LinkKind::Email,
64                })
65            } else {
66                None
67            }
68        }).collect::<Vec<_>>();
69        root_data.ext.insert(positions);
70    }
71}
72
73#[doc(hidden)]
74pub struct LinkifyScanner;
75impl InlineRule for LinkifyScanner {
76    const MARKER: char = ':';
77
78    fn run(state: &mut InlineState) -> Option<(Node, usize)> {
79        let mut chars = state.src[state.pos..state.pos_max].chars();
80        if chars.next().unwrap() != ':' { return None; }
81        if state.link_level > 0 { return None; }
82
83        let trailing = state.trailing_text_get();
84        if !SCHEME_RE.is_match(trailing) { return None; }
85
86        let map = state.get_map(state.pos, state.pos_max)?;
87        let (start, _) = map.get_byte_offsets();
88
89        let positions = state.root_ext.get::<LinkifyState>().unwrap();
90
91        let found_idx = positions.binary_search_by(|x| {
92            if x.start >= start {
93                Ordering::Greater
94            } else if x.end <= start {
95                Ordering::Less
96            } else {
97                Ordering::Equal
98            }
99        }).ok()?;
100
101        let found = positions[found_idx];
102        let proto_size = start - found.start;
103        if proto_size > trailing.len() { return None; }
104
105        debug_assert_eq!(
106            &trailing[trailing.len()-proto_size..],
107            &state.src[state.pos-proto_size..state.pos]
108        );
109
110        let url_start = state.pos - proto_size;
111        let url_end = state.pos - proto_size + found.end - found.start;
112        if url_end > state.pos_max { return None; }
113
114        let url = &state.src[url_start..url_end];
115        let full_url = state.md.link_formatter.normalize_link(url);
116
117        state.md.link_formatter.validate_link(&full_url)?;
118
119        let content = state.md.link_formatter.normalize_link_text(url);
120
121        let mut inner_node = Node::new(TextSpecial {
122            content: content.clone(),
123            markup: content,
124            info: "autolink",
125        });
126        inner_node.srcmap = state.get_map(url_start, url_end);
127
128        let mut node = Node::new(Linkified { url: full_url });
129        node.children.push(inner_node);
130
131        state.trailing_text_pop(proto_size);
132        state.pos -= proto_size;
133        Some((node, url_end - url_start))
134    }
135}