markdown_linkify/transform/
substitution.rs

1use pulldown_cmark::{Event, LinkType};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4
5use crate::{link::Link, LinkTransformer};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct Substitution {
9    #[serde(with = "serde_regex")]
10    tail: Regex,
11    tag: String,
12    replacement: String,
13
14    #[serde(default = "one")]
15    limit: usize,
16
17    #[serde(default)]
18    code: bool,
19
20    #[serde(default)]
21    tail_only: bool,
22
23    #[serde(default)]
24    replace_text: bool,
25}
26
27/// Workaround for <https://github.com/serde-rs/serde/issues/368>
28const fn one() -> usize {
29    1
30}
31
32impl Substitution {
33    #[must_use]
34    pub fn example() -> Self {
35        Self {
36            tail: regex::Regex::new(r"\d+").expect("Invalid example regex"),
37            tag: String::from("PS-"),
38            replacement: "mycompany.jira.com/issues/PS-$text".to_string(),
39            limit: 1,
40            code: false,
41            tail_only: false,
42            replace_text: false,
43        }
44    }
45}
46
47/// A [`Substitution`] is a link transformer of sorts, too.
48impl LinkTransformer for Substitution {
49    fn tag(&self) -> String {
50        self.tag.clone()
51    }
52
53    fn pattern(&self) -> Regex {
54        Regex::new(format!("(?<text>{}{})", self.tag(), self.tail).as_str()).expect("Invalid regex")
55    }
56
57    fn strip_tag(&self) -> bool {
58        self.tail_only
59    }
60
61    /// Perform the replacement.
62    fn apply(&self, link: &mut Link) -> anyhow::Result<()> {
63        if link.link_type == LinkType::ShortcutUnknown {
64            return Ok(());
65        }
66        let snippet = &self
67            .pattern()
68            .replacen(&link.destination, self.limit, &self.replacement);
69
70        let new_text = link.destination.clone();
71        let text = if let Some(caps) = self.tail.captures(&link.destination) {
72            caps.name(self.tail.as_str())
73                .map(|m: regex::Match<'_>| m.as_str())
74                .unwrap_or(&new_text)
75                .to_string()
76        } else {
77            snippet.to_string()
78        };
79
80        let text = if self.strip_tag() && text.starts_with(&self.tag()) {
81            text.replace(&self.tag(), "")
82        } else {
83            text
84        };
85        let event = if self.code {
86            Event::Code(text.into())
87        } else {
88            Event::Text(text.into())
89        };
90
91        link.destination = snippet.to_string().into();
92        if link.text.is_empty() || self.replace_text {
93            link.text = vec![event];
94        }
95        if link.title.is_empty() {
96            link.title = link.destination.clone();
97        }
98        Ok(())
99    }
100}
101
102#[cfg(test)]
103mod test {
104    use super::*;
105
106    #[test]
107    fn grtp_replacement() {
108        // let input = "[](GTPR-12355)";
109        let sub = Substitution {
110            tail: Regex::new(r"\d+").unwrap(),
111            tag: "GTPR-".into(),
112            replacement: "http://www.grtp.de/issue/$text".to_string(),
113            limit: 0,
114            code: true,
115            tail_only: false,
116            replace_text: false,
117        };
118        let link = &mut Link {
119            link_type: LinkType::Reference,
120            destination: "GTPR-12355".into(),
121            title: "".into(),
122            text: vec![],
123        };
124        sub.apply(link).unwrap();
125        dbg!(link);
126    }
127
128    #[test]
129    fn struct_keyword_replacement() {
130        let sub = Substitution {
131            tail: Regex::new(r"(?<word>\w+)").unwrap(),
132            tag: "keyword".into(),
133            replacement: "https://doc.rust-lang.org/std/keyword.$word.html".to_string(),
134            limit: 1,
135            code: true,
136            tail_only: true,
137            replace_text: false,
138        };
139        let link = &mut Link {
140            link_type: LinkType::Autolink,
141            destination: "keyword:struct".into(),
142            title: "".into(),
143            text: vec![],
144        };
145        sub.apply(link).unwrap();
146        dbg!(link);
147    }
148}