markdown_that/plugins/cmark/inline/
autolink.rs1use regex::Regex;
8use std::sync::LazyLock;
9
10use crate::parser::inline::{InlineRule, InlineState, TextSpecial};
11use crate::{MarkdownThat, Node, NodeValue, Renderer};
12
13#[derive(Debug)]
14pub struct Autolink {
15 pub url: String,
16}
17
18impl NodeValue for Autolink {
19 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
20 let mut attrs = node.attrs.clone();
21 attrs.push(("href", self.url.clone()));
22
23 fmt.open("a", &attrs);
24 fmt.contents(&node.children);
25 fmt.close("a");
26 }
27}
28
29pub fn add(md: &mut MarkdownThat) {
30 md.inline.add_rule::<AutolinkScanner>();
31}
32
33static AUTOLINK_RE: LazyLock<Regex> =
34 LazyLock::new(|| Regex::new(r"^([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)$").unwrap());
35
36static EMAIL_RE: LazyLock<Regex> = LazyLock::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)]
41pub struct AutolinkScanner;
42impl 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() != '<' {
48 return None;
49 }
50
51 let mut pos = state.pos + 2;
52
53 loop {
54 match chars.next() {
55 Some('<') | None => return None,
56 Some('>') => break,
57 Some(x) => pos += x.len_utf8(),
58 }
59 }
60
61 let url = &state.src[state.pos + 1..pos - 1];
62 let is_autolink = AUTOLINK_RE.is_match(url);
63 let is_email = EMAIL_RE.is_match(url);
64
65 if !is_autolink && !is_email {
66 return None;
67 }
68
69 let full_url = if is_autolink {
70 state.md.link_formatter.normalize_link(url)
71 } else {
72 state
73 .md
74 .link_formatter
75 .normalize_link(&("mailto:".to_owned() + url))
76 };
77
78 state.md.link_formatter.validate_link(&full_url)?;
79
80 let content = state.md.link_formatter.normalize_link_text(url);
81
82 let mut inner_node = Node::new(TextSpecial {
83 content: content.clone(),
84 markup: content,
85 info: "autolink",
86 });
87 inner_node.srcmap = state.get_map(state.pos + 1, pos - 1);
88
89 let mut node = Node::new(Autolink { url: full_url });
90 node.children.push(inner_node);
91
92 Some((node, pos - state.pos))
93 }
94}