Skip to main content

doido_view/
tera_engine.rs

1use crate::engine::TemplateEngine;
2use doido_core::{anyhow::Context as _, Result};
3use std::path::{Path, PathBuf};
4use std::sync::RwLock;
5
6pub struct TeraEngine {
7    tera: RwLock<tera::Tera>,
8    templates_dir: String,
9}
10
11impl TeraEngine {
12    pub fn new(templates_dir: &str) -> Result<Self> {
13        let tera = load(templates_dir)
14            .with_context(|| format!("failed to load templates from {templates_dir}"))?;
15        Ok(Self {
16            tera: RwLock::new(tera),
17            templates_dir: templates_dir.to_string(),
18        })
19    }
20}
21
22impl TemplateEngine for TeraEngine {
23    fn render(&self, template: &str, context: &serde_json::Value) -> Result<String> {
24        let template_name = format!("{}.html.tera", template);
25        let ctx = tera::Context::from_serialize(context)
26            .map_err(|e| doido_core::anyhow::anyhow!("invalid template context: {e}"))?;
27        self.tera
28            .read()
29            .unwrap()
30            .render(&template_name, &ctx)
31            .map_err(|e| doido_core::anyhow::anyhow!("template '{}' render failed: {e}", template))
32    }
33
34    fn render_named(&self, name: &str, context: &serde_json::Value) -> Result<String> {
35        let ctx = tera::Context::from_serialize(context)
36            .map_err(|e| doido_core::anyhow::anyhow!("invalid template context: {e}"))?;
37        self.tera
38            .read()
39            .unwrap()
40            .render(name, &ctx)
41            .map_err(|e| doido_core::anyhow::anyhow!("template '{}' render failed: {e}", name))
42    }
43
44    fn reload(&self) -> Result<()> {
45        let tera = load(&self.templates_dir)
46            .with_context(|| format!("reload failed for {}", self.templates_dir))?;
47        *self.tera.write().unwrap() = tera;
48        Ok(())
49    }
50}
51
52/// Load every `*.tera` file under `dir` into a Tera instance, keyed by the file's
53/// path relative to `dir` (so `dir/posts/index.html.tera` registers as
54/// `posts/index.html.tera`). Tera 2 dropped the glob constructor, so we walk the
55/// tree ourselves and add every template in one call (which resolves inheritance
56/// across the whole set regardless of insertion order).
57fn load(dir: &str) -> Result<tera::Tera> {
58    let base = Path::new(dir);
59    let mut files: Vec<(PathBuf, String)> = Vec::new();
60    if base.exists() {
61        collect(base, base, &mut files)?;
62    }
63    let mut tera = tera::Tera::new();
64    tera.add_template_files(files.iter().map(|(path, name)| (path, Some(name.as_str()))))
65        .map_err(|e| doido_core::anyhow::anyhow!("{e}"))?;
66    Ok(tera)
67}
68
69/// Recursively collect `*.tera` files under `dir`, pairing each with its path
70/// relative to `base` (forward-slash separated) to use as the Tera template name.
71fn collect(base: &Path, dir: &Path, out: &mut Vec<(PathBuf, String)>) -> Result<()> {
72    for entry in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
73        let path = entry?.path();
74        if path.is_dir() {
75            collect(base, &path, out)?;
76        } else if path.extension().and_then(|e| e.to_str()) == Some("tera") {
77            let rel = path.strip_prefix(base).unwrap_or(&path);
78            let name = rel
79                .components()
80                .map(|c| c.as_os_str().to_string_lossy())
81                .collect::<Vec<_>>()
82                .join("/");
83            out.push((path.clone(), name));
84        }
85    }
86    Ok(())
87}