1#[derive(Debug, Clone, PartialEq, Eq)]
6enum Part {
7 Text(String),
9 Placeholder(String),
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
25pub struct Template(Vec<Part>);
26
27impl Template {
28 pub fn parse(src: &str) -> Result<Self, String> {
33 let mut parts = Vec::new();
34 let mut text = String::new();
35 let mut chars = src.chars().peekable();
36 while let Some(c) = chars.next() {
37 match c {
38 '{' => match chars.peek() {
39 Some('{') => {
40 chars.next();
41 text.push('{');
42 }
43 Some(&next) if next == '}' || next.is_ascii_alphanumeric() || next == '_' => {
44 let mut name = String::new();
45 loop {
46 match chars.peek() {
47 Some('}') => {
48 chars.next();
49 break;
50 }
51 Some(&n)
54 if name.is_empty()
55 && (n.is_ascii_alphanumeric() || n == '_') =>
56 {
57 name.push(n);
58 chars.next();
59 }
60 Some(&n) if !name.is_empty() && n.is_ascii_alphanumeric() => {
61 name.push(n);
62 chars.next();
63 }
64 _ => break,
65 }
66 }
67 if name.is_empty() {
68 return Err(
69 "expected a placeholder name after `{` (e.g. `{match}`), or \
70 `{{` for a literal brace"
71 .into(),
72 );
73 }
74 if !text.is_empty() {
75 parts.push(Part::Text(std::mem::take(&mut text)));
76 }
77 parts.push(Part::Placeholder(name));
78 }
79 Some(_) => {
80 return Err(
81 "expected a placeholder name after `{` (e.g. `{match}`), or \
82 `{{` for a literal brace"
83 .into(),
84 )
85 }
86 None => return Err("unclosed `{` at the end of the template".into()),
87 },
88 '}' => {
89 if chars.peek() == Some(&'}') {
90 chars.next();
91 }
92 text.push('}');
93 }
94 _ => text.push(c),
95 }
96 }
97 if !text.is_empty() {
98 parts.push(Part::Text(text));
99 }
100 Ok(Self(parts))
101 }
102
103 pub fn validate(&self, regex: ®ex::Regex) -> Result<(), String> {
107 for part in &self.0 {
108 if let Part::Placeholder(name) = part {
109 if name == "match"
110 || regex.capture_names().flatten().any(|n| n == name)
111 || name
112 .parse::<usize>()
113 .is_ok_and(|index| index >= 1 && index < regex.captures_len())
114 {
115 continue;
116 }
117 return Err(format!(
118 "placeholder `{{{name}}}` does not name a capture group of the rule's \
119 pattern (known groups: {})",
120 known_groups(regex),
121 ));
122 }
123 }
124 Ok(())
125 }
126
127 pub fn render(&self, caps: ®ex::Captures<'_>) -> String {
129 let mut out = String::new();
130 for part in &self.0 {
131 match part {
132 Part::Text(t) => out.push_str(t),
133 Part::Placeholder(name) => {
134 let replacement = if name == "match" {
135 caps.get(0).map(|m| m.as_str())
136 } else {
137 caps.name(name).map(|m| m.as_str())
138 };
139 if let Some(replacement) = replacement {
140 out.push_str(replacement);
141 }
142 }
143 }
144 }
145 out
146 }
147
148 pub fn render_with(&self, match_text: &str, captures: &[(String, String)]) -> String {
152 let mut out = String::new();
153 for part in &self.0 {
154 match part {
155 Part::Text(t) => out.push_str(t),
156 Part::Placeholder(name) => {
157 if name == "match" {
158 out.push_str(match_text);
159 continue;
160 }
161 if let Some((_, value)) =
162 captures.iter().find(|(key, _)| key == name)
163 {
164 out.push_str(value);
165 }
166 }
167 }
168 }
169 out
170 }
171}
172
173fn known_groups(regex: ®ex::Regex) -> String {
174 let names: Vec<&str> = regex.capture_names().flatten().collect();
175 if names.is_empty() {
176 "(none — name a group with `(?<name>...)`)".to_string()
177 } else {
178 names.join(", ")
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn render(src: &str, pattern: &str, haystack: &str) -> String {
187 let template = Template::parse(src).unwrap();
188 let regex = regex::Regex::new(pattern).unwrap();
189 template.validate(®ex).unwrap();
190 template
191 .render(®ex.captures(haystack).expect("must match"))
192 }
193
194 #[test]
195 fn literal_passes_through() {
196 assert_eq!(render("no placeholders here", "x", "x"), "no placeholders here");
197 }
198
199 #[test]
200 fn match_placeholder() {
201 assert_eq!(render("found '{match}'", "o+", "foo"), "found 'oo'");
202 }
203
204 #[test]
205 fn named_group_placeholder() {
206 assert_eq!(
207 render("got {word}!", "(?<word>\\w+)", "hi"),
208 "got hi!"
209 );
210 }
211
212 #[test]
213 fn brace_escapes() {
214 assert_eq!(render("{{literal}} {match}", "x", "x"), "{literal} x");
215 }
216
217 #[test]
218 fn lone_closing_brace_is_literal() {
219 assert_eq!(render("a } b", "x", "x"), "a } b");
220 }
221
222 #[test]
223 fn unknown_placeholder_rejected() {
224 let template = Template::parse("{nope}").unwrap();
225 let regex = regex::Regex::new("x").unwrap();
226 assert!(template.validate(®ex).is_err());
227 }
228
229 #[test]
230 fn malformed_placeholders_rejected() {
231 for src in ["{", "{ bad}", "o{"] {
232 assert!(Template::parse(src).is_err(), "{src:?} must not parse");
233 }
234 }
235}