dysql_tpl/lib.rs
1//! # Dysql-tpl
2//!
3//! Fast [**Mustache**](https://mustache.github.io/) template engine implementation
4//! in pure Rust.
5//!
6//! **Dysql-tpl** loads and processes templates **at runtime**. It comes with a derive macro
7//! which allows for templates to be rendered from native Rust data structures without doing
8//! temporary allocations, intermediate `HashMap`s or what have you.
9//!
10//! With a touch of magic 🎩, the power of friendship 🥂, and a sparkle of
11//! [FNV hashing](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function)
12//! ✨, render times easily compete with static template engines like
13//! [**Askama**](https://github.com/djc/askama).
14//!
15//! What else do you want, a sticker?
16//!
17//! ## Example
18//!
19//! ```ignore
20//! use dysql_tpl::{Template, Content};
21//!
22//! #[derive(Content)]
23//! struct Post<'a> {
24//! title: &'a str,
25//! teaser: &'a str,
26//! }
27//!
28//! #[derive(Content)]
29//! struct Blog<'a> {
30//! title: String, // Strings are cool
31//! posts: Vec<Post<'a>>, // &'a [Post<'a>] would work too
32//! }
33//!
34//! // Standard Mustache action here
35//! let source = "<h1>{{title}}</h1>\
36//! {{#posts}}<article><h2>{{title}}</h2><p>{{teaser}}</p></article>{{/posts}}\
37//! {{^posts}}<p>No posts yet :(</p>{{/posts}}";
38//!
39//! let tpl = Template::new(source).unwrap();
40//!
41//! let rendered = tpl.render(&Blog {
42//! title: "My Awesome Blog!".to_string(),
43//! posts: vec![
44//! Post {
45//! title: "How I tried Ramhorns and found love 💖",
46//! teaser: "This can happen to you too",
47//! },
48//! Post {
49//! title: "Rust is kinda awesome",
50//! teaser: "Yes, even the borrow checker! 🦀",
51//! },
52//! ]
53//! });
54//!
55//! assert_eq!(rendered, "<h1>My Awesome Blog!</h1>\
56//! <article>\
57//! <h2>How I tried Ramhorns and found love 💖</h2>\
58//! <p>This can happen to you too</p>\
59//! </article>\
60//! <article>\
61//! <h2>Rust is kinda awesome</h2>\
62//! <p>Yes, even the borrow checker! 🦀</p>\
63//! </article>");
64//! ```
65
66#![allow(missing_docs)]
67use std::collections::HashMap;
68use std::hash::{BuildHasher, Hash, Hasher};
69use std::path::{Path, PathBuf};
70
71use std::io::ErrorKind;
72
73mod content;
74mod error;
75mod template;
76mod simple;
77pub mod traits;
78
79pub mod encoding;
80
81pub use content::Content;
82pub use error::TemplateError;
83use fnv::FnvHasher;
84pub use template::{Section, Template};
85pub use simple::{SimpleTemplate, SimpleSection, SimpleValue, SimpleError, SimpleInnerError};
86
87#[cfg(feature = "export_derive")]
88pub use dysql_tpl_derive::Content;
89use traits::Combine;
90
91/// Necessary so that the warning of very complex type created when compiling
92/// with `cargo clippy` doesn't propagate to downstream crates
93type Next<'section, C, X> = (<C as Combine>::I, <C as Combine>::J, <C as Combine>::K, X);
94
95#[inline]
96pub(crate) fn hash_name(name: &str) -> u64 {
97 let mut hasher = FnvHasher::default();
98 name.hash(&mut hasher);
99 hasher.finish()
100}
101
102/// Aggregator for [`Template`s](./struct.Template.html), that allows them to
103/// be loaded from the file system and use partials: `{{>partial}}`
104///
105/// For faster or DOS-resistant hashes, it is recommended to use
106/// [aHash](https://docs.rs/ahash/latest/ahash/) `RandomState` as hasher.
107pub struct Ramhorns<H = fnv::FnvBuildHasher> {
108 partials: HashMap<String, Template, H>,
109 dir: PathBuf,
110}
111
112impl<H: BuildHasher + Default> Ramhorns<H> {
113 /// Loads all the `.html` files as templates from the given folder, making them
114 /// accessible via their path, joining partials as required. If a custom
115 /// extension is wanted, see [from_folder_with_extension]
116 /// ```no_run
117 /// # use dysql_tpl::Ramhorns;
118 /// let tpls: Ramhorns = Ramhorns::from_folder("./templates").unwrap();
119 /// let content = "I am the content";
120 /// let rendered = tpls.get("hello.html").unwrap().render(&content);
121 /// ```
122 pub fn from_folder<P: AsRef<Path>>(dir: P) -> Result<Self, TemplateError> {
123 Self::from_folder_with_extension(dir, "html")
124 }
125
126 /// Loads all files with the extension given in the `extension` parameter as templates
127 /// from the given folder, making them accessible via their path, joining partials as
128 /// required.
129 /// ```no_run
130 /// # use dysql_tpl::Ramhorns;
131 /// let tpls: Ramhorns = Ramhorns::from_folder_with_extension("./templates", "mustache").unwrap();
132 /// let content = "I am the content";
133 /// let rendered = tpls.get("hello.mustache").unwrap().render(&content);
134 /// ```
135 #[inline]
136 pub fn from_folder_with_extension<P: AsRef<Path>>(
137 dir: P,
138 extension: &str,
139 ) -> Result<Self, TemplateError> {
140 let mut templates = Ramhorns::lazy(dir)?;
141 templates.load_folder(&templates.dir.clone(), extension)?;
142
143 Ok(templates)
144 }
145
146 /// Extends the template collection with files with `.html` extension
147 /// from the given folder, making them accessible via their path, joining partials as
148 /// required.
149 /// If there is a file with the same name as a previously loaded template or partial,
150 /// it will not be loaded.
151 pub fn extend_from_folder<P: AsRef<Path>>(&mut self, dir: P) -> Result<(), TemplateError> {
152 self.extend_from_folder_with_extension(dir, "html")
153 }
154
155 /// Extends the template collection with files with `extension`
156 /// from the given folder, making them accessible via their path, joining partials as
157 /// required.
158 /// If there is a file with the same name as a previously loaded template or partial,
159 /// it will not be loaded.
160 #[inline]
161 pub fn extend_from_folder_with_extension<P: AsRef<Path>>(
162 &mut self,
163 dir: P,
164 extension: &str,
165 ) -> Result<(), TemplateError> {
166 let dir = std::mem::replace(&mut self.dir, dir.as_ref().canonicalize()?);
167 self.load_folder(&self.dir.clone(), extension)?;
168 self.dir = dir;
169
170 Ok(())
171 }
172
173 fn load_folder(&mut self, dir: &Path, extension: &str) -> Result<(), TemplateError> {
174 for entry in std::fs::read_dir(dir)? {
175 let path = entry?.path();
176 if path.is_dir() {
177 self.load_folder(&path, extension)?;
178 } else if path.extension().map(|e| e == extension).unwrap_or(false) {
179 let name = path
180 .strip_prefix(&self.dir)
181 .unwrap_or(&path)
182 .to_string_lossy();
183 if !self.partials.contains_key(name.as_ref()) {
184 self.load_internal(&path, &name)?;
185 }
186 }
187 }
188 Ok(())
189 }
190
191 /// Create a new empty aggregator for a given folder. This won't do anything until
192 /// a template has been added using [`from_file`](#method.from_file).
193 /// ```no_run
194 /// # use dysql_tpl::Ramhorns;
195 /// let mut tpls: Ramhorns = Ramhorns::lazy("./templates").unwrap();
196 /// let content = "I am the content";
197 /// let rendered = tpls.from_file("hello.html").unwrap().render(&content);
198 /// ```
199 pub fn lazy<P: AsRef<Path>>(dir: P) -> Result<Self, TemplateError> {
200 Ok(Ramhorns {
201 partials: HashMap::default(),
202 dir: dir.as_ref().canonicalize()?,
203 })
204 }
205
206 /// Get the template with the given name, if it exists.
207 pub fn get(&self, name: &str) -> Option<&Template>
208 {
209 self.partials.get(name)
210 }
211
212 /// Get the template with the given name. If the template doesn't exist,
213 /// it will be loaded from file and parsed first.
214 ///
215 /// Use this method in tandem with [`lazy`](#method.lazy).
216 pub fn from_file(&mut self, name: &str) -> Result<&Template, TemplateError> {
217 let path = self.dir.join(name);
218 if !self.partials.contains_key(name) {
219 self.load_internal(&path, name)?;
220 }
221 Ok(&self.partials[name])
222 }
223
224 // Unsafe to expose as it loads the template from arbitrary path.
225 #[inline]
226 fn load_internal(&mut self, path: &Path, name: &str) -> Result<(), TemplateError> {
227 let file = match std::fs::read_to_string(&path) {
228 Ok(file) => Ok(file),
229 Err(e) if e.kind() == ErrorKind::NotFound => {
230 Err(TemplateError::NotFound(name.to_string().into()))
231 }
232 Err(e) => Err(TemplateError::Io(e)),
233 }?;
234 let template = Template::load(&file, self)?;
235 self.partials.insert(name.to_owned(), template);
236 Ok(())
237 }
238}
239
240pub(crate) trait Partials {
241 fn get_partial(&mut self, name: &str) -> Result<&Template, TemplateError>;
242}
243
244impl<H: BuildHasher + Default> Partials for Ramhorns<H> {
245 fn get_partial(&mut self, name: &str) -> Result<&Template, TemplateError> {
246 if !self.partials.contains_key(name) {
247 let path = self.dir.join(name).canonicalize()?;
248 if !path.starts_with(&self.dir) {
249 return Err(TemplateError::IllegalPartial(name.into()));
250 }
251 self.load_internal(&path, name)?;
252 }
253 Ok(&self.partials[name])
254 }
255}