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