simple_html_template/
lib.rs1use 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 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 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 &template_str[(start + "${".len())..end],
79 (
80 start,
82 (end + "}".len()),
84 ),
85 ));
86
87 cursor = end + "}".len();
89 } else {
90 return Err(Error::Unclosed(start));
94 }
95 } else {
96 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 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 (ka, va)
127 }
128 });
129
130 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 output.push_str(vars.get(key).unwrap().as_ref());
148 cursor = *end;
149 }
150
151 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
198pub 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}