1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! <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;">
//!
//! # Ramhorns
//!
//! Fast [**Mustache**](https://mustache.github.io/) template engine implementation
//! in pure Rust.
//!
//! **Ramhorns** loads and processes templates **at runtime**. It comes with a derive macro
//! which allows for templates to be rendered from native Rust data structures without doing
//! temporary allocations, intermediate `HashMap`s or what have you.
//!
//! With a touch of magic 🎩, the power of friendship 🥂, and a sparkle of
//! [FNV hashing](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function)
//! ✨, render times easily compete with static template engines like
//! [**Askama**](https://github.com/djc/askama).
//!
//! What else do you want, a sticker?
//!
//! ## Example
//!
//! ```rust
//! use ramhorns::{Template, Content};
//!
//! #[derive(Content)]
//! struct Post<'a> {
//!     title: &'a str,
//!     teaser: &'a str,
//! }
//!
//! #[derive(Content)]
//! struct Blog<'a> {
//!     title: String,        // Strings are cool
//!     posts: Vec<Post<'a>>, // &'a [Post<'a>] would work too
//! }
//!
//! // Standard Mustache action here
//! let source = "<h1>{{title}}</h1>\
//!               {{#posts}}<article><h2>{{title}}</h2><p>{{teaser}}</p></article>{{/posts}}\
//!               {{^posts}}<p>No posts yet :(</p>{{/posts}}";
//!
//! let tpl = Template::new(source).unwrap();
//!
//! let rendered = tpl.render(&Blog {
//!     title: "My Awesome Blog!".to_string(),
//!     posts: vec![
//!         Post {
//!             title: "How I tried Ramhorns and found love 💖",
//!             teaser: "This can happen to you too",
//!         },
//!         Post {
//!             title: "Rust is kinda awesome",
//!             teaser: "Yes, even the borrow checker! 🦀",
//!         },
//!     ]
//! });
//!
//! assert_eq!(rendered, "<h1>My Awesome Blog!</h1>\
//!                       <article>\
//!                           <h2>How I tried Ramhorns and found love 💖</h2>\
//!                           <p>This can happen to you too</p>\
//!                       </article>\
//!                       <article>\
//!                           <h2>Rust is kinda awesome</h2>\
//!                           <p>Yes, even the borrow checker! 🦀</p>\
//!                       </article>");
//! ```

#![warn(missing_docs)]
use std::collections::HashMap;
use std::fmt;
use std::hash::BuildHasher;
use std::path::{Path, PathBuf};

use beef::Cow;
use std::io::ErrorKind;

mod content;
mod error;
mod template;
pub mod traits;

pub mod encoding;

pub use content::Content;
pub use error::Error;
pub use template::{Section, Template};

#[cfg(feature = "export_derive")]
pub use ramhorns_derive::Content;

/// Aggregator for [`Template`s](./struct.Template.html), that allows them to
/// be loaded from the file system and use partials: `{{>partial}}`
///
/// For faster or DOS-resistant hashes, it is recommended to use
/// [aHash](https://docs.rs/ahash/latest/ahash/) `RandomState` as hasher.
pub struct Ramhorns<H = fnv::FnvBuildHasher> {
    partials: HashMap<Cow<'static, str>, Template<'static>, H>,
    dir: PathBuf,
}

impl<H> fmt::Debug for Ramhorns<H> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Ramhorns").field("dir", &self.dir).finish()
    }
}

impl<H: BuildHasher + Default> Ramhorns<H> {
    /// Loads all the `.html` files as templates from the given folder, making them
    /// accessible via their path, joining partials as required. If a custom
    /// extension is wanted, see [from_folder_with_extension]
    /// ```no_run
    /// # use ramhorns::Ramhorns;
    /// let tpls: Ramhorns = Ramhorns::from_folder("./templates").unwrap();
    /// let content = "I am the content";
    /// let rendered = tpls.get("hello.html").unwrap().render(&content);
    /// ```
    pub fn from_folder<P: AsRef<Path>>(dir: P) -> Result<Self, Error> {
        Self::from_folder_with_extension(dir, "html")
    }

    /// Loads all files with the extension given in the `extension` parameter as templates
    /// from the given folder, making them accessible via their path, joining partials as
    /// required.
    /// ```no_run
    /// # use ramhorns::Ramhorns;
    /// let tpls: Ramhorns = Ramhorns::from_folder_with_extension("./templates", "mustache").unwrap();
    /// let content = "I am the content";
    /// let rendered = tpls.get("hello.mustache").unwrap().render(&content);
    /// ```
    #[inline]
    pub fn from_folder_with_extension<P: AsRef<Path>>(
        dir: P,
        extension: &str,
    ) -> Result<Self, Error> {
        let mut templates = Ramhorns::lazy(dir)?;
        templates.load_folder(&templates.dir.clone(), extension)?;

        Ok(templates)
    }

