1use miette::{IntoDiagnostic, Result};
2use rhai::{Dynamic, EvalAltResult, ImmutableString, Map, NativeCallContext};
3use rhai::{FuncRegistration, Module};
4use std::path::PathBuf;
5
6use regex::Regex;
7
8use crate::yolk::EvalMode;
9
10use super::sysinfo::{SystemInfo, SystemInfoPaths};
11
12macro_rules! if_canonical_return {
13 ($eval_mode:expr) => {
14 if $eval_mode == EvalMode::Canonical {
15 return Ok(Default::default());
16 }
17 };
18 ($eval_mode:expr, $value:expr) => {
19 if $eval_mode == EvalMode::Canonical {
20 return Ok($value);
21 }
22 };
23}
24
25type IStr = ImmutableString;
26type Ncc<'a> = NativeCallContext<'a>;
27
28pub fn global_stuff() -> Module {
29 let mut module = Module::new();
30
31 FuncRegistration::new("to_string")
32 .in_global_namespace()
33 .set_into_module(&mut module, |x: &mut SystemInfo| format!("{x:#?}"));
34 FuncRegistration::new("to_debug")
35 .in_global_namespace()
36 .set_into_module(&mut module, |x: &mut SystemInfo| format!("{x:?}"));
37 FuncRegistration::new("to_string")
38 .in_global_namespace()
39 .set_into_module(&mut module, |x: &mut SystemInfoPaths| format!("{x:#?}"));
40 FuncRegistration::new("to_debug")
41 .in_global_namespace()
42 .set_into_module(&mut module, |x: &mut SystemInfoPaths| format!("{x:?}"));
43 module
44}
45
46pub fn utils_module() -> Module {
47 let mut module = Module::new();
48 module.set_doc(indoc::indoc! {r"
49 # Utility functions
50
51 A collection of utility functions
52 "});
53
54 let regex_match = |pattern: String, haystack: String| -> Result<bool, Box<EvalAltResult>> {
55 Ok(create_regex(&pattern)?.is_match(&haystack))
56 };
57 FuncRegistration::new("regex_match")
58 .with_comments(["/// Check if a given string matches a given regex pattern."])
59 .with_params_info(["pattern: &str", "haystack: &str", "Result<bool>"])
60 .in_global_namespace()
61 .set_into_module(&mut module, regex_match);
62
63 let regex_replace = |pattern: String,
64 haystack: String,
65 replacement: String|
66 -> Result<String, Box<EvalAltResult>> {
67 Ok(create_regex(&pattern)?
68 .replace_all(&haystack, &*replacement)
69 .to_string())
70 };
71 FuncRegistration::new("regex_replace")
72 .with_comments(["/// Replace a regex pattern in a string with a replacement."])
73 .with_params_info([
74 "pattern: &str",
75 "haystack: &str",
76 "replacement: &str",
77 "Result<String>",
78 ])
79 .in_global_namespace()
80 .set_into_module(&mut module, regex_replace);
81
82 let regex_captures =
83 |pattern: String, s: String| -> Result<Option<Vec<String>>, Box<EvalAltResult>> {
84 Ok(create_regex(&pattern)?.captures(s.as_str()).map(|caps| {
85 (0..caps.len())
86 .map(|x| caps.get(x).unwrap().as_str().to_string())
87 .collect::<Vec<_>>()
88 }))
89 };
90 FuncRegistration::new("regex_captures")
91 .with_comments([
92 "/// Match a string against a regex pattern and return the capture groups as a list.",
93 ])
94 .with_params_info(["pattern: &str", "s: &str", "Result<Option<Vec<String>>>"])
95 .in_global_namespace()
96 .set_into_module(&mut module, regex_captures);
97
98 let rhai_color_hex_to_rgb = |hex_string: String| -> Result<Map, Box<EvalAltResult>> {
99 let (r, g, b, a) = color_hex_to_rgb(&hex_string)?;
100 let mut map = Map::new();
101 map.insert("r".to_string().into(), Dynamic::from_int(r as i64));
102 map.insert("g".to_string().into(), Dynamic::from_int(g as i64));
103 map.insert("b".to_string().into(), Dynamic::from_int(b as i64));
104 map.insert("a".to_string().into(), Dynamic::from_int(a as i64));
105 Ok(map)
106 };
107 FuncRegistration::new("color_hex_to_rgb")
108 .with_comments(["/// Convert a hex color string to an RGB map."])
109 .with_params_info(["hex_string: &str", "Result<Map>"])
110 .in_global_namespace()
111 .set_into_module(&mut module, rhai_color_hex_to_rgb);
112
113 FuncRegistration::new("color_hex_to_rgb")
114 .with_comments(["/// Convert a hex color string to an RGB map."])
115 .with_params_info(["hex_string: &str", "Result<Map>"])
116 .in_global_namespace()
117 .set_into_module(&mut module, rhai_color_hex_to_rgb);
118
119 let color_hex_to_rgb_str = |hex_string: String| -> Result<String, Box<EvalAltResult>> {
120 let (r, g, b, _) = color_hex_to_rgb(&hex_string)?;
121 Ok(format!("rgb({r}, {g}, {b})"))
122 };
123
124 FuncRegistration::new("color_hex_to_rgb_str")
125 .with_comments(["/// Convert a hex color string to an RGB string."])
126 .with_params_info(["hex_string: &str", "Result<String>"])
127 .in_global_namespace()
128 .set_into_module(&mut module, color_hex_to_rgb_str);
129
130 let color_hex_to_rgba_str = |hex_string: String| -> Result<String, Box<EvalAltResult>> {
131 let (r, g, b, a) = color_hex_to_rgb(&hex_string)?;
132 Ok(format!("rgba({r}, {g}, {b}, {a})"))
133 };
134 FuncRegistration::new("color_hex_to_rgba_str")
135 .with_comments(["/// Convert a hex color string to an RGBA string."])
136 .with_params_info(["hex_string: &str", "Result<String>"])
137 .in_global_namespace()
138 .set_into_module(&mut module, color_hex_to_rgba_str);
139
140 let color_rgb_to_hex = |rgb_table: Map| -> Result<String, Box<EvalAltResult>> {
141 let r = rgb_table
142 .get("r")
143 .map(dynamic_to_u8)
144 .transpose()?
145 .unwrap_or(0);
146 let g = rgb_table
147 .get("g")
148 .map(dynamic_to_u8)
149 .transpose()?
150 .unwrap_or(0);
151 let b = rgb_table
152 .get("b")
153 .map(dynamic_to_u8)
154 .transpose()?
155 .unwrap_or(0);
156 let a = rgb_table.get("a").map(dynamic_to_u8).transpose()?;
157 match a {
158 Some(a) => Ok(format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)),
159 None => Ok(format!("#{:02x}{:02x}{:02x}", r, g, b)),
160 }
161 };
162 FuncRegistration::new("color_rgb_to_hex")
163 .with_comments(["/// Convert an RGB map to a hex color string."])
164 .with_params_info(["rgb_table: Map", "Result<String>"])
165 .in_global_namespace()
166 .set_into_module(&mut module, color_rgb_to_hex);
167
168 module
169}
170
171pub fn io_module(eval_mode: EvalMode) -> Module {
172 use which::which_all_global;
173 let mut module = Module::new();
174 module.set_doc(indoc::indoc! {r"
175 # IO Functions
176
177 A collection of functions that can read the environment and filesystem.
178 These return standardized values in canonical mode.
179 "});
180
181 let command_available = move |name: IStr| -> RhaiFnResult<bool> {
182 if_canonical_return!(eval_mode, false);
183 Ok(match which_all_global(&*name) {
184 Ok(mut iter) => iter.next().is_some(),
185 Err(err) => {
186 tracing::warn!("Error checking if command is available: {}", err);
187 false
188 }
189 })
190 };
191 FuncRegistration::new("command_available")
192 .with_comments(["/// Check if a given command is available"])
193 .with_params_info(["name: &str", "Result<bool>"])
194 .set_into_module(&mut module, command_available);
195
196 let env = move |name: IStr, def: IStr| -> RhaiFnResult<IStr> {
197 if_canonical_return!(eval_mode, def.clone());
198 Ok(std::env::var(&*name).map(|x| x.into()).unwrap_or(def))
199 };
200 FuncRegistration::new("env")
201 .with_comments(["/// Read an environment variable, or return the given default"])
202 .with_params_info(["name: &str", "def: &str", "Result<String>"])
203 .set_into_module(&mut module, env);
204
205 let path_exists = move |p: IStr| -> RhaiFnResult<bool> {
206 if_canonical_return!(eval_mode, false);
207 Ok(PathBuf::from(&*p).exists())
208 };
209 FuncRegistration::new("path_exists")
210 .with_comments(["/// Check if something exists at a given path"])
211 .with_params_info(["p: &str", "Result<bool>"])
212 .set_into_module(&mut module, path_exists);
213
214 let path_is_dir = move |p: String| -> RhaiFnResult<bool> {
215 if_canonical_return!(eval_mode, false);
216 Ok(fs_err::metadata(p).map(|m| m.is_dir()).unwrap_or(false))
217 };
218 FuncRegistration::new("path_is_dir")
219 .with_comments(["/// Check if the given path is a directory"])
220 .with_params_info(["p: &str", "Result<bool>"])
221 .set_into_module(&mut module, path_is_dir);
222
223 let path_is_file = move |p: String| -> RhaiFnResult<bool> {
224 if_canonical_return!(eval_mode, false);
225 Ok(fs_err::metadata(p).map(|m| m.is_file()).unwrap_or(false))
226 };
227 FuncRegistration::new("path_is_file")
228 .with_comments(["/// Check if the given path is a file"])
229 .with_params_info(["p: &str", "Result<bool>"])
230 .set_into_module(&mut module, path_is_file);
231
232 let read_file = move |p: String| -> RhaiFnResult<String> {
233 if_canonical_return!(eval_mode, String::new());
234 Ok(fs_err::read_to_string(p).unwrap_or_default())
235 };
236 FuncRegistration::new("read_file")
237 .with_comments(["/// Read the contents of a given file"])
238 .with_params_info(["p: &str", "Result<String>"])
239 .set_into_module(&mut module, read_file);
240
241 let read_dir = move |p: String| -> RhaiFnResult<Vec<String>> {
242 if_canonical_return!(eval_mode, vec![]);
243 fs_err::read_dir(p)
244 .into_diagnostic()
245 .map_err(|e| e.to_string())?
246 .map(|x| {
247 Ok(x.map_err(|e| e.to_string())?
248 .path()
249 .to_string_lossy()
250 .to_string())
251 })
252 .collect()
253 };
254 FuncRegistration::new("read_dir")
255 .with_comments(["/// Read the children of a given dir"])
256 .with_params_info(["p: &str", "Result<Vec<String>>"])
257 .set_into_module(&mut module, read_dir);
258
259 module
260}
261
262pub fn tag_module() -> Module {
263 use indoc::indoc;
264 let mut module = rhai::Module::new();
265 module.set_doc(indoc::indoc! {r"
266 # Template tag functions
267
268 Yolk template tags simply execute rhai functions that transform the block of text the tag operates on.
269
270 Quick reminder: Yolk has three different types of tags, that differ only in what text they operate on:
271
272 - Next-line tags (`{# ... #}`): These tags operate on the line following the tag.
273 - Inline tags (`{< ... >}`): These tags operate on everything before the tag within the same line.
274 - Block tags (`{% ... %} ... {% end %}`): These tags operate on everything between the tag and the corresponding `{% end %}` tag.
275
276 Inside these tags, you can call any of Yolks template tag functions (Or, in fact, any rhai expression that returns a string).
277 "});
278
279 fn tag_text_replace(text: &str, pattern: &str, replacement: &str) -> RhaiFnResult<String> {
280 let pattern = create_regex(pattern)?;
281 let after_replace = pattern.replace(text, replacement);
282 if let Some(original_value) = pattern.find(text) {
283 let original_value = original_value.as_str();
284 let reversed = pattern.replace(&after_replace, original_value);
285 if reversed != text {
286 return Err(format!(
287 "Refusing to run non-reversible replacement: {text} -> {after_replace}",
288 )
289 .into());
290 }
291 };
292 Ok(after_replace.to_string())
293 }
294
295 let f = |ctx: Ncc, regex: IStr, replacement: IStr| -> RhaiFnResult<_> {
296 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
297 tag_text_replace(&text, ®ex, &replacement)
298 };
299 FuncRegistration::new("replace_re")
300 .with_comments([indoc! {"
301 /// **shorthand**: `rr`.
302 ///
303 /// Replaces all occurrences of a Regex `pattern` with `replacement` in the text.
304 ///
305 /// #### Example
306 ///
307 /// ```handlebars
308 /// ui_font = \"Arial\" # {< replace_re(`\".*\"`, `\"{data.font.ui}\"`) >}
309 /// ```
310 ///
311 /// Note that the replacement value needs to contain the quotes, as those are also matched against in the regex pattern.
312 /// Otherwise, we would end up with invalid toml.
313 "}])
314 .with_params_info(["regex: &str", "replacement: &str", "Result<String>"])
315 .with_namespace(rhai::FnNamespace::Global)
316 .set_into_module(&mut module, f);
317 FuncRegistration::new("rr")
318 .in_global_namespace()
319 .set_into_module(&mut module, f);
320
321 let f = |ctx: Ncc, between: IStr, replacement: IStr| -> RhaiFnResult<_> {
322 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
323 let regex = format!("{between}[^{between}]*{between}");
324 tag_text_replace(&text, ®ex, &format!("{between}{replacement}{between}"))
325 };
326 FuncRegistration::new("replace_in")
327 .with_comments([indoc! {"
328 /// **shorthand**: `rin`.
329 ///
330 /// Replaces the text between two delimiters with the `replacement`.
331 ///
332 /// #### Example
333 ///
334 /// ```toml
335 /// ui_font = \"Arial\" # {< replace_in(`\"`, data.font.ui) >}
336 /// ```
337 ///
338 /// Note: we don't need to include the quotes in the replacement here.
339 "}])
340 .with_params_info(["between: &str", "replacement: &str", "Result<String>"])
341 .in_global_namespace()
342 .set_into_module(&mut module, f);
343 FuncRegistration::new("rin")
344 .in_global_namespace()
345 .set_into_module(&mut module, f);
346
347 let f = |ctx: Ncc, left: IStr, right: IStr, replacement: IStr| -> RhaiFnResult<_> {
348 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
349 let regex = format!("{left}[^{right}]*{right}");
350 tag_text_replace(&text, ®ex, &format!("{left}{replacement}{right}"))
351 };
352 FuncRegistration::new("replace_between")
353 .with_comments([indoc! {"
354 /// **shorthand**: `rbet`.
355 ///
356 /// Replaces the text between two delimiters with the `replacement`.
357 ///
358 /// #### Example
359 ///
360 /// ```handlebars
361 /// ui_font = (Arial) # {< replace_between(`(`, `)`, data.font.ui) >}
362 /// ```
363 ///
364 /// Note: we don't need to include the quotes in the replacement here.
365 "}])
366 .with_params_info([
367 "left: &str",
368 "right: &str",
369 "replacement: &str",
370 "Result<String>",
371 ])
372 .in_global_namespace()
373 .set_into_module(&mut module, f);
374 FuncRegistration::new("rbet")
375 .in_global_namespace()
376 .set_into_module(&mut module, f);
377
378 let f = |ctx: Ncc, replacement: IStr| -> RhaiFnResult<_> {
379 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
380 tag_text_replace(
381 &text,
382 r"#[\da-fA-F]{6}([\da-fA-F]{2})?",
383 replacement.as_ref(),
384 )
385 };
386 FuncRegistration::new("replace_color")
387 .with_comments([indoc! {"
388 /// **shorthand**: `rcol`.
389 ///
390 /// Replaces a hex color value with a new hex color.
391 ///
392 /// #### Example
393 ///
394 /// ```handlebars
395 /// background_color = \"#282828\" # {< replace_color(data.colors.bg) >}
396 /// ```
397 "}])
398 .with_params_info(["replacement: &str", "Result<String>"])
399 .in_global_namespace()
400 .set_into_module(&mut module, f);
401 FuncRegistration::new("rcol")
402 .in_global_namespace()
403 .set_into_module(&mut module, f);
404
405 let f = |ctx: Ncc, replacement: Dynamic| -> RhaiFnResult<_> {
406 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
407 tag_text_replace(&text, r"-?\d+(?:\.\d+)?", &replacement.to_string())
408 };
409 FuncRegistration::new("replace_number")
410 .with_comments([indoc! {"
411 /// **shorthand**: `rnum`.
412 ///
413 /// Replaces a number with another number.
414 ///
415 /// #### Example
416 ///
417 /// ```handlebars
418 /// cursor_size = 32 # {< replace_number(data.cursor_size) >}
419 /// ```
420 "}])
421 .with_params_info(["replacement: Dynamic", "Result<String>"])
422 .in_global_namespace()
423 .set_into_module(&mut module, f);
424 FuncRegistration::new("rnum")
425 .in_global_namespace()
426 .set_into_module(&mut module, f);
427
428 let f = |ctx: Ncc, replacement: IStr| -> RhaiFnResult<_> {
429 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
430 let mut result = tag_text_replace(&text, r#"".*""#, &format!("\"{replacement}\""))?;
431 if result == text {
432 result = tag_text_replace(&text, r#"`.*`"#, &format!("`{replacement}`"))?;
433 }
434 if result == text {
435 result = tag_text_replace(&text, r#"'.*'"#, &format!("'{replacement}'"))?;
436 }
437 Ok(result)
438 };
439 FuncRegistration::new("replace_quoted")
440 .with_comments([indoc! {"
441 /// **shorthand**: `rq`.
442 ///
443 /// Replaces a value between quotes with another value
444 ///
445 /// #### Example
446 ///
447 /// ```handlebars
448 /// ui_font = \"Arial\" # {< replace_quoted(data.font.ui) >}
449 /// ```
450 "}])
451 .with_params_info(["replacement: &str", "Result<String>"])
452 .in_global_namespace()
453 .set_into_module(&mut module, f);
454 FuncRegistration::new("rq")
455 .in_global_namespace()
456 .set_into_module(&mut module, f);
457
458 let f = |ctx: Ncc, replacement: IStr| -> RhaiFnResult<_> {
459 let text: IStr = ctx.call_fn("get_yolk_text", ())?;
460 let regex = create_regex(r"([=:])( *)([^\s]+)").unwrap();
461
462 if let Some(caps) = regex.captures(&text) {
463 let full_match = caps.get(0).unwrap();
464 let equals = caps.get(1).unwrap();
465 let space = caps.get(2).unwrap();
466 let new_value = regex.replace(
467 &text,
468 format!("{}{}{}", equals.as_str(), space.as_str(), replacement),
469 );
470 if regex.replace(&new_value, full_match.as_str()) == *text {
471 Ok(new_value.to_string())
472 } else {
473 Err(
474 format!("Refusing to run non-reversible replacement: {text} -> {new_value}",)
475 .into(),
476 )
477 }
478 } else {
479 Ok(text.into())
480 }
481 };
482 FuncRegistration::new("replace_value")
483 .with_comments([indoc! {"
484 /// **shorthand**: `rv`.
485 ///
486 /// Replaces a value (without spaces) after a `:` or a `=` with another value
487 ///
488 /// #### Example
489 ///
490 /// ```handlebars
491 /// ui_font = Arial # {< replace_value(data.font.ui) >}
492 /// ```
493 "}])
494 .with_params_info(["replacement: &str", "Result<String>"])
495 .in_global_namespace()
496 .set_into_module(&mut module, f);
497 FuncRegistration::new("rv")
498 .in_global_namespace()
499 .set_into_module(&mut module, f);
500
501 module
502}
503
504fn dynamic_to_u8(x: &Dynamic) -> RhaiFnResult<u8> {
505 let int = x
506 .as_int()
507 .map_err(|actual| format!("Failed to convert {actual} to int"))?;
508 let int = int
509 .try_into()
510 .map_err(|_| format!("Failed to convert {int} to u8"))?;
511 Ok(int)
512}
513
514fn color_hex_to_rgb(hex_string: &str) -> Result<(u8, u8, u8, u8), Box<EvalAltResult>> {
515 let hex = hex_string.trim_start_matches('#');
516 if hex.len() != 6 && hex.len() != 8 {
517 return Err(format!("Invalid hex color: {}", hex_string).into());
518 }
519 let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?;
520 let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?;
521 let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?;
522 let a = if hex.len() == 8 {
523 u8::from_str_radix(&hex[6..8], 16).map_err(|e| e.to_string())?
524 } else {
525 255
526 };
527 Ok((r, g, b, a))
528}
529
530type RhaiFnResult<T> = Result<T, Box<EvalAltResult>>;
531
532fn create_regex(s: &str) -> RhaiFnResult<Regex> {
533 Ok(crate::util::create_regex(s).map_err(|e| e.to_string())?)
534}
535
536#[cfg(not(feature = "docgen"))]
537#[extend::ext]
538impl FuncRegistration {
539 fn with_comments(self, _: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
540 self
541 }
542 fn with_params_info(self, _: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
543 self
544 }
545}
546#[cfg(not(feature = "docgen"))]
547#[extend::ext]
548impl Module {
549 fn set_doc(&mut self, _: &str) {}
550}
551
552#[cfg(test)]
553mod test {
554 use crate::util::test_util::TestResult;
555 use miette::IntoDiagnostic as _;
556 use rhai::Variant;
557
558 use crate::{script::eval_ctx::EvalCtx, yolk::EvalMode};
559
560 pub fn run_expr<T: Variant + Clone>(code: &str) -> miette::Result<T> {
561 let mut eval_ctx = EvalCtx::new_in_mode(EvalMode::Local)?;
562 Ok(eval_ctx.eval_rhai::<T>(code)?)
563 }
564 pub fn run_tag_expr(text: &str, code: &str) -> miette::Result<String> {
565 let text = text.to_string();
566 let mut eval_ctx = EvalCtx::new_in_mode(EvalMode::Local)?;
567 eval_ctx
568 .engine_mut()
569 .register_fn("get_yolk_text", move || text.clone());
570 eval_ctx.eval_rhai::<String>(code).into_diagnostic()
571 }
572
573 use rstest::rstest;
574
575 #[rstest]
576 #[case::match_found(Some(vec![
577 "<aaaXb>".to_string(),
578 "aaa".to_string(),
579 "b".to_string()
580 ]), "regex_captures(`<(.*)X(.)>`, `foo <aaaXb> bar`)")]
581 #[case::no_match(None, "regex_captures(`<(.*)X(.)>`, `asdf`)")]
582 pub fn test_regex_captures(
583 #[case] expected: Option<Vec<String>>,
584 #[case] expr: &str,
585 ) -> TestResult {
586 assert_eq!(expected, run_expr::<Option<Vec<String>>>(expr)?);
587 Ok(())
588 }
589
590 #[rstest]
591 #[case::replace("foo:'aaa'", "replace_re(`'.*'`, `'xxx'`)", "foo:'xxx'")]
592 #[case::non_reversible("foo:'aaa'", "replace_re(`'.*'`, `xxx`)", "foo:'aaa'")]
593 pub fn test_replace(
594 #[case] input: &str,
595 #[case] expr: &str,
596 #[case] expected: &str,
597 ) -> TestResult {
598 if expected == input {
599 assert!(
600 run_tag_expr(input, expr).is_err(),
601 "replace performed non-reversible replacement",
602 );
603 } else {
604 assert_eq!(expected, run_tag_expr(input, expr)?);
605 }
606 Ok(())
607 }
608
609 #[rstest]
610 #[case::replace("foo:'aaa'", "replace_in(`'`, `xxx`)", "foo:'xxx'")]
611 #[case::non_reversible("foo:'aaa'", "replace_in(`'`, `x'xx`)", "foo:'aaa'")]
612 pub fn test_replace_in(
613 #[case] input: &str,
614 #[case] expr: &str,
615 #[case] expected: &str,
616 ) -> TestResult {
617 if expected == input {
618 assert!(
619 run_tag_expr(input, expr).is_err(),
620 "replace performed non-reversible replacement",
621 );
622 } else {
623 assert_eq!(expected, run_tag_expr(input, expr)?);
624 }
625 Ok(())
626 }
627
628 #[rstest]
629 #[case::replace("foo: #ff0000", "replace_color(`#00ff00`)", "foo: #00ff00")]
630 #[case::replace_alpha("foo: #ff0000", "replace_color(`#00ff0000`)", "foo: #00ff0000")]
631 #[case::non_reversible_no_hash("foo: #ff0000", "replace_color(`00ff00`)", "foo: #ff0000")]
632 #[case::non_reversible_bad_color("foo: #ff0000", "replace_color(`bad color`)", "foo: #ff0000")]
633 pub fn test_replace_color(
634 #[case] input: &str,
635 #[case] expr: &str,
636 #[case] expected: &str,
637 ) -> TestResult {
638 if expected == input {
639 assert!(
640 run_tag_expr(input, expr).is_err(),
641 "replace_color performed non-reversible replacement",
642 );
643 } else {
644 assert_eq!(expected, run_tag_expr(input, expr)?);
645 }
646 Ok(())
647 }
648
649 #[rstest]
650 #[case::single_quote("foo: 'old'", "replace_quoted(`new`)", "foo: 'new'")]
651 #[case::double_quote("foo: \"old\"", "replace_quoted(`new`)", "foo: \"new\"")]
652 #[case::backtick("foo: `old`", "replace_quoted(`new`)", "foo: `new`")]
653 pub fn test_replace_quoted(
654 #[case] input: &str,
655 #[case] expr: &str,
656 #[case] expected: &str,
657 ) -> TestResult {
658 assert_eq!(expected, run_tag_expr(input, expr)?);
659 Ok(())
660 }
661
662 #[rstest]
663 #[case::replace("foo: bar # baz", "replace_value(`xxx`)", "foo: xxx # baz")]
664 #[case::non_reversible("foo: bar # baz", "replace_value(`x xx`)", "foo: bar # baz")]
665 pub fn test_replace_value(
666 #[case] input: &str,
667 #[case] expr: &str,
668 #[case] expected: &str,
669 ) -> TestResult {
670 if expected == input {
671 assert!(
672 run_tag_expr(input, expr).is_err(),
673 "replace_value performed non-reversible replacement",
674 );
675 } else {
676 assert_eq!(expected, run_tag_expr(input, expr)?);
677 }
678 Ok(())
679 }
680
681 #[rstest]
682 #[case::integer("foo 123 bar", "replace_number(999)", "foo 999 bar")]
683 #[case::float("foo 1.23 bar", "replace_number(99.9)", "foo 99.9 bar")]
684 #[case::non_reversible("foo 99.9 bar", "replace_number(`hi`)", "foo 99.9 bar")]
685 pub fn test_replace_number(
686 #[case] input: &str,
687 #[case] expr: &str,
688 #[case] expected: &str,
689 ) -> TestResult {
690 if expected == input {
691 assert!(
692 run_tag_expr(input, expr).is_err(),
693 "replace_number performed non-reversible replacement",
694 );
695 } else {
696 assert_eq!(expected, run_tag_expr(input, expr)?);
697 }
698 Ok(())
699 }
700}