saorsa_agent/templates/
engine.rs1use crate::error::{Result, SaorsaAgentError};
4use std::collections::HashMap;
5
6pub type TemplateContext = HashMap<String, String>;
8
9#[derive(Debug, Default)]
11pub struct TemplateEngine;
12
13impl TemplateEngine {
14 pub fn new() -> Self {
16 Self
17 }
18
19 pub fn render(&self, template: &str, context: &TemplateContext) -> Result<String> {
26 let mut result = String::new();
27 let mut chars = template.chars().peekable();
28
29 while let Some(ch) = chars.next() {
30 if ch == '{' && chars.peek() == Some(&'{') {
31 chars.next(); let tag = self.collect_until(&mut chars, '}', '}')?;
33 let rendered = self.render_tag(&tag, context, template)?;
34 result.push_str(&rendered);
35 } else {
36 result.push(ch);
37 }
38 }
39
40 Ok(result)
41 }
42
43 fn collect_until(
45 &self,
46 chars: &mut std::iter::Peekable<std::str::Chars>,
47 delim1: char,
48 delim2: char,
49 ) -> Result<String> {
50 let mut content = String::new();
51 while let Some(ch) = chars.next() {
52 if ch == delim1 && chars.peek() == Some(&delim2) {
53 chars.next(); return Ok(content);
55 }
56 content.push(ch);
57 }
58 Err(SaorsaAgentError::Context(
59 "Unclosed template tag".to_string(),
60 ))
61 }
62
63 fn render_tag(
65 &self,
66 tag: &str,
67 context: &TemplateContext,
68 full_template: &str,
69 ) -> Result<String> {
70 let tag = tag.trim();
71
72 if let Some(var_name) = tag.strip_prefix("#if ") {
73 self.render_if(var_name.trim(), context, full_template)
74 } else if let Some(var_name) = tag.strip_prefix("#unless ") {
75 self.render_unless(var_name.trim(), context, full_template)
76 } else if tag == "/if" || tag == "/unless" {
77 Ok(String::new())
79 } else {
80 self.render_variable(tag, context)
82 }
83 }
84
85 fn render_variable(&self, var_name: &str, context: &TemplateContext) -> Result<String> {
87 context.get(var_name).cloned().ok_or_else(|| {
88 SaorsaAgentError::Context(format!("Missing template variable: {}", var_name))
89 })
90 }
91
92 fn render_if(
96 &self,
97 _var_name: &str,
98 _context: &TemplateContext,
99 _full_template: &str,
100 ) -> Result<String> {
101 Ok(String::new())
102 }
103
104 fn render_unless(
108 &self,
109 _var_name: &str,
110 _context: &TemplateContext,
111 _full_template: &str,
112 ) -> Result<String> {
113 Ok(String::new())
114 }
115}
116
117pub fn render_simple(template: &str, context: &TemplateContext) -> Result<String> {
122 let mut result = template.to_string();
123
124 for (key, value) in context {
126 let placeholder = format!("{{{{{}}}}}", key);
127 result = result.replace(&placeholder, value);
128 }
129
130 let if_pattern_start = "{{#if ";
133 let if_pattern_end = "{{/if}}";
134
135 while let Some(start_pos) = result.find(if_pattern_start) {
136 if let Some(content_start) = result[start_pos..].find("}}") {
137 let var_end = start_pos + if_pattern_start.len();
138 let var_name = &result[var_end..start_pos + content_start];
139
140 if let Some(end_pos) = result[start_pos..].find(if_pattern_end) {
141 let full_start = start_pos;
142 let full_end = start_pos + end_pos + if_pattern_end.len();
143 let content = &result[start_pos + content_start + 2..start_pos + end_pos];
144
145 let replacement = if context.contains_key(var_name.trim())
146 && !context.get(var_name.trim()).is_some_and(|v| v.is_empty())
147 {
148 content.to_string()
149 } else {
150 String::new()
151 };
152
153 result.replace_range(full_start..full_end, &replacement);
154 } else {
155 break;
156 }
157 } else {
158 break;
159 }
160 }
161
162 let unless_pattern_start = "{{#unless ";
164 let unless_pattern_end = "{{/unless}}";
165
166 while let Some(start_pos) = result.find(unless_pattern_start) {
167 if let Some(content_start) = result[start_pos..].find("}}") {
168 let var_end = start_pos + unless_pattern_start.len();
169 let var_name = &result[var_end..start_pos + content_start];
170
171 if let Some(end_pos) = result[start_pos..].find(unless_pattern_end) {
172 let full_start = start_pos;
173 let full_end = start_pos + end_pos + unless_pattern_end.len();
174 let content = &result[start_pos + content_start + 2..start_pos + end_pos];
175
176 let replacement = if !context.contains_key(var_name.trim())
177 || context.get(var_name.trim()).is_some_and(|v| v.is_empty())
178 {
179 content.to_string()
180 } else {
181 String::new()
182 };
183
184 result.replace_range(full_start..full_end, &replacement);
185 } else {
186 break;
187 }
188 } else {
189 break;
190 }
191 }
192
193 Ok(result)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_variable_substitution() {
202 let template = "Hello {{name}}!";
203 let mut context = TemplateContext::new();
204 context.insert("name".to_string(), "World".to_string());
205
206 let result = render_simple(template, &context);
207 assert!(result.is_ok());
208
209 match result {
210 Ok(r) => assert_eq!(r, "Hello World!"),
211 Err(_) => unreachable!("Should render successfully"),
212 }
213 }
214
215 #[test]
216 fn test_multiple_variables() {
217 let template = "{{greeting}} {{name}}!";
218 let mut context = TemplateContext::new();
219 context.insert("greeting".to_string(), "Hi".to_string());
220 context.insert("name".to_string(), "Alice".to_string());
221
222 let result = render_simple(template, &context);
223 assert!(result.is_ok());
224
225 match result {
226 Ok(r) => assert_eq!(r, "Hi Alice!"),
227 Err(_) => unreachable!("Should render successfully"),
228 }
229 }
230
231 #[test]
232 fn test_if_conditional_true() {
233 let template = "Start {{#if show}}visible{{/if}} end";
234 let mut context = TemplateContext::new();
235 context.insert("show".to_string(), "yes".to_string());
236
237 let result = render_simple(template, &context);
238 assert!(result.is_ok());
239
240 match result {
241 Ok(r) => assert_eq!(r, "Start visible end"),
242 Err(_) => unreachable!("Should render successfully"),
243 }
244 }
245
246 #[test]
247 fn test_if_conditional_false() {
248 let template = "Start {{#if show}}hidden{{/if}} end";
249 let context = TemplateContext::new();
250
251 let result = render_simple(template, &context);
252 assert!(result.is_ok());
253
254 match result {
255 Ok(r) => assert_eq!(r, "Start end"),
256 Err(_) => unreachable!("Should render successfully"),
257 }
258 }
259
260 #[test]
261 fn test_unless_conditional_true() {
262 let template = "Start {{#unless hide}}visible{{/unless}} end";
263 let context = TemplateContext::new();
264
265 let result = render_simple(template, &context);
266 assert!(result.is_ok());
267
268 match result {
269 Ok(r) => assert_eq!(r, "Start visible end"),
270 Err(_) => unreachable!("Should render successfully"),
271 }
272 }
273
274 #[test]
275 fn test_unless_conditional_false() {
276 let template = "Start {{#unless hide}}hidden{{/unless}} end";
277 let mut context = TemplateContext::new();
278 context.insert("hide".to_string(), "yes".to_string());
279
280 let result = render_simple(template, &context);
281 assert!(result.is_ok());
282
283 match result {
284 Ok(r) => assert_eq!(r, "Start end"),
285 Err(_) => unreachable!("Should render successfully"),
286 }
287 }
288
289 #[test]
290 fn test_empty_context() {
291 let template = "No variables here";
292 let context = TemplateContext::new();
293
294 let result = render_simple(template, &context);
295 assert!(result.is_ok());
296
297 match result {
298 Ok(r) => assert_eq!(r, "No variables here"),
299 Err(_) => unreachable!("Should render successfully"),
300 }
301 }
302
303 #[test]
304 fn test_template_engine_new() {
305 let engine = TemplateEngine::new();
306 let template = "Hello {{name}}";
307 let mut context = TemplateContext::new();
308 context.insert("name".to_string(), "Test".to_string());
309
310 let result = engine.render(template, &context);
311 assert!(result.is_ok());
312 }
313}