ramhorns/
lib.rs

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