markdown_linkify/transform/
substitution.rs1use 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
27const 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
47impl 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 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 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}