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}