| 1 | //! Autolinks |
| 2 | //! |
| 3 | //! `<https://example.org>` |
| 4 | //! |
| 5 | //! <https://spec.commonmark.org/0.30/#autolinks> |
| 6 | use once_cell::sync::Lazy; |
| 7 | use regex::Regex; |
| 8 | |
| 9 | use crate::parser::inline::{InlineRule, InlineState, TextSpecial}; |
| 10 | use crate::{MarkdownIt, Node, NodeValue, Renderer}; |
| 11 | |
| 12 | #[derive(Debug)] |
| 13 | pub struct Autolink { |
| 14 | pub url: String, |
| 15 | } |
| 16 | |
| 17 | impl NodeValue for Autolink { |
| 18 | fn render(&self, node: &Node, fmt: &mut dyn Renderer) { |
| 19 | let mut attrs = node.attrs.clone(); |
| 20 | attrs.push(("href", self.url.clone())); |
| 21 | |
| 22 | fmt.open("a", &attrs); |
| 23 | fmt.contents(&node.children); |
| 24 | fmt.close("a"); |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | pub fn add(md: &mut MarkdownIt) { |
| 29 | md.inline.add_rule::<AutolinkScanner>(); |
| 30 | } |
| 31 | |
| 32 | static AUTOLINK_RE : Lazy<Regex> = Lazy::new(|| { |
| 33 | Regex::new(r"^([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)$").unwrap() |
| 34 | }); |
| 35 | |
| 36 | static EMAIL_RE : Lazy<Regex> = Lazy::new(|| { |
| 37 | Regex::new(r"^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$").unwrap() |
| 38 | }); |
| 39 | |
| 40 | #[doc(hidden)] |
| 41 | pub struct AutolinkScanner; |
| 42 | impl InlineRule for AutolinkScanner { |
| 43 | const MARKER: char = '<'; |
| 44 | |
| 45 | fn run(state: &mut InlineState) -> Option<(Node, usize)> { |
| 46 | let mut chars = state.src[state.pos..state.pos_max].chars(); |
| 47 | if chars.next().unwrap() != '<' { return None; } |
| 48 | |
| 49 | let mut pos = state.pos + 2; |
| 50 | |
| 51 | loop { |
| 52 | match chars.next() { |
| 53 | Some('<') | None => return None, |
| 54 | Some('>') => break, |
| 55 | Some(x) => pos += x.len_utf8(), |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | let url = &state.src[state.pos+1..pos-1]; |
| 60 | let is_autolink = AUTOLINK_RE.is_match(url); |
| 61 | let is_email = EMAIL_RE.is_match(url); |
| 62 | |
| 63 | if !is_autolink && !is_email { return None; } |
| 64 | |
| 65 | let full_url = if is_autolink { |
| 66 | state.md.link_formatter.normalize_link(url) |
| 67 | } else { |
| 68 | state.md.link_formatter.normalize_link(&("mailto:".to_owned() + url)) |
| 69 | }; |
| 70 | |
| 71 | state.md.link_formatter.validate_link(&full_url)?; |
| 72 | |
| 73 | let content = state.md.link_formatter.normalize_link_text(url); |
| 74 | |
| 75 | let mut inner_node = Node::new(TextSpecial { |
| 76 | content: content.clone(), |
| 77 | markup: content, |
| 78 | info: "autolink", |
| 79 | }); |
| 80 | inner_node.srcmap = state.get_map(state.pos + 1, pos - 1); |
| 81 | |
| 82 | let mut node = Node::new(Autolink { url: full_url }); |
| 83 | node.children.push(inner_node); |
| 84 | |
| 85 | Some((node, pos - state.pos)) |
| 86 | } |
| 87 | } |