sweet_potator/
template.rs1use 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 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}