use minijinja::{Environment, Value};
use std::collections::HashMap;
use crate::error::RenderError;
pub trait TemplateEngine {
fn render_template(
&self,
template: &str,
data: &serde_json::Value,
) -> Result<String, RenderError>;
fn add_template(&mut self, name: &str, source: &str) -> Result<(), RenderError>;
fn render_named(&self, name: &str, data: &serde_json::Value) -> Result<String, RenderError>;
fn has_template(&self, name: &str) -> bool;
fn render_with_context(
&self,
template: &str,
data: &serde_json::Value,
context: HashMap<String, serde_json::Value>,
) -> Result<String, RenderError>;
fn supports_includes(&self) -> bool;
fn supports_filters(&self) -> bool;
fn supports_control_flow(&self) -> bool;
}
pub struct MiniJinjaEngine {
env: Environment<'static>,
}
impl MiniJinjaEngine {
pub fn new() -> Self {
let mut env = Environment::new();
register_filters(&mut env);
Self { env }
}
pub fn environment(&self) -> &Environment<'static> {
&self.env
}
pub fn environment_mut(&mut self) -> &mut Environment<'static> {
&mut self.env
}
}
impl Default for MiniJinjaEngine {
fn default() -> Self {
Self::new()
}
}
impl TemplateEngine for MiniJinjaEngine {
fn render_template(
&self,
template: &str,
data: &serde_json::Value,
) -> Result<String, RenderError> {
let value = Value::from_serialize(data);
Ok(self.env.render_str(template, value)?)
}
fn add_template(&mut self, name: &str, source: &str) -> Result<(), RenderError> {
self.env
.add_template_owned(name.to_string(), source.to_string())?;
Ok(())
}
fn render_named(&self, name: &str, data: &serde_json::Value) -> Result<String, RenderError> {
let tmpl = self.env.get_template(name)?;
let value = Value::from_serialize(data);
Ok(tmpl.render(value)?)
}
fn has_template(&self, name: &str) -> bool {
self.env.get_template(name).is_ok()
}
fn render_with_context(
&self,
template: &str,
data: &serde_json::Value,
context: HashMap<String, serde_json::Value>,
) -> Result<String, RenderError> {
let mut combined = HashMap::new();
for (key, value) in context {
combined.insert(key, Value::from_serialize(value));
}
if let serde_json::Value::Object(map) = data {
for (key, value) in map {
combined.insert(key.clone(), Value::from_serialize(value));
}
}
Ok(self.env.render_str(template, &combined)?)
}
fn supports_includes(&self) -> bool {
true
}
fn supports_filters(&self) -> bool {
true
}
fn supports_control_flow(&self) -> bool {
true
}
}
pub fn register_filters(env: &mut Environment<'static>) {
use minijinja::{Error, ErrorKind};
env.add_filter("nl", |value: Value| -> String { format!("{}\n", value) });
env.add_filter(
"style",
|_value: Value, _name: String| -> Result<String, Error> {
Err(Error::new(
ErrorKind::InvalidOperation,
"The `style()` filter was removed in Standout 1.0. \
Use tag syntax instead: [stylename]{{ value }}[/stylename]",
))
},
);
crate::tabular::filters::register_tabular_filters(env);
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
#[derive(Serialize)]
struct TestData {
name: String,
count: usize,
}
#[test]
fn test_minijinja_engine_simple() {
let engine = MiniJinjaEngine::new();
let data = TestData {
name: "World".into(),
count: 42,
};
let data_value = serde_json::to_value(&data).unwrap();
let output = engine
.render_template("Hello, {{ name }}!", &data_value)
.unwrap();
assert_eq!(output, "Hello, World!");
}
#[test]
fn test_minijinja_engine_with_loop() {
let engine = MiniJinjaEngine::new();
#[derive(Serialize)]
struct ListData {
items: Vec<String>,
}
let data = ListData {
items: vec!["a".into(), "b".into(), "c".into()],
};
let data_value = serde_json::to_value(&data).unwrap();
let output = engine
.render_template(
"{% for item in items %}{{ item }},{% endfor %}",
&data_value,
)
.unwrap();
assert_eq!(output, "a,b,c,");
}
#[test]
fn test_minijinja_engine_named_template() {
let mut engine = MiniJinjaEngine::new();
engine
.add_template("greeting", "Hello, {{ name }}!")
.unwrap();
let data = TestData {
name: "World".into(),
count: 0,
};
let data_value = serde_json::to_value(&data).unwrap();
let output = engine.render_named("greeting", &data_value).unwrap();
assert_eq!(output, "Hello, World!");
}
#[test]
fn test_minijinja_engine_template_error() {
let engine = MiniJinjaEngine::new();
let result = engine.render_template("{{ unclosed", &serde_json::Value::Null);
assert!(result.is_err());
}
#[test]
fn test_minijinja_engine_with_context() {
let engine = MiniJinjaEngine::new();
#[derive(Serialize)]
struct Data {
name: String,
}
let mut context = HashMap::new();
context.insert(
"version".to_string(),
serde_json::Value::String("1.0.0".into()),
);
let data = Data {
name: "Test".into(),
};
let data_value = serde_json::to_value(&data).unwrap();
let output = engine
.render_with_context("{{ name }} v{{ version }}", &data_value, context)
.unwrap();
assert_eq!(output, "Test v1.0.0");
}
#[test]
fn test_minijinja_engine_supports_features() {
let engine = MiniJinjaEngine::new();
assert!(engine.supports_includes());
assert!(engine.supports_filters());
assert!(engine.supports_control_flow());
}
}