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!("&quot;&lt;&gt;&amp;", 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!("&quot;&lt;&gt;&amp;", 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