simple_html_template/
lib.rs

1use std::collections::HashMap;
2mod errors;
3#[cfg(test)]
4mod tests;
5
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::*;
8#[cfg(feature = "wasm")]
9use wasm_bindgen::JsCast;
10#[cfg(feature = "wasm")]
11use web_sys::{Document, DocumentFragment, HtmlTemplateElement, HtmlElement};
12
13pub use errors::{Error, Errors};
14
15#[macro_export]
16macro_rules! hash_map(
17    { $($key:expr => $value:expr),* $(,)? } => {
18        {
19            let mut m = ::std::collections::HashMap::new();
20            $(
21                m.insert($key, $value);
22            )+
23            m
24        }
25     };
26);
27
28#[macro_export]
29macro_rules! html_map(
30    { $($key:expr => $value:expr),* $(,)? } => {
31        {
32            let mut m = ::std::collections::HashMap::new();
33            $(
34                m.insert($key, htmlescape::encode_minimal($value));
35            )+
36            m
37        }
38     };
39);
40
41#[macro_export]
42macro_rules! html_map_strong(
43    { $($key:expr => $value:expr),* $(,)? } => {
44        {
45            let mut m = ::std::collections::HashMap::new();
46            $(
47                m.insert($key, htmlescape::encode_attribute($value));
48            )+
49            m
50        }
51     };
52);
53
54pub struct Template<'a> {
55    // Stores (key, (key_start, key_end))
56    pub replaces: Vec<(&'a str, (usize, usize))>,
57    pub template_str:&'a str,
58}
59
60
61impl <'a> Template <'a> {
62    pub fn new (template_str: &'a str) -> Result<Self, Error> {
63        let template_str = template_str.trim();
64        let mut template = Self { replaces: Vec::new(), template_str };
65
66        let replaces = &mut template.replaces;
67
68        // Current position in the format string
69        let mut cursor = 0;
70
71        while cursor < template_str.len() {
72            if let Some(start) = (&template_str[cursor..]).find("${") {
73                let start = start + cursor;
74                if let Some(end) = (&template_str[cursor..]).find('}') {
75                    let end = end + cursor;
76                    replaces.push((
77                        // The extracted key
78                        &template_str[(start + "${".len())..end],
79                        (
80                            // Points to the `$` in the `${`
81                            start,
82                            // Just after the matching `}`
83                            (end + "}".len()),
84                        ),
85                    ));
86
87                    // Move cursor to the end of this match
88                    cursor = end + "}".len();
89                } else {
90                    // Bail immediately: if there's an unclosed delimiter, then
91                    // we basically can't guess about what provided key-value
92                    // pairs are needed
93                    return Err(Error::Unclosed(start));
94                }
95            } else {
96                // No more matches
97                break;
98            }
99        }
100        Ok(template)
101    }
102
103    pub fn render<V: AsRef<str>>(&self, vars:&HashMap<&str, V>) -> Result<String, Errors> {
104        let mut errors = Vec::new();
105        let replaces = &self.replaces;
106        let template_str = &self.template_str;
107
108        for k in vars.keys() {
109            if !replaces.iter().any(|(x, (_, _))| x == k) {
110                errors.push(Error::Extra((*k).to_string()));
111            }
112        }
113
114        // Wait on bailing out if there are errors so we can display all the errors
115        // at once instead of making the user have to try to fix it twice.
116
117        // Calculate the size of the text to be added (vs) and the amount of space
118        // the keys take up in the original text (ks)
119        let (ks, vs) = replaces.iter().fold((0, 0), |(ka, va), (k, _)| {
120            if let Some(v) = vars.get(k) {
121                (ka + k.len(), va + v.as_ref().len())
122            } else {
123                errors.push(Error::Missing((*k).to_string()));
124
125                // This is mostly just to get past the typechecker
126                (ka, va)
127            }
128        });
129
130        // If there were errors, bail out
131        if !errors.is_empty() {
132            return Err(Errors {
133                inner: errors,
134            });
135        }
136
137        let final_len = (template_str.len() - ("${}".len() * replaces.len())) + vs - ks;
138
139        let mut output = String::with_capacity(final_len);
140
141        let mut cursor:usize = 0;
142
143        for (key, (start, end)) in replaces.into_iter() {
144            output.push_str(&template_str[cursor..*start]);
145            // Unwrapping should be safe at this point because we should have caught
146            // it while calculating replace_size.
147            output.push_str(vars.get(key).unwrap().as_ref());
148            cursor = *end;
149        }
150
151        // If there's more text after the final `${}`
152        if cursor < template_str.len() {
153            output.push_str(&template_str[cursor..]);
154        }
155
156        #[cfg(test)]
157        assert_eq!(output.len(), final_len);
158
159        Ok(output)
160    }
161    
162    pub fn render_plain(&self) -> &str {
163        self.template_str 
164    }
165
166
167    #[cfg(feature = "wasm")]
168    pub fn render_fragment<V: AsRef<str>>(&self, doc:&Document, data:&HashMap<&str, V>) -> Result<DocumentFragment, Errors> {
169        let html = self.render(data)?;
170        let el: HtmlTemplateElement = doc.create_element("template").unwrap_throw().unchecked_into();
171        el.set_inner_html(&html);
172        Ok(el.content())
173    }
174
175    #[cfg(feature = "wasm")]
176    pub fn render_fragment_plain(&self, doc:&Document) -> DocumentFragment {
177        let el: HtmlTemplateElement = doc.create_element("template").unwrap_throw().unchecked_into();
178        el.set_inner_html(&self.template_str);
179        el.content()
180    }
181
182    #[cfg(feature = "wasm")]
183    pub fn render_elem<V: AsRef<str>>(&self, doc:&Document, data:&HashMap<&str, V>) -> Result<HtmlElement, Errors> {
184        self.render_fragment(doc, data)
185            .map(|frag| {
186                frag.first_child().unwrap().unchecked_into()
187            })
188    }
189
190    #[cfg(feature = "wasm")]
191    pub fn render_elem_plain(&self, doc:&Document) -> HtmlElement {
192        let frag = self.render_fragment_plain(doc);
193        frag.first_child().unwrap_throw().unchecked_into()
194    }
195}
196
197
198/// render functions panic if the template name doesn't exist
199pub struct TemplateCache <'a> {
200    pub templates: HashMap<&'a str, Template<'a>>,
201    #[cfg(feature = "wasm")]
202    pub doc: Document,
203}
204
205impl <'a> TemplateCache <'a> {
206
207    pub fn new(templates:&[(&'a str, &'a str)]) -> Self{
208        let mut _templates = HashMap::new();
209
210        for (name, data) in templates {
211            _templates.insert(*name, Template::new(data).unwrap());
212        }
213
214        Self::_new(_templates)
215    }
216
217    cfg_if::cfg_if! {
218        if #[cfg(feature = "wasm")] {
219            fn _new(_templates:HashMap<&'a str, Template<'a>>) -> Self {
220                let window = web_sys::window().unwrap_throw();
221                let doc = window.document().unwrap_throw();
222
223                Self { templates: _templates, doc }
224            }
225        } else {
226            fn _new(_templates:HashMap<&'a str, Template<'a>>) -> Self {
227                Self {templates: _templates }
228            }
229        }
230    }
231
232    pub fn render<V: AsRef<str>>(&self, name:&str, data:&HashMap<&str,V>) -> Result<String, Errors> {
233        self.templates.get(name).unwrap().render(data)
234    }
235
236    pub fn render_plain(&self, name:&str) -> &str {
237        self.templates.get(name).unwrap().template_str
238    }
239
240    #[cfg(feature = "wasm")]
241    pub fn render_fragment<V: AsRef<str>>(&self, name:&str, data:&HashMap<&str, V>) -> Result<DocumentFragment, Errors> {
242        self.templates.get(name).unwrap_throw().render_fragment(&self.doc, data)
243    }
244
245    #[cfg(feature = "wasm")]
246    pub fn render_fragment_plain(&self, name:&str) -> DocumentFragment {
247        self.templates.get(name).unwrap_throw().render_fragment_plain(&self.doc)
248    }
249
250    #[cfg(feature = "wasm")]
251    pub fn render_elem<V: AsRef<str>>(&self, name:&str, data:&HashMap<&str, V>) -> Result<HtmlElement, Errors> {
252        self.templates.get(name).unwrap_throw().render_elem(&self.doc, data)
253    }
254
255    #[cfg(feature = "wasm")]
256    pub fn render_elem_plain(&self, name:&str) -> HtmlElement {
257        self.templates.get(name).unwrap_throw().render_elem_plain(&self.doc)
258    }
259}