1use handlebars::{Handlebars, Renderable, Template as HandlebarsTemplate};
2use minijinja::{Environment, Template as JinjaTemplate, Value as JinjaValue};
3use minijinja_contrib::add_to_environment;
4use self_cell::self_cell;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum TemplateDialect {
10 #[default]
11 Jinja,
12 Static,
13 Handlebars,
14}
15
16enum Engine {
17 Jinja {
18 env: Environment<'static>,
19 },
20 Static {
21 env: HashMap<String, String>,
22 },
23 Handlebars {
24 registry: Handlebars<'static>,
25 globals: HashMap<String, serde_json::Value>,
26 },
27}
28
29pub enum Template<'env, 'source> {
30 Jinja(JinjaTemplate<'env, 'source>),
31 Static(&'env str),
32 Handlebars {
33 engine: &'env TemplateEngine,
34 template: &'env HandlebarsTemplate,
35 },
36}
37
38impl<'env, 'source> Template<'env, 'source> {
39 pub fn render<S: Serialize>(&self, ctx: S) -> anyhow::Result<String> {
40 match &self {
41 Self::Jinja(t) => Ok(t.render(ctx)?),
42 Self::Static(s) => Ok(s.to_string()),
43 Self::Handlebars { .. } => {
44 let mut output: Vec<u8> = vec![];
45 self.render_to_write(&ctx, &mut output)?;
46 Ok(String::from_utf8(output)?)
47 }
48 }
49 }
50
51 pub fn render_to_write<S: Serialize, W: std::io::Write>(
52 &self,
53 ctx: S,
54 mut w: W,
55 ) -> anyhow::Result<()> {
56 match &self {
57 Self::Jinja(t) => {
58 t.render_to_write(ctx, w)?;
59 Ok(())
60 }
61 Self::Static(s) => {
62 w.write(s.as_bytes())?;
63 Ok(())
64 }
65 Self::Handlebars { engine, template } => {
66 let Engine::Handlebars { registry, globals } = &engine.engine else {
67 anyhow::bail!("impossible Handlebars Template vs. TemplateEngine state")
68 };
69
70 let context = serde_json::to_value(ctx)?;
71 let context = merge_contexts(globals, context);
72 let context = handlebars::Context::wraps(context)?;
73
74 let mut render_context = handlebars::RenderContext::new(None);
75 let is_html = template
76 .name
77 .as_deref()
78 .map(|name| name.ends_with(".html"))
79 .unwrap_or(false);
80 render_context.set_disable_escape(!is_html);
81
82 let output = template.renders(registry, &context, &mut render_context)?;
83 w.write_all(output.as_bytes())?;
84 Ok(())
85 }
86 }
87 }
88}
89
90pub struct TemplateEngine {
92 engine: Engine,
93}
94
95impl TemplateEngine {
96 pub fn new() -> Self {
97 Self::with_dialect(TemplateDialect::default())
98 }
99
100 pub fn with_dialect(dialect: TemplateDialect) -> Self {
101 match dialect {
102 TemplateDialect::Jinja => {
103 let mut env = Environment::new();
104 env.set_unknown_method_callback(
105 minijinja_contrib::pycompat::unknown_method_callback,
106 );
107 add_to_environment(&mut env);
108
109 env.add_filter(
110 "normalize_smtp_response",
111 mod_smtp_response_normalize::normalize,
112 );
113
114 Self {
115 engine: Engine::Jinja { env },
116 }
117 }
118 TemplateDialect::Static => Self {
119 engine: Engine::Static {
120 env: HashMap::new(),
121 },
122 },
123 TemplateDialect::Handlebars => Self {
124 engine: Engine::Handlebars {
125 registry: Handlebars::new(),
126 globals: HashMap::new(),
127 },
128 },
129 }
130 }
131
132 pub fn add_template<N, S>(&mut self, name: N, source: S) -> anyhow::Result<()>
136 where
137 N: Into<String>,
138 S: Into<String>,
139 {
140 match &mut self.engine {
141 Engine::Jinja { env } => Ok(env.add_template_owned(name.into(), source.into())?),
142 Engine::Static { env } => {
143 env.insert(name.into(), source.into());
144 Ok(())
145 }
146 Engine::Handlebars { registry, .. } => {
147 registry.register_template_string(&name.into(), source.into())?;
148 Ok(())
149 }
150 }
151 }
152
153 pub fn get_template(&self, name: &str) -> anyhow::Result<Template<'_, '_>> {
155 match &self.engine {
156 Engine::Jinja { env } => Ok(Template::Jinja(env.get_template(name)?)),
157 Engine::Static { env } => {
158 Ok(Template::Static(env.get(name).ok_or_else(|| {
159 anyhow::anyhow!("template {name} is not defined")
160 })?))
161 }
162 Engine::Handlebars { registry, .. } => {
163 let template = registry
164 .get_template(name)
165 .ok_or_else(|| anyhow::anyhow!("template {name} is not defined"))?;
166
167 Ok(Template::Handlebars {
168 engine: self,
169 template,
170 })
171 }
172 }
173 }
174
175 pub fn add_global<N, V>(&mut self, name: N, value: V) -> anyhow::Result<()>
177 where
178 N: Into<String>,
179 V: Serialize,
180 {
181 match &mut self.engine {
182 Engine::Jinja { env } => env.add_global(name.into(), JinjaValue::from_serialize(value)),
183 Engine::Static { .. } => { }
184 Engine::Handlebars { globals, .. } => {
185 globals.insert(name.into(), serde_json::to_value(value)?);
186 }
187 }
188
189 Ok(())
190 }
191
192 pub fn render<CTX>(&self, name: &str, source: &str, context: CTX) -> anyhow::Result<String>
193 where
194 CTX: serde::Serialize,
195 {
196 match &self.engine {
197 Engine::Jinja { env } => Ok(env.render_named_str(name, source, context)?),
198 Engine::Static { .. } => Ok(source.to_string()),
199 Engine::Handlebars { .. } => {
200 let template = HandlebarsTemplate::compile_with_name(source, name.to_string())?;
201 let template = Template::Handlebars {
202 engine: self,
203 template: &template,
204 };
205
206 template.render(context)
207 }
208 }
209 }
210}
211
212fn merge_contexts(
213 globals: &HashMap<String, serde_json::Value>,
214 over: serde_json::Value,
215) -> serde_json::Value {
216 match over {
217 serde_json::Value::Object(mut obj) => {
218 for (k, v) in globals {
219 if !obj.contains_key(k) {
220 obj.insert(k.into(), v.clone());
221 }
222 }
223 serde_json::Value::Object(obj)
224 }
225 other => other,
226 }
227}
228
229pub type TemplateList<'a> = Vec<Template<'a, 'a>>;
230
231self_cell!(
232 pub struct CompiledTemplates {
254 owner: TemplateEngine,
255 #[covariant]
256 dependent: TemplateList,
257 }
258);