    /// Extends the template collection with files with `.html` extension
    /// from the given folder, making them accessible via their path, joining partials as
    /// required.
    /// If there is a file with the same name as a  previously loaded template or partial,
    /// it will not be loaded.
    pub fn extend_from_folder<P: AsRef<Path>>(&mut self, dir: P) -> Result<(), Error> {
        self.extend_from_folder_with_extension(dir, "html")
    }

    /// Extends the template collection with files with `extension`
    /// from the given folder, making them accessible via their path, joining partials as
    /// required.
    /// If there is a file with the same name as a  previously loaded template or partial,
    /// it will not be loaded.
    #[inline]
    pub fn extend_from_folder_with_extension<P: AsRef<Path>>(
        &mut self,
        dir: P,
        extension: &str,
    ) -> Result<(), Error> {
        let dir = std::mem::replace(&mut self.dir, dir.as_ref().canonicalize()?);
        self.load_folder(&self.dir.clone(), extension)?;
        self.dir = dir;

        Ok(())
    }

    fn load_folder(&mut self, dir: &Path, extension: &str) -> Result<(), Error> {
        for entry in std::fs::read_dir(dir)? {
            let path = entry?.path();
            if path.is_dir() {
                self.load_folder(&path, extension)?;
            } else if path.extension().map(|e| e == extension).unwrap_or(false) {
                let name = path
                    .strip_prefix(&self.dir)
                    .unwrap_or(&path)
                    .to_string_lossy();
                if !self.partials.contains_key(name.as_ref()) {
                    self.load_internal(&path, Cow::owned(name.to_string()))?;
                }
            }
        }
        Ok(())
    }

    /// Create a new empty aggregator for a given folder. This won't do anything until
    /// a template has been added using [`from_file`](#method.from_file).
    /// ```no_run
    /// # use ramhorns::Ramhorns;
    /// let mut tpls: Ramhorns = Ramhorns::lazy("./templates").unwrap();
    /// let content = "I am the content";
    /// let rendered = tpls.from_file("hello.html").unwrap().render(&content);
    /// ```
    pub fn lazy<P: AsRef<Path>>(dir: P) -> Result<Self, Error> {
        Ok(Ramhorns {
            partials: HashMap::default(),
            dir: dir.as_ref().canonicalize()?,
        })
    }

    /// Get the template with the given name, if it exists.
    pub fn get(&self, name: &str) -> Option<&Template<'static>> {
        self.partials.get(name)
    }

    /// Get the template with the given name. If the template doesn't exist,
    /// it will be loaded from file and parsed first.
    ///
    /// Use this method in tandem with [`lazy`](#method.lazy).
    pub fn from_file(&mut self, name: &str) -> Result<&Template<'static>, Error> {
        let path = self.dir.join(name);
        if !self.partials.contains_key(name) {
            self.load_internal(&path, Cow::owned(name.to_string()))?;
        }
        Ok(&self.partials[name])
    }

    // Unsafe to expose as it loads the template from arbitrary path.
    #[inline]
    fn load_internal(&mut self, path: &Path, name: Cow<'static, str>) -> Result<(), Error> {
        let file = match std::fs::read_to_string(path) {
            Ok(file) => Ok(file),
            Err(e) if e.kind() == ErrorKind::NotFound => {
                Err(Error::NotFound(name.to_string().into()))
            }
            Err(e) => Err(Error::Io(e)),
        }?;
        self.insert(file, name)
    }

    /// Insert a template parsed from `src` with the name `name`.
    /// If a template with this name is present, it gets replaced.
    ///
    /// # Warning
    /// This can load partials from an arbitrary path. Use only with trusted source.
    pub fn insert<S, T>(&mut self, src: S, name: T) -> Result<(), Error>
    where
        S: Into<Cow<'static, str>>,
        T: Into<Cow<'static, str>>,
    {
        let template = Template::load(src, self)?;
        self.partials.insert(name.into(), template);
        Ok(())
    }
}

pub(crate) trait Partials<'tpl> {
    fn get_partial(&mut self, name: &'tpl str) -> Result<&Template<'tpl>, Error>;
}

impl<H: BuildHasher + Default> Partials<'static> for Ramhorns<H> {
    fn get_partial(&mut self, name: &'static str) -> Result<&Template<'static>, Error> {
        if !self.partials.contains_key(name) {
            let path = self.dir.join(name).canonicalize()?;
            if !path.starts_with(&self.dir) {
                return Err(Error::IllegalPartial(name.into()));
            }
            self.load_internal(&path, Cow::borrowed(name))?;
        }
        Ok(&self.partials[name])
    }
}