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}