1use std::{borrow::Borrow, borrow::Cow, hash::Hash};
2
3use litcheck::{
4 diagnostics::{Diagnostic, SourceSpan, Span},
5 StaticCow,
6};
7use serde::Deserialize;
8
9use crate::FxIndexMap;
10
11#[derive(Diagnostic, Debug, thiserror::Error)]
12#[error("invalid substitution pattern: '{pattern}': {error}")]
13#[diagnostic()]
14pub struct InvalidSubstitutionPatternError {
15 #[label("{error}")]
16 span: SourceSpan,
17 #[source]
18 error: regex::Error,
19 pattern: String,
20}
21
22pub struct ScopedSubstitutionSet<'a> {
23 parent: &'a SubstitutionSet,
24 set: SubstitutionSet,
25}
26impl<'scope> ScopedSubstitutionSet<'scope> {
27 pub fn new(parent: &'scope SubstitutionSet) -> Self {
28 Self {
29 parent,
30 set: SubstitutionSet::default(),
31 }
32 }
33
34 #[allow(unused)]
35 pub fn is_empty(&self) -> bool {
36 self.set.is_empty() && self.parent.is_empty()
37 }
38
39 #[allow(unused)]
40 pub fn get<Q>(&self, pattern: &Q) -> Option<&StaticCow<str>>
41 where
42 StaticCow<str>: Borrow<Q>,
43 Q: Hash + Eq + ?Sized,
44 {
45 self.set.get(pattern).or_else(|| self.parent.get(pattern))
46 }
47
48 pub fn contains<Q>(&self, pattern: &Q) -> bool
49 where
50 StaticCow<str>: Borrow<Q>,
51 Q: Hash + Eq + ?Sized,
52 {
53 self.set.contains(pattern) || self.parent.contains(pattern)
54 }
55
56 #[inline]
57 pub fn iter(&self) -> impl Iterator<Item = (&StaticCow<str>, &StaticCow<str>)> + '_ {
58 SubstitutionsIter {
59 set: &self.set,
60 iter: self.parent.iter(),
61 filter: true,
62 }
63 }
64
65 pub fn find_matching<'a>(&'a self, pattern: &'a str) -> Matches<'a> {
67 let has_exact = self.contains(pattern);
68 let has_fuzzy = self
69 .iter()
70 .any(|(k, _)| k != pattern && k.contains(pattern));
71 match (has_exact, has_fuzzy) {
72 (false, _) => Matches::Empty,
73 (true, false) => Matches::Exact(pattern),
74 (true, true) => Matches::Fuzzy {
75 exact: pattern,
76 keys: SubstitutionsIter {
77 set: &self.set,
78 iter: self.parent.iter(),
79 filter: true,
80 },
81 },
82 }
83 }
84
85 pub fn extend<I, K, V>(&mut self, substitutions: I)
86 where
87 StaticCow<str>: From<K>,
88 StaticCow<str>: From<V>,
89 I: IntoIterator<Item = (K, V)>,
90 {
91 let substitutions = substitutions
92 .into_iter()
93 .map(|(k, v)| (StaticCow::from(k), StaticCow::from(v)));
94 self.set.extend(substitutions);
95 }
96
97 pub fn insert<K, V>(&mut self, pattern: K, replacement: V)
98 where
99 StaticCow<str>: From<K>,
100 StaticCow<str>: From<V>,
101 {
102 self.set
103 .insert(StaticCow::from(pattern), StaticCow::from(replacement));
104 }
105
106 pub fn apply<'a>(
107 &self,
108 input: Span<&'a str>,
109 ) -> Result<Cow<'a, str>, InvalidSubstitutionPatternError> {
110 let escape_re = regex::Regex::new("%%").unwrap();
111
112 let (span, input) = input.into_parts();
113 let mut buffer = Cow::Borrowed(input);
114 let mut needs_unescape = false;
115 let mut needs_escaping = true;
116 for (pattern, replacement) in self.iter() {
117 if needs_escaping {
118 if let Cow::Owned(escaped) =
119 escape_re.replace_all(&buffer, regex::NoExpand("#_MARKER_#"))
120 {
121 buffer = Cow::Owned(escaped);
122 needs_unescape = true;
123 } else {
124 needs_escaping = false;
125 }
126 }
127 let re =
128 regex::Regex::new(pattern).map_err(|error| InvalidSubstitutionPatternError {
129 span,
130 error,
131 pattern: pattern.clone().into_owned(),
132 })?;
133 if let Cow::Owned(replaced) = re.replace_all(&buffer, replacement) {
134 buffer = Cow::Owned(replaced);
135 needs_escaping = true;
136 }
137 }
138
139 if needs_unescape {
140 let unescape_re = regex::Regex::new("#_MARKER_#").unwrap();
141 if let Cow::Owned(unescaped) = unescape_re.replace_all(&buffer, regex::NoExpand("%")) {
142 buffer = Cow::Owned(unescaped);
143 }
144 }
145
146 Ok(buffer)
147 }
148}
149
150pub struct SubstitutionsIter<'a> {
151 set: &'a SubstitutionSet,
152 iter: indexmap::map::Iter<'a, StaticCow<str>, StaticCow<str>>,
153 filter: bool,
154}
155impl<'a> Iterator for SubstitutionsIter<'a> {
156 type Item = (&'a StaticCow<str>, &'a StaticCow<str>);
157
158 fn next(&mut self) -> Option<Self::Item> {
159 if self.filter {
160 let result = self
161 .iter
162 .next()
163 .and_then(|item @ (k, _)| self.set.get(k).map(|v| (k, v)).or(Some(item)));
164 if result.is_none() {
165 self.iter = self.set.iter();
166 self.filter = false;
167 } else {
168 return result;
169 }
170 }
171 self.iter.next()
172 }
173}
174
175#[derive(Default, Clone, Debug, Deserialize)]
176#[serde(transparent)]
177pub struct SubstitutionSet {
178 set: FxIndexMap<StaticCow<str>, StaticCow<str>>,
179}
180impl IntoIterator for SubstitutionSet {
181 type Item = (StaticCow<str>, StaticCow<str>);
182 type IntoIter = indexmap::map::IntoIter<StaticCow<str>, StaticCow<str>>;
183
184 #[inline]
185 fn into_iter(self) -> Self::IntoIter {
186 self.set.into_iter()
187 }
188}
189impl SubstitutionSet {
190 #[allow(unused)]
191 pub fn new<I, K, V>(substitutions: I) -> Self
192 where
193 StaticCow<str>: From<K>,
194 StaticCow<str>: From<V>,
195 I: IntoIterator<Item = (K, V)>,
196 {
197 let set = substitutions
198 .into_iter()
199 .map(|(k, v)| (StaticCow::from(k), StaticCow::from(v)))
200 .collect();
201 Self { set }
202 }
203
204 #[inline]
205 pub fn is_empty(&self) -> bool {
206 self.set.is_empty()
207 }
208
209 pub fn contains<Q>(&self, pattern: &Q) -> bool
210 where
211 StaticCow<str>: Borrow<Q>,
212 Q: Hash + Eq + ?Sized,
213 {
214 self.set.contains_key(pattern)
215 }
216
217 pub fn get<Q>(&self, pattern: &Q) -> Option<&StaticCow<str>>
218 where
219 StaticCow<str>: Borrow<Q>,
220 Q: Hash + Eq + ?Sized,
221 {
222 self.set.get(pattern)
223 }
224
225 #[inline]
226 pub fn iter(&self) -> indexmap::map::Iter<'_, StaticCow<str>, StaticCow<str>> {
227 self.set.iter()
228 }
229
230 #[inline]
231 pub fn keys(&self) -> indexmap::map::Keys<'_, StaticCow<str>, StaticCow<str>> {
232 self.set.keys()
233 }
234
235 pub fn extend<I, K, V>(&mut self, substitutions: I)
236 where
237 StaticCow<str>: From<K>,
238 StaticCow<str>: From<V>,
239 I: IntoIterator<Item = (K, V)>,
240 {
241 let substitutions = substitutions
242 .into_iter()
243 .map(|(k, v)| (StaticCow::from(k), StaticCow::from(v)));
244 self.set.extend(substitutions);
245 }
246
247 pub fn insert<K, V>(&mut self, pattern: K, replacement: V)
248 where
249 StaticCow<str>: From<K>,
250 StaticCow<str>: From<V>,
251 {
252 self.set
253 .insert(StaticCow::from(pattern), StaticCow::from(replacement));
254 }
255}
256
257pub enum Matches<'a> {
258 Empty,
259 Exact(&'a str),
260 Fuzzy {
261 exact: &'a str,
262 keys: SubstitutionsIter<'a>,
263 },
264}
265impl<'a> Iterator for Matches<'a> {
266 type Item = &'a str;
267
268 fn next(&mut self) -> Option<Self::Item> {
269 match self {
270 Self::Empty => None,
271 Self::Exact(s) => {
272 let s = *s;
273 *self = Self::Empty;
274 Some(s)
275 }
276 Self::Fuzzy { exact, keys } => loop {
277 if let Some((key, _)) = keys.next() {
278 if key.contains(*exact) {
279 break Some(key.as_ref());
280 }
281 } else {
282 *self = Self::Empty;
283 return None;
284 }
285 },
286 }
287 }
288}