| 1 | //! Find urls and emails, and turn them into links |
| 2 | |
| 3 | use linkify::{LinkFinder, LinkKind}; |
| 4 | use once_cell::sync::Lazy; |
| 5 | use regex::Regex; |
| 6 | use std::cmp::Ordering; |
| 7 | |
| 8 | use crate::parser::core::{CoreRule, Root}; |
| 9 | use crate::parser::extset::RootExt; |
| 10 | use crate::parser::inline::builtin::InlineParserRule; |
| 11 | use crate::parser::inline::{InlineRule, InlineState, TextSpecial}; |
| 12 | use crate::{MarkdownIt, Node, NodeValue, Renderer}; |
| 13 | |
| 14 | static 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)] |
| 19 | pub struct Linkified { |
| 20 | pub url: String, |
| 21 | } |
| 22 | |
| 23 | impl 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 | |
| 34 | pub fn add(md: &mut MarkdownIt) { |
| 35 | md.add_rule::<LinkifyPrescan>() |
| 36 | .before::<InlineParserRule>(); |
| 37 | |
| 38 | md.inline.add_rule::<LinkifyScanner>(); |
| 39 | } |
| 40 | |
| 41 | type LinkifyState = Vec<LinkifyPosition>; |
| 42 | impl RootExt for LinkifyState {} |
| 43 | |
| 44 | #[derive(Debug, Clone, Copy)] |
| 45 | struct LinkifyPosition { |
| 46 | start: usize, |
| 47 | end: usize, |
| 48 | //email: bool, |
| 49 | } |
| 50 | |
| 51 | #[doc(hidden)] |
| 52 | pub struct LinkifyPrescan; |
| 53 | impl 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)] |
| 74 | pub struct LinkifyScanner; |
| 75 | impl 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 | } |