Skip to main content

voro_core/
template.rs

1//! The one substitution routine (DESIGN.md §8). Every template Voro fills —
2//! agent command lines, dispatch preambles, planning and refine prompts — goes
3//! through [`render`], because chained `String::replace` calls re-scan values
4//! they have already emitted: whichever untrusted value goes in last, the ones
5//! before it were searched for the placeholders that came after. A task body
6//! discussing `{task_id}` is then silently rewritten before the agent sees it.
7//!
8//! Shell quoting lives here too, so command assembly owns it rather than
9//! borrowing it from the TUI crate.
10
11use std::path::Path;
12
13/// Fill `template` from `bindings` in a single left-to-right pass: each bound
14/// placeholder's value is emitted verbatim and never re-scanned, so a value
15/// containing another placeholder survives whatever order the bindings are
16/// given in. An unrecognised `{…}` is copied through untouched, which is what
17/// makes composing nested blocks safe — render the inner block first, then bind
18/// the finished text.
19pub 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        // Longest match wins, so one placeholder that prefixes another cannot
26        // shadow it whichever order the bindings arrive in.
27        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
46/// Single-quote a path for safe substitution into an `sh -c` command line.
47pub 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    /// The defect this routine exists for: a value that itself contains a
67    /// placeholder reaches the output as written, whichever order the bindings
68    /// are given in. Chained `String::replace` cannot promise that.
69    #[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}