standout_render/template/
engine.rs1use minijinja::{Environment, Value};
8
9use std::collections::HashMap;
10
11use crate::error::RenderError;
12
13pub trait TemplateEngine: Send + Sync {
24 fn render_template(
30 &self,
31 template: &str,
32 data: &serde_json::Value,
33 ) -> Result<String, RenderError>;
34
35 fn add_template(&mut self, name: &str, source: &str) -> Result<(), RenderError>;
39
40 fn render_named(&self, name: &str, data: &serde_json::Value) -> Result<String, RenderError>;
44
45 fn has_template(&self, name: &str) -> bool;
47
48 fn render_with_context(
53 &self,
54 template: &str,
55 data: &serde_json::Value,
56 context: HashMap<String, serde_json::Value>,
57 ) -> Result<String, RenderError>;
58
59 fn supports_includes(&self) -> bool;
61
62 fn supports_filters(&self) -> bool;
64
65 fn supports_control_flow(&self) -> bool;
67}
68
69pub struct MiniJinjaEngine {
99 env: Environment<'static>,
100}
101
102impl MiniJinjaEngine {
103 pub fn new() -> Self {
105 let mut env = Environment::new();
106 register_filters(&mut env);
107 Self { env }
108 }
109
110 pub fn environment(&self) -> &Environment<'static> {
115 &self.env
116 }
117
118 pub fn environment_mut(&mut self) -> &mut Environment<'static> {
123 &mut self.env
124 }
125}
126
127impl Default for MiniJinjaEngine {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl TemplateEngine for MiniJinjaEngine {
134 fn render_template(
135 &self,
136 template: &str,
137 data: &serde_json::Value,
138 ) -> Result<String, RenderError> {
139 let value = Value::from_serialize(data);
140 Ok(self.env.render_str(template, value)?)
141 }
142
143 fn add_template(&mut self, name: &str, source: &str) -> Result<(), RenderError> {
144 self.env
145 .add_template_owned(name.to_string(), source.to_string())?;
146 Ok(())
147 }
148
149 fn render_named(&self, name: &str, data: &serde_json::Value) -> Result<String, RenderError> {
150 let tmpl = self.env.get_template(name)?;
151 let value = Value::from_serialize(data);
152 Ok(tmpl.render(value)?)
153 }
154
155 fn has_template(&self, name: &str) -> bool {
156 self.env.get_template(name).is_ok()
157 }
158
159 fn render_with_context(
160 &self,
161 template: &str,
162 data: &serde_json::Value,
163 context: HashMap<String, serde_json::Value>,
164 ) -> Result<String, RenderError> {
165 let mut combined = HashMap::new();
167 for (key, value) in context {
168 combined.insert(key, Value::from_serialize(value));
169 }
170
171 if let serde_json::Value::Object(map) = data {
172 for (key, value) in map {
173 combined.insert(key.clone(), Value::from_serialize(value));
174 }
175 }
176
177 Ok(self.env.render_str(template, &combined)?)
178 }
179
180 fn supports_includes(&self) -> bool {
181 true
182 }
183
184 fn supports_filters(&self) -> bool {
185 true
186 }
187
188 fn supports_control_flow(&self) -> bool {
189 true
190 }
191}
192
193pub fn register_filters(env: &mut Environment<'static>) {
198 use minijinja::{Error, ErrorKind};
199
200 env.add_filter("nl", |value: Value| -> String { format!("{}\n", value) });
202
203 env.add_filter(
205 "style",
206 |_value: Value, _name: String| -> Result<String, Error> {
207 Err(Error::new(
208 ErrorKind::InvalidOperation,
209 "The `style()` filter was removed in Standout 1.0. \
210 Use tag syntax instead: [stylename]{{ value }}[/stylename]",
211 ))
212 },
213 );
214
215 crate::tabular::filters::register_tabular_filters(env);
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use serde::Serialize;
223
224 #[derive(Serialize)]
225 struct TestData {
226 name: String,
227 count: usize,
228 }
229
230 #[test]
231 fn test_minijinja_engine_simple() {
232 let engine = MiniJinjaEngine::new();
233 let data = TestData {
234 name: "World".into(),
235 count: 42,
236 };
237 let data_value = serde_json::to_value(&data).unwrap();
238 let output = engine
239 .render_template("Hello, {{ name }}!", &data_value)
240 .unwrap();
241 assert_eq!(output, "Hello, World!");
242 }
243
244 #[test]
245 fn test_minijinja_engine_with_loop() {
246 let engine = MiniJinjaEngine::new();
247
248 #[derive(Serialize)]
249 struct ListData {
250 items: Vec<String>,
251 }
252
253 let data = ListData {
254 items: vec!["a".into(), "b".into(), "c".into()],
255 };
256 let data_value = serde_json::to_value(&data).unwrap();
257 let output = engine
258 .render_template(
259 "{% for item in items %}{{ item }},{% endfor %}",
260 &data_value,
261 )
262 .unwrap();
263 assert_eq!(output, "a,b,c,");
264 }
265
266 #[test]
267 fn test_minijinja_engine_named_template() {
268 let mut engine = MiniJinjaEngine::new();
269 engine
270 .add_template("greeting", "Hello, {{ name }}!")
271 .unwrap();
272
273 let data = TestData {
274 name: "World".into(),
275 count: 0,
276 };
277 let data_value = serde_json::to_value(&data).unwrap();
278 let output = engine.render_named("greeting", &data_value).unwrap();
279 assert_eq!(output, "Hello, World!");
280 }
281
282 #[test]
283 fn test_minijinja_engine_template_error() {
284 let engine = MiniJinjaEngine::new();
285 let result = engine.render_template("{{ unclosed", &serde_json::Value::Null);
286 assert!(result.is_err());
287 }
288
289 #[test]
290 fn test_minijinja_engine_with_context() {
291 let engine = MiniJinjaEngine::new();
292
293 #[derive(Serialize)]
294 struct Data {
295 name: String,
296 }
297
298 let mut context = HashMap::new();
299 context.insert(
300 "version".to_string(),
301 serde_json::Value::String("1.0.0".into()),
302 );
303
304 let data = Data {
305 name: "Test".into(),
306 };
307 let data_value = serde_json::to_value(&data).unwrap();
308 let output = engine
309 .render_with_context("{{ name }} v{{ version }}", &data_value, context)
310 .unwrap();
311 assert_eq!(output, "Test v1.0.0");
312 }
313
314 #[test]
315 fn test_minijinja_engine_supports_features() {
316 let engine = MiniJinjaEngine::new();
317 assert!(engine.supports_includes());
318 assert!(engine.supports_filters());
319 assert!(engine.supports_control_flow());
320 }
321}