handlebars/
registry.rs

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