1 use std::borrow::Cow;
2 use std::collections::HashMap;
3 use std::fmt::{self, Debug, Formatter};
4 use std::io::{Error as IoError, Write};
5 use std::path::Path;
6 use std::sync::Arc;
7
8 use serde::Serialize;
9
10 use crate::context::Context;
11 use crate::decorators::{self, DecoratorDef};
12 #[cfg(feature = "script_helper")]
13 use crate::error::ScriptError;
14 use crate::error::{RenderError, TemplateError};
15 use crate::helpers::{self, HelperDef};
16 use crate::output::{Output, StringOutput, WriteOutput};
17 use crate::render::{RenderContext, Renderable};
18 use crate::sources::{FileSource, Source};
19 use crate::support::str::{self, StringWriter};
20 use crate::template::Template;
21
22 #[cfg(feature = "dir_source")]
23 use std::path;
24 #[cfg(feature = "dir_source")]
25 use walkdir::{DirEntry, WalkDir};
26
27 #[cfg(feature = "script_helper")]
28 use rhai::Engine;
29
30 #[cfg(feature = "script_helper")]
31 use crate::helpers::scripting::ScriptHelper;
32
33 /// This type represents an *escape fn*, that is a function whose purpose it is
34 /// to escape potentially problematic characters in a string.
35 ///
36 /// An *escape fn* is represented as a `Box` to avoid unnecessary type
37 /// parameters (and because traits cannot be aliased using `type`).
38 pub type EscapeFn = Arc<dyn Fn(&str) -> String + Send + Sync>;
39
40 /// The default *escape fn* replaces the characters `&"<>`
41 /// with the equivalent html / xml entities.
html_escape(data: &str) -> String42 pub fn html_escape(data: &str) -> String {
43 str::escape_html(data)
44 }
45
46 /// `EscapeFn` that does not change anything. Useful when using in a non-html
47 /// environment.
no_escape(data: &str) -> String48 pub fn no_escape(data: &str) -> String {
49 data.to_owned()
50 }
51
52 /// The single entry point of your Handlebars templates
53 ///
54 /// It maintains compiled templates and registered helpers.
55 #[derive(Clone)]
56 pub struct Registry<'reg> {
57 templates: HashMap<String, Template>,
58
59 helpers: HashMap<String, Arc<dyn HelperDef + Send + Sync + 'reg>>,
60 decorators: HashMap<String, Arc<dyn DecoratorDef + Send + Sync + 'reg>>,
61
62 escape_fn: EscapeFn,
63 strict_mode: bool,
64 dev_mode: bool,
65 #[cfg(feature = "script_helper")]
66 pub(crate) engine: Arc<Engine>,
67
68 template_sources:
69 HashMap<String, Arc<dyn Source<Item = String, Error = IoError> + Send + Sync + 'reg>>,
70 #[cfg(feature = "script_helper")]
71 script_sources:
72 HashMap<String, Arc<dyn Source<Item = String, Error = IoError> + Send + Sync + 'reg>>,
73 }
74
75 impl<'reg> Debug for Registry<'reg> {
fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error>76 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
77 f.debug_struct("Handlebars")
78 .field("templates", &self.templates)
79 .field("helpers", &self.helpers.keys())
80 .field("decorators", &self.decorators.keys())
81 .field("strict_mode", &self.strict_mode)
82 .field("dev_mode", &self.dev_mode)
83 .finish()
84 }
85 }
86
87 impl<'reg> Default for Registry<'reg> {
default() -> Self88 fn default() -> Self {
89 Self::new()
90 }
91 }
92
93 #[cfg(feature = "dir_source")]
filter_file(entry: &DirEntry, suffix: &str) -> bool94 fn filter_file(entry: &DirEntry, suffix: &str) -> bool {
95 let path = entry.path();
96
97 // ignore hidden files, emacs buffers and files with wrong suffix
98 !path.is_file()
99 || path
100 .file_name()
101 .map(|s| {
102 let ds = s.to_string_lossy();
103 ds.starts_with('.') || ds.starts_with('#') || !ds.ends_with(suffix)
104 })
105 .unwrap_or(true)
106 }
107
108 #[cfg(feature = "script_helper")]
rhai_engine() -> Engine109 fn rhai_engine() -> Engine {
110 Engine::new()
111 }
112
113 impl<'reg> Registry<'reg> {
new() -> Registry<'reg>114 pub fn new() -> Registry<'reg> {
115 let r = Registry {
116 templates: HashMap::new(),
117 template_sources: HashMap::new(),
118 helpers: HashMap::new(),
119 decorators: HashMap::new(),
120 escape_fn: Arc::new(html_escape),
121 strict_mode: false,
122 dev_mode: false,
123 #[cfg(feature = "script_helper")]
124 engine: Arc::new(rhai_engine()),
125 #[cfg(feature = "script_helper")]
126 script_sources: HashMap::new(),
127 };
128
129 r.setup_builtins()
130 }
131
setup_builtins(mut self) -> Registry<'reg>132 fn setup_builtins(mut self) -> Registry<'reg> {
133 self.register_helper("if", Box::new(helpers::IF_HELPER));
134 self.register_helper("unless", Box::new(helpers::UNLESS_HELPER));
135 self.register_helper("each", Box::new(helpers::EACH_HELPER));
136 self.register_helper("with", Box::new(helpers::WITH_HELPER));
137 self.register_helper("lookup", Box::new(helpers::LOOKUP_HELPER));
138 self.register_helper("raw", Box::new(helpers::RAW_HELPER));
139 self.register_helper("log", Box::new(helpers::LOG_HELPER));
140
141 self.register_helper("eq", Box::new(helpers::helper_extras::eq));
142 self.register_helper("ne", Box::new(helpers::helper_extras::ne));
143 self.register_helper("gt", Box::new(helpers::helper_extras::gt));
144 self.register_helper("gte", Box::new(helpers::helper_extras::gte));
145 self.register_helper("lt", Box::new(helpers::helper_extras::lt));
146 self.register_helper("lte", Box::new(helpers::helper_extras::lte));
147 self.register_helper("and", Box::new(helpers::helper_extras::and));
148 self.register_helper("or", Box::new(helpers::helper_extras::or));
149 self.register_helper("not", Box::new(helpers::helper_extras::not));
150 self.register_helper("len", Box::new(helpers::helper_extras::len));
151
152 self.register_decorator("inline", Box::new(decorators::INLINE_DECORATOR));
153 self
154 }
155
156 /// Enable or disable handlebars strict mode
157 ///
158 /// By default, handlebars renders empty string for value that
159 /// undefined or never exists. Since rust is a static type
160 /// language, we offer strict mode in handlebars-rust. In strict
161 /// mode, if you were to render a value that doesn't exist, a
162 /// `RenderError` will be raised.
set_strict_mode(&mut self, enabled: bool)163 pub fn set_strict_mode(&mut self, enabled: bool) {
164 self.strict_mode = enabled;
165 }
166
167 /// Return strict mode state, default is false.
168 ///
169 /// By default, handlebars renders empty string for value that
170 /// undefined or never exists. Since rust is a static type
171 /// language, we offer strict mode in handlebars-rust. In strict
172 /// mode, if you were access a value that doesn't exist, a
173 /// `RenderError` will be raised.
strict_mode(&self) -> bool174 pub fn strict_mode(&self) -> bool {
175 self.strict_mode
176 }
177
178 /// Return dev mode state, default is false
179 ///
180 /// With dev mode turned on, handlebars enables a set of development
181 /// friendly features, that may affect its performance.
dev_mode(&self) -> bool182 pub fn dev_mode(&self) -> bool {
183 self.dev_mode
184 }
185
186 /// Enable or disable dev mode
187 ///
188 /// With dev mode turned on, handlebars enables a set of development
189 /// friendly features, that may affect its performance.
190 ///
191 /// **Note that you have to enable dev mode before adding templates to
192 /// the registry**. Otherwise it won't take effect at all.
set_dev_mode(&mut self, enabled: bool)193 pub fn set_dev_mode(&mut self, enabled: bool) {
194 self.dev_mode = enabled;
195
196 // clear template source when disabling dev mode
197 if !enabled {
198 self.template_sources.clear();
199 }
200 }
201
202 /// Register a `Template`
203 ///
204 /// This is infallible since the template has already been parsed and
205 /// insert cannot fail. If there is an existing template with this name it
206 /// will be overwritten.
207 ///
208 /// Dev mode doesn't apply for pre-compiled template because it's lifecycle
209 /// is not managed by the registry.
register_template(&mut self, name: &str, tpl: Template)210 pub fn register_template(&mut self, name: &str, tpl: Template) {
211 self.templates.insert(name.to_string(), tpl);
212 }
213
214 /// Register a template string
215 ///
216 /// Returns `TemplateError` if there is syntax error on parsing the template.
register_template_string<S>( &mut self, name: &str, tpl_str: S, ) -> Result<(), TemplateError> where S: AsRef<str>,217 pub fn register_template_string<S>(
218 &mut self,
219 name: &str,
220 tpl_str: S,
221 ) -> Result<(), TemplateError>
222 where
223 S: AsRef<str>,
224 {
225 let template = Template::compile_with_name(tpl_str, name.to_owned())?;
226 self.register_template(name, template);
227 Ok(())
228 }
229
230 /// Register a partial string
231 ///
232 /// A named partial will be added to the registry. It will overwrite template with
233 /// same name. Currently a registered partial is just identical to a template.
register_partial<S>(&mut self, name: &str, partial_str: S) -> Result<(), TemplateError> where S: AsRef<str>,234 pub fn register_partial<S>(&mut self, name: &str, partial_str: S) -> Result<(), TemplateError>
235 where
236 S: AsRef<str>,
237 {
238 self.register_template_string(name, partial_str)
239 }
240
241 /// Register a template from a path on file system
242 ///
243 /// If dev mode is enabled, the registry will keep reading the template file
244 /// from file system everytime it's visited.
register_template_file<P>( &mut self, name: &str, tpl_path: P, ) -> Result<(), TemplateError> where P: AsRef<Path>,245 pub fn register_template_file<P>(
246 &mut self,
247 name: &str,
248 tpl_path: P,
249 ) -> Result<(), TemplateError>
250 where
251 P: AsRef<Path>,
252 {
253 let source = FileSource::new(tpl_path.as_ref().into());
254 let template_string = source
255 .load()
256 .map_err(|err| TemplateError::from((err, name.to_owned())))?;
257
258 self.register_template_string(name, template_string)?;
259 if self.dev_mode {
260 self.template_sources
261 .insert(name.to_owned(), Arc::new(source));
262 }
263
264 Ok(())
265 }
266
267 /// Register templates from a directory
268 ///
269 /// * `tpl_extension`: the template file extension
270 /// * `dir_path`: the path of directory
271 ///
272 /// Hidden files and tempfile (starts with `#`) will be ignored. All registered
273 /// will use their relative name as template name. For example, when `dir_path` is
274 /// `templates/` and `tpl_extension` is `.hbs`, the file
275 /// `templates/some/path/file.hbs` will be registered as `some/path/file`.
276 ///
277 /// This method is not available by default.
278 /// You will need to enable the `dir_source` feature to use it.
279 ///
280 /// When dev_mode enabled, like `register_template_file`, templates is reloaded
281 /// from file system everytime it's visied.
282 #[cfg(feature = "dir_source")]
283 #[cfg_attr(docsrs, doc(cfg(feature = "dir_source")))]
register_templates_directory<P>( &mut self, tpl_extension: &'static str, dir_path: P, ) -> Result<(), TemplateError> where P: AsRef<Path>,284 pub fn register_templates_directory<P>(
285 &mut self,
286 tpl_extension: &'static str,
287 dir_path: P,
288 ) -> Result<(), TemplateError>
289 where
290 P: AsRef<Path>,
291 {
292 let dir_path = dir_path.as_ref();
293
294 let prefix_len = if dir_path
295 .to_string_lossy()
296 .ends_with(|c| c == '\\' || c == '/')
297 // `/` will work on windows too so we still need to check
298 {
299 dir_path.to_string_lossy().len()
300 } else {
301 dir_path.to_string_lossy().len() + 1
302 };
303
304 let walker = WalkDir::new(dir_path);
305 let dir_iter = walker
306 .min_depth(1)
307 .into_iter()
308 .filter(|e| e.is_ok() && !filter_file(e.as_ref().unwrap(), tpl_extension));
309
310 for entry in dir_iter {
311 let entry = entry?;
312
313 let tpl_path = entry.path();
314 let tpl_file_path = entry.path().to_string_lossy();
315
316 let tpl_name = &tpl_file_path[prefix_len..tpl_file_path.len() - tpl_extension.len()];
317 // replace platform path separator with our internal one
318 let tpl_canonical_name = tpl_name.replace(path::MAIN_SEPARATOR, "/");
319 self.register_template_file(&tpl_canonical_name, &tpl_path)?;
320 }
321
322 Ok(())
323 }
324
325 /// Remove a template from the registry
unregister_template(&mut self, name: &str)326 pub fn unregister_template(&mut self, name: &str) {
327 self.templates.remove(name);
328 self.template_sources.remove(name);
329 }
330
331 /// Register a helper
register_helper(&mut self, name: &str, def: Box<dyn HelperDef + Send + Sync + 'reg>)332 pub fn register_helper(&mut self, name: &str, def: Box<dyn HelperDef + Send + Sync + 'reg>) {
333 self.helpers.insert(name.to_string(), def.into());
334 }
335
336 /// Register a [rhai](https://docs.rs/rhai/) script as handlebars helper
337 ///
338 /// Currently only simple helpers are supported. You can do computation or
339 /// string formatting with rhai script.
340 ///
341 /// Helper parameters and hash are available in rhai script as array `params`
342 /// and map `hash`. Example script:
343 ///
344 /// ```handlebars
345 /// {{percent 0.34 label="%"}}
346 /// ```
347 ///
348 /// ```rhai
349 /// // percent.rhai
350 /// let value = params[0];
351 /// let label = hash["label"];
352 ///
353 /// (value * 100).to_string() + label
354 /// ```
355 ///
356 #[cfg(feature = "script_helper")]
357 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
register_script_helper(&mut self, name: &str, script: &str) -> Result<(), ScriptError>358 pub fn register_script_helper(&mut self, name: &str, script: &str) -> Result<(), ScriptError> {
359 let compiled = self.engine.compile(script)?;
360 let script_helper = ScriptHelper { script: compiled };
361 self.helpers
362 .insert(name.to_string(), Arc::new(script_helper));
363 Ok(())
364 }
365
366 /// Register a [rhai](https://docs.rs/rhai/) script from file
367 ///
368 /// When dev mode is enable, script file is reloaded from original file
369 /// everytime it is called.
370 #[cfg(feature = "script_helper")]
371 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
register_script_helper_file<P>( &mut self, name: &str, script_path: P, ) -> Result<(), ScriptError> where P: AsRef<Path>,372 pub fn register_script_helper_file<P>(
373 &mut self,
374 name: &str,
375 script_path: P,
376 ) -> Result<(), ScriptError>
377 where
378 P: AsRef<Path>,
379 {
380 let source = FileSource::new(script_path.as_ref().into());
381 let script = source.load()?;
382
383 self.script_sources
384 .insert(name.to_owned(), Arc::new(source));
385 self.register_script_helper(name, &script)
386 }
387
388 /// Borrow a read-only reference to current rhai engine
389 #[cfg(feature = "script_helper")]
390 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
engine(&self) -> &Engine391 pub fn engine(&self) -> &Engine {
392 self.engine.as_ref()
393 }
394
395 /// Set a custom rhai engine for the registry.
396 ///
397 /// *Note that* you need to set custom engine before adding scripts.
398 #[cfg(feature = "script_helper")]
399 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
set_engine(&mut self, engine: Engine)400 pub fn set_engine(&mut self, engine: Engine) {
401 self.engine = Arc::new(engine);
402 }
403
404 /// Register a decorator
register_decorator( &mut self, name: &str, def: Box<dyn DecoratorDef + Send + Sync + 'reg>, )405 pub fn register_decorator(
406 &mut self,
407 name: &str,
408 def: Box<dyn DecoratorDef + Send + Sync + 'reg>,
409 ) {
410 self.decorators.insert(name.to_string(), def.into());
411 }
412
413 /// Register a new *escape fn* to be used from now on by this registry.
register_escape_fn<F: 'static + Fn(&str) -> String + Send + Sync>( &mut self, escape_fn: F, )414 pub fn register_escape_fn<F: 'static + Fn(&str) -> String + Send + Sync>(
415 &mut self,
416 escape_fn: F,
417 ) {
418 self.escape_fn = Arc::new(escape_fn);
419 }
420
421 /// Restore the default *escape fn*.
unregister_escape_fn(&mut self)422 pub fn unregister_escape_fn(&mut self) {
423 self.escape_fn = Arc::new(html_escape);
424 }
425
426 /// Get a reference to the current *escape fn*.
get_escape_fn(&self) -> &dyn Fn(&str) -> String427 pub fn get_escape_fn(&self) -> &dyn Fn(&str) -> String {
428 &*self.escape_fn
429 }
430
431 /// Return `true` if a template is registered for the given name
has_template(&self, name: &str) -> bool432 pub fn has_template(&self, name: &str) -> bool {
433 self.get_template(name).is_some()
434 }
435
436 /// Return a registered template,
get_template(&self, name: &str) -> Option<&Template>437 pub fn get_template(&self, name: &str) -> Option<&Template> {
438 self.templates.get(name)
439 }
440
441 #[inline]
get_or_load_template_optional( &'reg self, name: &str, ) -> Option<Result<Cow<'reg, Template>, RenderError>>442 pub(crate) fn get_or_load_template_optional(
443 &'reg self,
444 name: &str,
445 ) -> Option<Result<Cow<'reg, Template>, RenderError>> {
446 if let (true, Some(source)) = (self.dev_mode, self.template_sources.get(name)) {
447 let r = source
448 .load()
449 .map_err(|e| TemplateError::from((e, name.to_owned())))
450 .and_then(|tpl_str| Template::compile_with_name(tpl_str, name.to_owned()))
451 .map(Cow::Owned)
452 .map_err(RenderError::from);
453 Some(r)
454 } else {
455 self.templates.get(name).map(|t| Ok(Cow::Borrowed(t)))
456 }
457 }
458
459 #[inline]
get_or_load_template( &'reg self, name: &str, ) -> Result<Cow<'reg, Template>, RenderError>460 pub(crate) fn get_or_load_template(
461 &'reg self,
462 name: &str,
463 ) -> Result<Cow<'reg, Template>, RenderError> {
464 if let Some(result) = self.get_or_load_template_optional(name) {
465 result
466 } else {
467 Err(RenderError::new(format!("Template not found: {}", name)))
468 }
469 }
470
471 /// Return a registered helper
472 #[inline]
get_or_load_helper( &'reg self, name: &str, ) -> Result<Option<Arc<dyn HelperDef + Send + Sync + 'reg>>, RenderError>473 pub(crate) fn get_or_load_helper(
474 &'reg self,
475 name: &str,
476 ) -> Result<Option<Arc<dyn HelperDef + Send + Sync + 'reg>>, RenderError> {
477 #[cfg(feature = "script_helper")]
478 if let (true, Some(source)) = (self.dev_mode, self.script_sources.get(name)) {
479 return source
480 .load()
481 .map_err(ScriptError::from)
482 .and_then(|s| {
483 let helper = Box::new(ScriptHelper {
484 script: self.engine.compile(&s)?,
485 }) as Box<dyn HelperDef + Send + Sync>;
486 Ok(Some(helper.into()))
487 })
488 .map_err(RenderError::from);
489 }
490
491 Ok(self.helpers.get(name).cloned())
492 }
493
494 #[inline]
has_helper(&self, name: &str) -> bool495 pub(crate) fn has_helper(&self, name: &str) -> bool {
496 self.helpers.contains_key(name)
497 }
498
499 /// Return a registered decorator
500 #[inline]
get_decorator( &self, name: &str, ) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)>501 pub(crate) fn get_decorator(
502 &self,
503 name: &str,
504 ) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> {
505 self.decorators.get(name).map(|v| v.as_ref())
506 }
507
508 /// Return all templates registered
509 ///
510 /// **Note that** in dev mode, the template returned from this method may
511 /// not reflect its latest state. This method doesn't try to reload templates
512 /// from its source.
get_templates(&self) -> &HashMap<String, Template>513 pub fn get_templates(&self) -> &HashMap<String, Template> {
514 &self.templates
515 }
516
517 /// Unregister all templates
clear_templates(&mut self)518 pub fn clear_templates(&mut self) {
519 self.templates.clear();
520 self.template_sources.clear();
521 }
522
523 #[inline]
render_to_output<O>( &self, name: &str, ctx: &Context, output: &mut O, ) -> Result<(), RenderError> where O: Output,524 fn render_to_output<O>(
525 &self,
526 name: &str,
527 ctx: &Context,
528 output: &mut O,
529 ) -> Result<(), RenderError>
530 where
531 O: Output,
532 {
533 self.get_or_load_template(name).and_then(|t| {
534 let mut render_context = RenderContext::new(t.name.as_ref());
535 t.render(self, ctx, &mut render_context, output)
536 })
537 }
538
539 /// Render a registered template with some data into a string
540 ///
541 /// * `name` is the template name you registered previously
542 /// * `data` is the data that implements `serde::Serialize`
543 ///
544 /// Returns rendered string or a struct with error information
render<T>(&self, name: &str, data: &T) -> Result<String, RenderError> where T: Serialize,545 pub fn render<T>(&self, name: &str, data: &T) -> Result<String, RenderError>
546 where
547 T: Serialize,
548 {
549 let mut output = StringOutput::new();
550 let ctx = Context::wraps(&data)?;
551 self.render_to_output(name, &ctx, &mut output)?;
552 output.into_string().map_err(RenderError::from)
553 }
554
555 /// Render a registered template with reused context
render_with_context(&self, name: &str, ctx: &Context) -> Result<String, RenderError>556 pub fn render_with_context(&self, name: &str, ctx: &Context) -> Result<String, RenderError> {
557 let mut output = StringOutput::new();
558 self.render_to_output(name, ctx, &mut output)?;
559 output.into_string().map_err(RenderError::from)
560 }
561
562 /// Render a registered template and write some data to the `std::io::Write`
render_to_write<T, W>(&self, name: &str, data: &T, writer: W) -> Result<(), RenderError> where T: Serialize, W: Write,563 pub fn render_to_write<T, W>(&self, name: &str, data: &T, writer: W) -> Result<(), RenderError>
564 where
565 T: Serialize,
566 W: Write,
567 {
568 let mut output = WriteOutput::new(writer);
569 let ctx = Context::wraps(data)?;
570 self.render_to_output(name, &ctx, &mut output)
571 }
572
573 /// Render a template string using current registry without registering it
render_template<T>(&self, template_string: &str, data: &T) -> Result<String, RenderError> where T: Serialize,574 pub fn render_template<T>(&self, template_string: &str, data: &T) -> Result<String, RenderError>
575 where
576 T: Serialize,
577 {
578 let mut writer = StringWriter::new();
579 self.render_template_to_write(template_string, data, &mut writer)?;
580 Ok(writer.into_string())
581 }
582
583 /// Render a template string using reused context data
render_template_with_context( &self, template_string: &str, ctx: &Context, ) -> Result<String, RenderError>584 pub fn render_template_with_context(
585 &self,
586 template_string: &str,
587 ctx: &Context,
588 ) -> Result<String, RenderError> {
589 let tpl = Template::compile(template_string)?;
590
591 let mut out = StringOutput::new();
592 {
593 let mut render_context = RenderContext::new(None);
594 tpl.render(self, ctx, &mut render_context, &mut out)?;
595 }
596
597 out.into_string().map_err(RenderError::from)
598 }
599
600 /// Render a template string using current registry without registering it
render_template_to_write<T, W>( &self, template_string: &str, data: &T, writer: W, ) -> Result<(), RenderError> where T: Serialize, W: Write,601 pub fn render_template_to_write<T, W>(
602 &self,
603 template_string: &str,
604 data: &T,
605 writer: W,
606 ) -> Result<(), RenderError>
607 where
608 T: Serialize,
609 W: Write,
610 {
611 let tpl = Template::compile(template_string)?;
612 let ctx = Context::wraps(data)?;
613 let mut render_context = RenderContext::new(None);
614 let mut out = WriteOutput::new(writer);
615 tpl.render(self, &ctx, &mut render_context, &mut out)
616 }
617 }
618
619 #[cfg(test)]
620 mod test {
621 use crate::context::Context;
622 use crate::error::RenderError;
623 use crate::helpers::HelperDef;
624 use crate::output::Output;
625 use crate::registry::Registry;
626 use crate::render::{Helper, RenderContext, Renderable};
627 use crate::support::str::StringWriter;
628 use crate::template::Template;
629 use std::fs::File;
630 use std::io::Write;
631 use tempfile::tempdir;
632
633 #[derive(Clone, Copy)]
634 struct DummyHelper;
635
636 impl HelperDef for DummyHelper {
call<'reg: 'rc, 'rc>( &self, h: &Helper<'reg, 'rc>, r: &'reg Registry<'reg>, ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output, ) -> Result<(), RenderError>637 fn call<'reg: 'rc, 'rc>(
638 &self,
639 h: &Helper<'reg, 'rc>,
640 r: &'reg Registry<'reg>,
641 ctx: &'rc Context,
642 rc: &mut RenderContext<'reg, 'rc>,
643 out: &mut dyn Output,
644 ) -> Result<(), RenderError> {
645 h.template().unwrap().render(r, ctx, rc, out)
646 }
647 }
648
649 static DUMMY_HELPER: DummyHelper = DummyHelper;
650
651 #[test]
test_registry_operations()652 fn test_registry_operations() {
653 let mut r = Registry::new();
654
655 assert!(r.register_template_string("index", "<h1></h1>").is_ok());
656
657 let tpl = Template::compile("<h2></h2>").unwrap();
658 r.register_template("index2", tpl);
659
660 assert_eq!(r.templates.len(), 2);
661
662 r.unregister_template("index");
663 assert_eq!(r.templates.len(), 1);
664
665 r.clear_templates();
666 assert_eq!(r.templates.len(), 0);
667
668 r.register_helper("dummy", Box::new(DUMMY_HELPER));
669
670 // built-in helpers plus 1
671 let num_helpers = 7;
672 let num_boolean_helpers = 10; // stuff like gt and lte
673 let num_custom_helpers = 1; // dummy from above
674 assert_eq!(
675 r.helpers.len(),
676 num_helpers + num_boolean_helpers + num_custom_helpers
677 );
678 }
679
680 #[test]
681 #[cfg(feature = "dir_source")]
test_register_templates_directory()682 fn test_register_templates_directory() {
683 use std::fs::DirBuilder;
684
685 let mut r = Registry::new();
686 {
687 let dir = tempdir().unwrap();
688
689 assert_eq!(r.templates.len(), 0);
690
691 let file1_path = dir.path().join("t1.hbs");
692 let mut file1: File = File::create(&file1_path).unwrap();
693 writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();
694
695 let file2_path = dir.path().join("t2.hbs");
696 let mut file2: File = File::create(&file2_path).unwrap();
697 writeln!(file2, "<h1>Hola {{world}}!</h1>").unwrap();
698
699 let file3_path = dir.path().join("t3.hbs");
700 let mut file3: File = File::create(&file3_path).unwrap();
701 writeln!(file3, "<h1>Hallo {{world}}!</h1>").unwrap();
702
703 let file4_path = dir.path().join(".t4.hbs");
704 let mut file4: File = File::create(&file4_path).unwrap();
705 writeln!(file4, "<h1>Hallo {{world}}!</h1>").unwrap();
706
707 r.register_templates_directory(".hbs", dir.path()).unwrap();
708
709 assert_eq!(r.templates.len(), 3);
710 assert_eq!(r.templates.contains_key("t1"), true);
711 assert_eq!(r.templates.contains_key("t2"), true);
712 assert_eq!(r.templates.contains_key("t3"), true);
713 assert_eq!(r.templates.contains_key("t4"), false);
714
715 drop(file1);
716 drop(file2);
717 drop(file3);
718
719 dir.close().unwrap();
720 }
721
722 {
723 let dir = tempdir().unwrap();
724
725 let file1_path = dir.path().join("t4.hbs");
726 let mut file1: File = File::create(&file1_path).unwrap();
727 writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();
728
729 let file2_path = dir.path().join("t5.erb");
730 let mut file2: File = File::create(&file2_path).unwrap();
731 writeln!(file2, "<h1>Hello {{% world %}}!</h1>").unwrap();
732
733 let file3_path = dir.path().join("t6.html");
734 let mut file3: File = File::create(&file3_path).unwrap();
735 writeln!(file3, "<h1>Hello world!</h1>").unwrap();
736
737 r.register_templates_directory(".hbs", dir.path()).unwrap();
738
739 assert_eq!(r.templates.len(), 4);
740 assert_eq!(r.templates.contains_key("t4"), true);
741
742 drop(file1);
743 drop(file2);
744 drop(file3);
745
746 dir.close().unwrap();
747 }
748
749 {
750 let dir = tempdir().unwrap();
751
752 let _ = DirBuilder::new().create(dir.path().join("french")).unwrap();
753 let _ = DirBuilder::new()
754 .create(dir.path().join("portugese"))
755 .unwrap();
756 let _ = DirBuilder::new()
757 .create(dir.path().join("italian"))
758 .unwrap();
759
760 let file1_path = dir.path().join("french/t7.hbs");
761 let mut file1: File = File::create(&file1_path).unwrap();
762 writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();
763
764 let file2_path = dir.path().join("portugese/t8.hbs");
765 let mut file2: File = File::create(&file2_path).unwrap();
766 writeln!(file2, "<h1>Ola {{world}}!</h1>").unwrap();
767
768 let file3_path = dir.path().join("italian/t9.hbs");
769 let mut file3: File = File::create(&file3_path).unwrap();
770 writeln!(file3, "<h1>Ciao {{world}}!</h1>").unwrap();
771
772 r.register_templates_directory(".hbs", dir.path()).unwrap();
773
774 assert_eq!(r.templates.len(), 7);
775 assert_eq!(r.templates.contains_key("french/t7"), true);
776 assert_eq!(r.templates.contains_key("portugese/t8"), true);
777 assert_eq!(r.templates.contains_key("italian/t9"), true);
778
779 drop(file1);
780 drop(file2);
781 drop(file3);
782
783 dir.close().unwrap();
784 }
785
786 {
787 let dir = tempdir().unwrap();
788
789 let file1_path = dir.path().join("t10.hbs");
790 let mut file1: File = File::create(&file1_path).unwrap();
791 writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();
792
793 let mut dir_path = dir
794 .path()
795 .to_string_lossy()
796 .replace(std::path::MAIN_SEPARATOR, "/");
797 if !dir_path.ends_with("/") {
798 dir_path.push('/');
799 }
800 r.register_templates_directory(".hbs", dir_path).unwrap();
801
802 assert_eq!(r.templates.len(), 8);
803 assert_eq!(r.templates.contains_key("t10"), true);
804
805 drop(file1);
806 dir.close().unwrap();
807 }
808 }
809
810 #[test]
test_render_to_write()811 fn test_render_to_write() {
812 let mut r = Registry::new();
813
814 assert!(r.register_template_string("index", "<h1></h1>").is_ok());
815
816 let mut sw = StringWriter::new();
817 {
818 r.render_to_write("index", &(), &mut sw).ok().unwrap();
819 }
820
821 assert_eq!("<h1></h1>".to_string(), sw.into_string());
822 }
823
824 #[test]
test_escape_fn()825 fn test_escape_fn() {
826 let mut r = Registry::new();
827
828 let input = String::from("\"<>&");
829
830 r.register_template_string("test", String::from("{{this}}"))
831 .unwrap();
832
833 assert_eq!(""<>&", r.render("test", &input).unwrap());
834
835 r.register_escape_fn(|s| s.into());
836
837 assert_eq!("\"<>&", r.render("test", &input).unwrap());
838
839 r.unregister_escape_fn();
840
841 assert_eq!(""<>&", r.render("test", &input).unwrap());
842 }
843
844 #[test]
test_escape()845 fn test_escape() {
846 let r = Registry::new();
847 let data = json!({"hello": "world"});
848
849 assert_eq!(
850 "{{hello}}",
851 r.render_template(r"\{{hello}}", &data).unwrap()
852 );
853
854 assert_eq!(
855 " {{hello}}",
856 r.render_template(r" \{{hello}}", &data).unwrap()
857 );
858
859 assert_eq!(r"\world", r.render_template(r"\\{{hello}}", &data).unwrap());
860 }
861
862 #[test]
test_strict_mode()863 fn test_strict_mode() {
864 let mut r = Registry::new();
865 assert!(!r.strict_mode());
866
867 r.set_strict_mode(true);
868 assert!(r.strict_mode());
869
870 let data = json!({
871 "the_only_key": "the_only_value"
872 });
873
874 assert!(r
875 .render_template("accessing the_only_key {{the_only_key}}", &data)
876 .is_ok());
877 assert!(r
878 .render_template("accessing non-exists key {{the_key_never_exists}}", &data)
879 .is_err());
880
881 let render_error = r
882 .render_template("accessing non-exists key {{the_key_never_exists}}", &data)
883 .unwrap_err();
884 assert_eq!(render_error.column_no.unwrap(), 26);
885
886 let data2 = json!([1, 2, 3]);
887 assert!(r
888 .render_template("accessing valid array index {{this.[2]}}", &data2)
889 .is_ok());
890 assert!(r
891 .render_template("accessing invalid array index {{this.[3]}}", &data2)
892 .is_err());
893 let render_error2 = r
894 .render_template("accessing invalid array index {{this.[3]}}", &data2)
895 .unwrap_err();
896 assert_eq!(render_error2.column_no.unwrap(), 31);
897 }
898
899 use crate::json::value::ScopedJson;
900 struct GenMissingHelper;
901 impl HelperDef for GenMissingHelper {
call_inner<'reg: 'rc, 'rc>( &self, _: &Helper<'reg, 'rc>, _: &'reg Registry<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, ) -> Result<ScopedJson<'reg, 'rc>, RenderError>902 fn call_inner<'reg: 'rc, 'rc>(
903 &self,
904 _: &Helper<'reg, 'rc>,
905 _: &'reg Registry<'reg>,
906 _: &'rc Context,
907 _: &mut RenderContext<'reg, 'rc>,
908 ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
909 Ok(ScopedJson::Missing)
910 }
911 }
912
913 #[test]
test_strict_mode_in_helper()914 fn test_strict_mode_in_helper() {
915 let mut r = Registry::new();
916 r.set_strict_mode(true);
917
918 r.register_helper(
919 "check_missing",
920 Box::new(
921 |h: &Helper<'_, '_>,
922 _: &Registry<'_>,
923 _: &Context,
924 _: &mut RenderContext<'_, '_>,
925 _: &mut dyn Output|
926 -> Result<(), RenderError> {
927 let value = h.param(0).unwrap();
928 assert!(value.is_value_missing());
929 Ok(())
930 },
931 ),
932 );
933
934 r.register_helper("generate_missing_value", Box::new(GenMissingHelper));
935
936 let data = json!({
937 "the_key_we_have": "the_value_we_have"
938 });
939 assert!(r
940 .render_template("accessing non-exists key {{the_key_we_dont_have}}", &data)
941 .is_err());
942 assert!(r
943 .render_template(
944 "accessing non-exists key from helper {{check_missing the_key_we_dont_have}}",
945 &data
946 )
947 .is_ok());
948 assert!(r
949 .render_template(
950 "accessing helper that generates missing value {{generate_missing_value}}",
951 &data
952 )
953 .is_err());
954 }
955
956 #[test]
test_html_expression()957 fn test_html_expression() {
958 let reg = Registry::new();
959 assert_eq!(
960 reg.render_template("{{{ a }}}", &json!({"a": "<b>bold</b>"}))
961 .unwrap(),
962 "<b>bold</b>"
963 );
964 assert_eq!(
965 reg.render_template("{{ &a }}", &json!({"a": "<b>bold</b>"}))
966 .unwrap(),
967 "<b>bold</b>"
968 );
969 }
970
971 #[test]
test_render_context()972 fn test_render_context() {
973 let mut reg = Registry::new();
974
975 let data = json!([0, 1, 2, 3]);
976
977 assert_eq!(
978 "0123",
979 reg.render_template_with_context(
980 "{{#each this}}{{this}}{{/each}}",
981 &Context::wraps(&data).unwrap()
982 )
983 .unwrap()
984 );
985
986 reg.register_template_string("t0", "{{#each this}}{{this}}{{/each}}")
987 .unwrap();
988 assert_eq!(
989 "0123",
990 reg.render_with_context("t0", &Context::wraps(&data).unwrap())
991 .unwrap()
992 );
993 }
994
995 #[test]
test_keys_starts_with_null()996 fn test_keys_starts_with_null() {
997 env_logger::init();
998 let reg = Registry::new();
999 let data = json!({
1000 "optional": true,
1001 "is_null": true,
1002 "nullable": true,
1003 "null": true,
1004 "falsevalue": true,
1005 });
1006 assert_eq!(
1007 "optional: true --> true",
1008 reg.render_template(
1009 "optional: {{optional}} --> {{#if optional }}true{{else}}false{{/if}}",
1010 &data
1011 )
1012 .unwrap()
1013 );
1014 assert_eq!(
1015 "is_null: true --> true",
1016 reg.render_template(
1017 "is_null: {{is_null}} --> {{#if is_null }}true{{else}}false{{/if}}",
1018 &data
1019 )
1020 .unwrap()
1021 );
1022 assert_eq!(
1023 "nullable: true --> true",
1024 reg.render_template(
1025 "nullable: {{nullable}} --> {{#if nullable }}true{{else}}false{{/if}}",
1026 &data
1027 )
1028 .unwrap()
1029 );
1030 assert_eq!(
1031 "falsevalue: true --> true",
1032 reg.render_template(
1033 "falsevalue: {{falsevalue}} --> {{#if falsevalue }}true{{else}}false{{/if}}",
1034 &data
1035 )
1036 .unwrap()
1037 );
1038 assert_eq!(
1039 "null: true --> false",
1040 reg.render_template(
1041 "null: {{null}} --> {{#if null }}true{{else}}false{{/if}}",
1042 &data
1043 )
1044 .unwrap()
1045 );
1046 assert_eq!(
1047 "null: true --> true",
1048 reg.render_template(
1049 "null: {{null}} --> {{#if this.[null]}}true{{else}}false{{/if}}",
1050 &data
1051 )
1052 .unwrap()
1053 );
1054 }
1055
1056 #[test]
test_dev_mode_template_reload()1057 fn test_dev_mode_template_reload() {
1058 let mut reg = Registry::new();
1059 reg.set_dev_mode(true);
1060 assert!(reg.dev_mode());
1061
1062 let dir = tempdir().unwrap();
1063 let file1_path = dir.path().join("t1.hbs");
1064 {
1065 let mut file1: File = File::create(&file1_path).unwrap();
1066 write!(file1, "<h1>Hello {{{{name}}}}!</h1>").unwrap();
1067 }
1068
1069 reg.register_template_file("t1", &file1_path).unwrap();
1070
1071 assert_eq!(
1072 reg.render("t1", &json!({"name": "Alex"})).unwrap(),
1073 "<h1>Hello Alex!</h1>"
1074 );
1075
1076 {
1077 let mut file1: File = File::create(&file1_path).unwrap();
1078 write!(file1, "<h1>Privet {{{{name}}}}!</h1>").unwrap();
1079 }
1080
1081 assert_eq!(
1082 reg.render("t1", &json!({"name": "Alex"})).unwrap(),
1083 "<h1>Privet Alex!</h1>"
1084 );
1085
1086 dir.close().unwrap();
1087 }
1088
1089 #[test]
1090 #[cfg(feature = "script_helper")]
test_script_helper()1091 fn test_script_helper() {
1092 let mut reg = Registry::new();
1093
1094 reg.register_script_helper("acc", "params.reduce(|sum, x| x + sum, 0)")
1095 .unwrap();
1096
1097 assert_eq!(
1098 reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(),
1099 "10"
1100 );
1101 }
1102
1103 #[test]
1104 #[cfg(feature = "script_helper")]
test_script_helper_dev_mode()1105 fn test_script_helper_dev_mode() {
1106 let mut reg = Registry::new();
1107 reg.set_dev_mode(true);
1108
1109 let dir = tempdir().unwrap();
1110 let file1_path = dir.path().join("acc.rhai");
1111 {
1112 let mut file1: File = File::create(&file1_path).unwrap();
1113 write!(file1, "params.reduce(|sum, x| x + sum, 0)").unwrap();
1114 }
1115
1116 reg.register_script_helper_file("acc", &file1_path).unwrap();
1117
1118 assert_eq!(
1119 reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(),
1120 "10"
1121 );
1122
1123 {
1124 let mut file1: File = File::create(&file1_path).unwrap();
1125 write!(file1, "params.reduce(|sum, x| x * sum, 1)").unwrap();
1126 }
1127
1128 assert_eq!(
1129 reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(),
1130 "24"
1131 );
1132
1133 dir.close().unwrap();
1134 }
1135
1136 #[test]
1137 #[cfg(feature = "script_helper")]
test_engine_access()1138 fn test_engine_access() {
1139 use rhai::Engine;
1140
1141 let mut registry = Registry::new();
1142 let mut eng = Engine::new();
1143 eng.set_max_string_size(1000);
1144 registry.set_engine(eng);
1145
1146 assert_eq!(1000, registry.engine().max_string_size());
1147 }
1148 }
1149