Skip to main content

sweet_potator/
template.rs

1use std::{
2    fs, io,
3    path::{Path, PathBuf},
4};
5
6use tera::Tera;
7use toml::Value;
8
9use crate::error::{Error, Result};
10
11pub type Context = tera::Context;
12
13pub const LANGUAGE_DIR: &str = "lang";
14pub const LANGUAGE_FILE_EXTENSION: &str = "toml";
15pub const STATIC_DIR: &str = "static";
16pub const TERA_DIR: &str = "tera";
17
18pub const INDEX_NAME: &str = "index";
19pub const RECIPE_NAME: &str = "recipe";
20
21pub struct Engine {
22    tera: Tera,
23    file_ext: String,
24    language_path: PathBuf,
25    static_path: PathBuf,
26    pub language: Option<String>,
27    pub forced_context: Option<Context>,
28}
29
30impl Engine {
31    /// # Panics
32    ///
33    /// Will panic if `PathBuf::to_str` returns `None`
34    pub fn new<E: Into<String>>(
35        path: &Path,
36        escape: bool,
37        file_ext: E,
38        language: Option<&str>,
39    ) -> Result<Self> {
40        let file_ext = file_ext.into();
41        let mut glob_path = path.join(TERA_DIR).join("**/*");
42        glob_path.set_extension(&file_ext);
43        let mut tera = Tera::new(glob_path.to_str().expect("invalid template path"))?;
44        if escape {
45            tera.autoescape_on(vec![""]);
46        } else {
47            tera.autoescape_on(Vec::new());
48        }
49        let engine = Self {
50            tera,
51            file_ext,
52            language_path: path.join(LANGUAGE_DIR),
53            static_path: path.join(STATIC_DIR),
54            language: language.map(Into::into),
55            forced_context: None,
56        };
57        if !engine.has_template(RECIPE_NAME) {
58            return Err(Error::MissingTemplateFile(
59                engine.template_path(RECIPE_NAME),
60            ));
61        }
62        Ok(engine)
63    }
64
65    pub(crate) fn has_index_template(&self) -> bool {
66        self.has_template(INDEX_NAME)
67    }
68
69    pub(crate) fn render_index(&self, context: Context, writer: impl io::Write) -> Result<()> {
70        self.render(INDEX_NAME, context, writer)
71    }
72
73    pub(crate) fn render_recipe(&self, context: Context, writer: impl io::Write) -> Result<()> {
74        self.render(RECIPE_NAME, context, writer)
75    }
76
77    pub(crate) fn static_path(&self) -> &Path {
78        &self.static_path
79    }
80
81    fn has_template(&self, name: &str) -> bool {
82        let path = self.template_path(name);
83        self.tera.get_template_names().any(|name| name == path)
84    }
85
86    fn load_language_file(&self) -> Result<Option<Value>> {
87        if let Some(language) = &self.language {
88            let path = self
89                .language_path
90                .join(language)
91                .with_extension(LANGUAGE_FILE_EXTENSION);
92            match fs::read_to_string(path) {
93                Ok(data) => Ok(Some(data.parse()?)),
94                Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
95                Err(error) => Err(error.into()),
96            }
97        } else {
98            Ok(None)
99        }
100    }
101
102    fn render(
103        &self,
104        template_name: &str,
105        mut context: Context,
106        writer: impl io::Write,
107    ) -> Result<()> {
108        if let Some(data) = self.load_language_file()? {
109            context.insert("lang", &data);
110        }
111        if let Some(fc) = &self.forced_context {
112            context.extend(fc.clone());
113        }
114        Ok(self
115            .tera
116            .render_to(&self.template_path(template_name), &context, writer)?)
117    }
118
119    fn template_path(&self, template_name: &str) -> String {
120        format!("{}.{}", template_name, self.file_ext)
121    }
122}