1use std::path::Path;
12
13pub fn render(template: &str, bindings: &[(&str, &str)]) -> String {
20 let mut out = String::with_capacity(template.len());
21 let mut rest = template;
22 while let Some(open) = rest.find('{') {
23 out.push_str(&rest[..open]);
24 rest = &rest[open..];
25 let matched = bindings
28 .iter()
29 .filter(|(name, _)| rest.starts_with(name))
30 .max_by_key(|(name, _)| name.len());
31 match matched {
32 Some((name, value)) => {
33 out.push_str(value);
34 rest = &rest[name.len()..];
35 }
36 None => {
37 out.push('{');
38 rest = &rest[1..];
39 }
40 }
41 }
42 out.push_str(rest);
43 out
44}
45
46pub fn shell_quote(path: &Path) -> String {
48 format!("'{}'", path.to_string_lossy().replace('\'', "'\\''"))
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn bound_placeholders_are_filled_and_unknown_ones_copied_through() {
57 assert_eq!(
58 render("run {a} then {b} and {c}", &[("{a}", "1"), ("{b}", "2")]),
59 "run 1 then 2 and {c}"
60 );
61 assert_eq!(render("nothing to do", &[("{a}", "1")]), "nothing to do");
62 assert_eq!(render("{a}{a}{a}", &[("{a}", "x")]), "xxx");
63 assert_eq!(render("a { b } c", &[("{b}", "x")]), "a { b } c");
64 }
65
66 #[test]
70 fn a_value_containing_another_placeholder_is_emitted_verbatim() {
71 let body = "the note says {task_id} is wrong, and {db} too";
72 for bindings in [
73 vec![("{seed}", body), ("{task_id}", "42"), ("{db}", " --db x")],
74 vec![("{task_id}", "42"), ("{db}", " --db x"), ("{seed}", body)],
75 ] {
76 assert_eq!(
77 render("task {task_id}:\n{seed}\n{db}", &bindings),
78 format!("task 42:\n{body}\n --db x")
79 );
80 }
81 }
82
83 #[test]
84 fn the_longest_matching_placeholder_wins() {
85 for bindings in [
86 vec![("{project}", "p"), ("{project_arg}", "'p'")],
87 vec![("{project_arg}", "'p'"), ("{project}", "p")],
88 ] {
89 assert_eq!(render("{project} {project_arg}", &bindings), "p 'p'");
90 }
91 }
92
93 #[test]
94 fn unicode_before_an_unbound_brace_is_not_split() {
95 assert_eq!(render("— {x} — {y}", &[("{x}", "ok")]), "— ok — {y}");
96 }
97
98 #[test]
99 fn shell_quote_wraps_and_escapes() {
100 assert_eq!(shell_quote(Path::new("/tmp/a b")), "'/tmp/a b'");
101 assert_eq!(shell_quote(Path::new("/tmp/it's")), "'/tmp/it'\\''s'");
102 }
103}