1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Find urls and emails, and turn them into links

use linkify::{LinkFinder, LinkKind};
use once_cell::sync::Lazy;
use regex::Regex;
use std::cmp::Ordering;

use crate::parser::core::{CoreRule, Root};
use crate::parser::extset::RootExt;
use crate::parser::inline::builtin::InlineParserRule;
use crate::parser::inline::{InlineRule, InlineState, TextSpecial};
use crate::{MarkdownIt, Node, NodeValue, Renderer};

static SCHEME_RE : Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?i)(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$").unwrap()
});

#[derive(Debug)]
pub struct Linkified {
    pub url: String,
}

impl NodeValue for Linkified {
    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
        let mut attrs = node.attrs.clone();
        attrs.push(("href", self.url.clone()));

        fmt.open("a", &attrs);
        fmt.contents(&node.children);
        fmt.close("a");
    }
}

pub fn add(md: &mut MarkdownIt) {
    md.add_rule::<LinkifyPrescan>()
        .before::<InlineParserRule>();

    md.inline.add_rule::<LinkifyScanner>();
}

type LinkifyState = Vec<LinkifyPosition>;
impl RootExt for LinkifyState {}

#[derive(Debug, Clone, Copy)]
struct LinkifyPosition {
    start: usize,
    end:   usize,
    //email: bool,
}

#[doc(hidden)]
pub struct LinkifyPrescan;
impl CoreRule for LinkifyPrescan {
    fn run(root: &mut Node, _: &MarkdownIt) {
        let root_data = root.cast_mut::<Root>().unwrap();
        let source = root_data.content.as_str();
        let finder = LinkFinder::new();
        let positions = finder.links(source).filter_map(|link| {
            if *link.kind() == LinkKind::Url {
                Some(LinkifyPosition {
                    start: link.start(),
                    end:   link.end(),
                    //email: *link.kind() == LinkKind::Email,
                })
            } else {
                None
            }
        }).collect::<Vec<_>>();
        root_data.ext.insert(positions);
    }
}

#[doc(hidden)]
pub struct LinkifyScanner;
impl InlineRule for LinkifyScanner {
    const MARKER: char = ':';

    fn run(state: &mut InlineState) -> Option<(Node, usize)> {
        let mut chars = state.src[state.pos..state.pos_max].chars();
        if chars.next().unwrap() != ':' { return None; }
        if state.link_level > 0 { return None; }

        let trailing = state.trailing_text_get();
        if !SCHEME_RE.is_match(trailing) { return None; }

        let map = state.get_map(state.pos, state.pos_max)?;
        let (start, _) = map.get_byte_offsets();

        let positions = state.root_ext.get::<LinkifyState>().unwrap();

        let found_idx = positions.binary_search_by(|x| {
            if x.start >= start {
                Ordering::Greater
            } else if x.end <= start {
                Ordering::Less
            } else {
                Ordering::Equal
            }
        }).ok()?;

        let found = positions[found_idx];
        let proto_size = start - found.start;
        if proto_size > trailing.len() { return None; }

        debug_assert_eq!(
            &trailing[trailing.len()-proto_size..],
            &state.src[state.pos-proto_size..state.pos]
        );

        let url_start = state.pos - proto_size;
        let url_end = state.pos - proto_size + found.end - found.start;
        if url_end > state.pos_max { return None; }

        let url = &state.src[url_start..url_end];
        let full_url = state.md.link_formatter.normalize_link(url);

        state.md.link_formatter.validate_link(&full_url)?;

        let content = state.md.link_formatter.normalize_link_text(url);

        let mut inner_node = Node::new(TextSpecial {
            content: content.clone(),
            markup: content,
            info: "autolink",
        });
        inner_node.srcmap = state.get_map(url_start, url_end);

        let mut node = Node::new(Linkified { url: full_url });
        node.children.push(inner_node);

        state.trailing_text_pop(proto_size);
        state.pos -= proto_size;
        Some((node, url_end - url_start))
    }
}