1use std::borrow::Cow;
2use std::collections::{BTreeMap, HashMap};
3use std::convert::AsRef;
4use std::fmt::{self, Debug, Formatter};
5use std::io::{Error as IoError, Write};
6use std::path::Path;
7use std::sync::Arc;
8
9use serde::Serialize;
10
11use crate::context::Context;
12use crate::decorators::{self, DecoratorDef};
13#[cfg(feature = "script_helper")]
14use crate::error::ScriptError;
15use crate::error::{RenderError, RenderErrorReason, TemplateError};
16use crate::helpers::{self, HelperDef};
17use crate::output::{Output, StringOutput, WriteOutput};
18use crate::render::{RenderContext, Renderable};
19use crate::sources::{FileSource, Source};
20use crate::support::str::{self, StringWriter};
21use crate::template::{Template, TemplateOptions};
22
23#[cfg(feature = "dir_source")]
24use walkdir::WalkDir;
25
26#[cfg(feature = "dir_source")]
27use derive_builder::Builder;
28
29#[cfg(feature = "script_helper")]
30use rhai::Engine;
31
32#[cfg(feature = "script_helper")]
33use crate::helpers::scripting::ScriptHelper;
34
35#[cfg(feature = "rust-embed")]
36use crate::sources::LazySource;
37#[cfg(feature = "rust-embed")]
38use rust_embed::RustEmbed;
39
40pub type EscapeFn = Arc<dyn Fn(&str) -> String + Send + Sync>;
46
47pub fn html_escape(data: &str) -> String {
50 str::escape_html(data)
51}
52
53pub fn no_escape(data: &str) -> String {
56 data.to_owned()
57}
58
59#[derive(Clone)]
63pub struct Registry<'reg> {
64 templates: HashMap<String, Template>,
65
66 helpers: HashMap<String, Arc<dyn HelperDef + Send + Sync + 'reg>>,
67 decorators: HashMap<String, Arc<dyn DecoratorDef + Send + Sync + 'reg>>,
68
69 escape_fn: EscapeFn,
70 strict_mode: bool,
71 dev_mode: bool,
72 prevent_indent: bool,
73 #[cfg(feature = "script_helper")]
74 pub(crate) engine: Arc<Engine>,
75
76 template_sources:
77 HashMap<String, Arc<dyn Source<Item = String, Error = IoError> + Send + Sync + 'reg>>,
78 #[cfg(feature = "script_helper")]
79 script_sources:
80 HashMap<String, Arc<dyn Source<Item = String, Error = IoError> + Send + Sync + 'reg>>,
81}
82
83impl Debug for Registry<'_> {
84 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
85 f.debug_struct("Handlebars")
86 .field("templates", &self.templates)
87 .field("helpers", &self.helpers.keys())
88 .field("decorators", &self.decorators.keys())
89 .field("strict_mode", &self.strict_mode)
90 .field("dev_mode", &self.dev_mode)
91 .finish()
92 }
93}
94
95impl Default for Registry<'_> {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101#[cfg(feature = "script_helper")]
102fn rhai_engine() -> Engine {
103 Engine::new()
104}
105
106#[non_exhaustive]
108#[derive(Builder)]
109#[cfg(feature = "dir_source")]
110pub struct DirectorySourceOptions {
111 #[builder(setter(into))]
113 pub tpl_extension: String,
114 pub hidden: bool,
116 pub temporary: bool,
118}
119
120#[cfg(feature = "dir_source")]
121impl DirectorySourceOptions {
122 fn ignore_file(&self, name: &str) -> bool {
123 self.ignored_as_hidden_file(name) || self.ignored_as_temporary_file(name)
124 }
125
126 #[inline]
127 fn ignored_as_hidden_file(&self, name: &str) -> bool {
128 !self.hidden && name.starts_with('.')
129 }
130
131 #[inline]
132 fn ignored_as_temporary_file(&self, name: &str) -> bool {
133 !self.temporary && name.starts_with('#')
134 }
135}
136
137#[cfg(feature = "dir_source")]
138impl Default for DirectorySourceOptions {
139 fn default() -> Self {
140 DirectorySourceOptions {
141 tpl_extension: ".hbs".to_owned(),
142 hidden: false,
143 temporary: false,
144 }
145 }
146}
147
148impl<'reg> Registry<'reg> {
149 pub fn new() -> Registry<'reg> {
150 let r = Registry {
151 templates: HashMap::new(),
152 template_sources: HashMap::new(),
153 helpers: HashMap::new(),
154 decorators: HashMap::new(),
155 escape_fn: Arc::new(html_escape),
156 strict_mode: false,
157 dev_mode: false,
158 prevent_indent: false,
159 #[cfg(feature = "script_helper")]
160 engine: Arc::new(rhai_engine()),
161 #[cfg(feature = "script_helper")]
162 script_sources: HashMap::new(),
163 };
164
165 r.setup_builtins()
166 }
167
168 fn setup_builtins(mut self) -> Registry<'reg> {
169 self.register_helper("if", Box::new(helpers::IF_HELPER));
170 self.register_helper("unless", Box::new(helpers::UNLESS_HELPER));
171 self.register_helper("each", Box::new(helpers::EACH_HELPER));
172 self.register_helper("with", Box::new(helpers::WITH_HELPER));
173 self.register_helper("lookup", Box::new(helpers::LOOKUP_HELPER));
174 self.register_helper("raw", Box::new(helpers::RAW_HELPER));
175 self.register_helper("log", Box::new(helpers::LOG_HELPER));
176
177 self.register_helper("eq", Box::new(helpers::helper_extras::eq));
178 self.register_helper("ne", Box::new(helpers::helper_extras::ne));
179 self.register_helper("gt", Box::new(helpers::helper_extras::gt));
180 self.register_helper("gte", Box::new(helpers::helper_extras::gte));
181 self.register_helper("lt", Box::new(helpers::helper_extras::lt));
182 self.register_helper("lte", Box::new(helpers::helper_extras::lte));
183 self.register_helper("and", Box::new(helpers::helper_extras::and));
184 self.register_helper("or", Box::new(helpers::helper_extras::or));
185 self.register_helper("not", Box::new(helpers::helper_extras::not));
186 self.register_helper("len", Box::new(helpers::helper_extras::len));
187
188 #[cfg(feature = "string_helpers")]
189 self.register_string_helpers();
190
191 self.register_decorator("inline", Box::new(decorators::INLINE_DECORATOR));
192 self
193 }
194
195 pub fn set_strict_mode(&mut self, enabled: bool) {
203 self.strict_mode = enabled;
204 }
205
206 pub fn strict_mode(&self) -> bool {
214 self.strict_mode
215 }
216
217 pub fn dev_mode(&self) -> bool {
222 self.dev_mode
223 }
224
225 pub fn set_dev_mode(&mut self, enabled: bool) {
233 self.dev_mode = enabled;
234
235 if !enabled {
237 self.template_sources.clear();
238 }
239 }
240
241 pub fn set_prevent_indent(&mut self, enable: bool) {
246 self.prevent_indent = enable;
247 }
248
249 pub fn prevent_indent(&self) -> bool {
251 self.prevent_indent
252 }
253
254 pub fn register_template(&mut self, name: &str, tpl: Template) {
263 self.templates.insert(name.to_string(), tpl);
264 }
265
266 pub fn register_template_string<S>(
270 &mut self,
271 name: &str,
272 tpl_str: S,
273 ) -> Result<(), TemplateError>
274 where
275 S: AsRef<str>,
276 {
277 let template = Template::compile2(
278 tpl_str.as_ref(),
279 TemplateOptions {
280 name: Some(name.to_owned()),
281 is_partial: false,
282 prevent_indent: self.prevent_indent,
283 },
284 )?;
285 self.register_template(name, template);
286 Ok(())
287 }
288
289 pub fn register_partial<S>(&mut self, name: &str, partial_str: S) -> Result<(), TemplateError>
294 where
295 S: AsRef<str>,
296 {
297 self.register_template_string(name, partial_str)
298 }
299
300 pub fn register_template_file<P>(
305 &mut self,
306 name: &str,
307 tpl_path: P,
308 ) -> Result<(), TemplateError>
309 where
310 P: AsRef<Path>,
311 {
312 let source = FileSource::new(tpl_path.as_ref().into());
313 let template_string = source
314 .load()
315 .map_err(|err| TemplateError::from((err, name.to_owned())))?;
316
317 self.register_template_string(name, template_string)?;
318 if self.dev_mode {
319 self.template_sources
320 .insert(name.to_owned(), Arc::new(source));
321 }
322
323 Ok(())
324 }
325
326 #[cfg(feature = "dir_source")]
343 #[cfg_attr(docsrs, doc(cfg(feature = "dir_source")))]
344 pub fn register_templates_directory<P>(
345 &mut self,
346 dir_path: P,
347 options: DirectorySourceOptions,
348 ) -> Result<(), TemplateError>
349 where
350 P: AsRef<Path>,
351 {
352 let dir_path = dir_path.as_ref();
353
354 let walker = WalkDir::new(dir_path);
355 let dir_iter = walker
356 .min_depth(1)
357 .into_iter()
358 .filter_map(|e| e.ok().map(|e| e.into_path()))
359 .filter(|tpl_path| {
361 tpl_path
362 .to_string_lossy()
363 .ends_with(options.tpl_extension.as_str())
364 })
365 .filter(|tpl_path| {
367 tpl_path
368 .file_stem()
369 .map(|stem| !options.ignore_file(&stem.to_string_lossy()))
370 .unwrap_or(false)
371 })
372 .filter_map(|tpl_path| {
373 tpl_path
374 .strip_prefix(dir_path)
375 .ok()
376 .map(|tpl_canonical_name| {
377 let tpl_name = tpl_canonical_name
378 .components()
379 .map(|component| component.as_os_str().to_string_lossy())
380 .collect::<Vec<_>>()
381 .join("/");
382
383 tpl_name
384 .strip_suffix(options.tpl_extension.as_str())
385 .map(|s| s.to_owned())
386 .unwrap_or(tpl_name)
387 })
388 .map(|tpl_canonical_name| (tpl_canonical_name, tpl_path))
389 });
390
391 for (tpl_canonical_name, tpl_path) in dir_iter {
392 self.register_template_file(&tpl_canonical_name, &tpl_path)?;
393 }
394
395 Ok(())
396 }
397
398 #[cfg(feature = "rust-embed")]
415 #[cfg_attr(docsrs, doc(cfg(feature = "rust-embed")))]
416 pub fn register_embed_templates<E>(&mut self) -> Result<(), TemplateError>
417 where
418 E: RustEmbed,
419 {
420 self.register_embed_templates_with_extension::<E>("")
421 }
422
423 #[cfg(feature = "rust-embed")]
442 #[cfg_attr(docsrs, doc(cfg(feature = "rust-embed")))]
443 pub fn register_embed_templates_with_extension<E>(
444 &mut self,
445 tpl_extension: &str,
446 ) -> Result<(), TemplateError>
447 where
448 E: RustEmbed,
449 {
450 for file_name in E::iter().filter(|x| x.ends_with(tpl_extension)) {
451 let tpl_name = file_name
452 .strip_suffix(tpl_extension)
453 .unwrap_or(&file_name)
454 .to_owned();
455 let source = LazySource::new(move || {
456 E::get(&file_name)
457 .map(|file| file.data.to_vec())
458 .and_then(|data| String::from_utf8(data).ok())
459 });
460 let tpl_content = source
461 .load()
462 .map_err(|e| (e, "Template load error".to_owned()))?;
463 self.register_template_string(&tpl_name, &tpl_content)?;
464
465 if self.dev_mode {
466 self.template_sources.insert(tpl_name, Arc::new(source));
467 }
468 }
469 Ok(())
470 }
471
472 pub fn unregister_template(&mut self, name: &str) {
474 self.templates.remove(name);
475 self.template_sources.remove(name);
476 }
477
478 pub fn register_helper(&mut self, name: &str, def: Box<dyn HelperDef + Send + Sync + 'reg>) {
480 self.helpers.insert(name.to_string(), def.into());
481 }
482
483 #[cfg(feature = "script_helper")]
504 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
505 pub fn register_script_helper(&mut self, name: &str, script: &str) -> Result<(), ScriptError> {
506 let compiled = self.engine.compile(script)?;
507 let script_helper = ScriptHelper { script: compiled };
508 self.helpers
509 .insert(name.to_string(), Arc::new(script_helper));
510 Ok(())
511 }
512
513 #[cfg(feature = "script_helper")]
518 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
519 pub fn register_script_helper_file<P>(
520 &mut self,
521 name: &str,
522 script_path: P,
523 ) -> Result<(), ScriptError>
524 where
525 P: AsRef<Path>,
526 {
527 let source = FileSource::new(script_path.as_ref().into());
528 let script = source.load()?;
529
530 self.script_sources
531 .insert(name.to_owned(), Arc::new(source));
532 self.register_script_helper(name, &script)
533 }
534
535 #[cfg(feature = "script_helper")]
537 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
538 pub fn engine(&self) -> &Engine {
539 self.engine.as_ref()
540 }
541
542 #[cfg(feature = "script_helper")]
546 #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))]
547 pub fn set_engine(&mut self, engine: Engine) {
548 self.engine = Arc::new(engine);
549 }
550
551 pub fn register_decorator(
553 &mut self,
554 name: &str,
555 def: Box<dyn DecoratorDef + Send + Sync + 'reg>,
556 ) {
557 self.decorators.insert(name.to_string(), def.into());
558 }
559
560 pub fn register_escape_fn<F: 'static + Fn(&str) -> String + Send + Sync>(
562 &mut self,
563 escape_fn: F,
564 ) {
565 self.escape_fn = Arc::new(escape_fn);
566 }
567
568 pub fn unregister_escape_fn(&mut self) {
570 self.escape_fn = Arc::new(html_escape);
571 }
572
573 pub fn get_escape_fn(&self) -> &dyn Fn(&str) -> String {
575 self.escape_fn.as_ref()
576 }
577
578 pub fn has_template(&self, name: &str) -> bool {
580 self.get_template(name).is_some()
581 }
582
583 pub fn get_template(&self, name: &str) -> Option<&Template> {
585 self.templates.get(name)
586 }
587
588 #[inline]
589 pub(crate) fn get_or_load_template_optional(
590 &'reg self,
591 name: &str,
592 ) -> Option<Result<Cow<'reg, Template>, RenderError>> {
593 if let (true, Some(source)) = (self.dev_mode, self.template_sources.get(name)) {
594 let r = source
595 .load()
596 .map_err(|e| TemplateError::from((e, name.to_owned())))
597 .and_then(|tpl_str| {
598 Template::compile2(
599 tpl_str.as_ref(),
600 TemplateOptions {
601 name: Some(name.to_owned()),
602 prevent_indent: self.prevent_indent,
603 is_partial: false,
604 },
605 )
606 })
607 .map(Cow::Owned)
608 .map_err(RenderError::from);
609 Some(r)
610 } else {
611 self.templates.get(name).map(|t| Ok(Cow::Borrowed(t)))
612 }
613 }
614
615 #[inline]
616 pub(crate) fn get_or_load_template(
617 &'reg self,
618 name: &str,
619 ) -> Result<Cow<'reg, Template>, RenderError> {
620 if let Some(result) = self.get_or_load_template_optional(name) {
621 result
622 } else {
623 Err(RenderErrorReason::TemplateNotFound(name.to_owned()).into())
624 }
625 }
626
627 #[inline]
629 pub(crate) fn get_or_load_helper(
630 &'reg self,
631 name: &str,
632 ) -> Result<Option<Arc<dyn HelperDef + Send + Sync + 'reg>>, RenderError> {
633 #[cfg(feature = "script_helper")]
634 if let (true, Some(source)) = (self.dev_mode, self.script_sources.get(name)) {
635 return source
636 .load()
637 .map_err(ScriptError::from)
638 .and_then(|s| {
639 let helper = Box::new(ScriptHelper {
640 script: self.engine.compile(s)?,
641 }) as Box<dyn HelperDef + Send + Sync>;
642 Ok(Some(helper.into()))
643 })
644 .map_err(|e| RenderError::from(RenderErrorReason::from(e)));
645 }
646
647 Ok(self.helpers.get(name).cloned())
648 }
649
650 #[inline]
651 pub(crate) fn has_helper(&self, name: &str) -> bool {
652 self.helpers.contains_key(name)
653 }
654
655 #[inline]
657 pub(crate) fn get_decorator(
658 &self,
659 name: &str,
660 ) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> {
661 self.decorators.get(name).map(AsRef::as_ref)
662 }
663
664 pub fn get_templates(&self) -> &HashMap<String, Template> {
670 &self.templates
671 }
672
673 pub fn clear_templates(&mut self) {
675 self.templates.clear();
676 self.template_sources.clear();
677 }
678
679 fn gather_dev_mode_templates(
680 &'reg self,
681 prebound: Option<(&str, Cow<'reg, Template>)>,
682 ) -> Result<BTreeMap<String, Cow<'reg, Template>>, RenderError> {
683 let prebound_name = prebound.as_ref().map(|(name, _)| *name);
684 let mut res = BTreeMap::new();
685 for name in self.template_sources.keys() {
686 if Some(&**name) == prebound_name {
687 continue;
688 }
689 res.insert(name.clone(), self.get_or_load_template(name)?);
690 }
691 if let Some((name, prebound)) = prebound {
692 res.insert(name.to_owned(), prebound);
693 }
694 Ok(res)
695 }
696
697 fn render_resolved_template_to_output(
698 &self,
699 name: Option<&str>,
700 template: Cow<'_, Template>,
701 ctx: &Context,
702 output: &mut impl Output,
703 ) -> Result<(), RenderError> {
704 if !self.dev_mode {
705 let mut render_context = RenderContext::new(template.name.as_ref());
706 return template.render(self, ctx, &mut render_context, output);
707 }
708
709 let dev_mode_templates;
710 let template = if let Some(name) = name {
711 dev_mode_templates = self.gather_dev_mode_templates(Some((name, template)))?;
712 &dev_mode_templates[name]
713 } else {
714 dev_mode_templates = self.gather_dev_mode_templates(None)?;
715 &template
716 };
717
718 let mut render_context = RenderContext::new(template.name.as_ref());
719
720 render_context.set_dev_mode_templates(Some(&dev_mode_templates));
721
722 template.render(self, ctx, &mut render_context, output)
723 }
724
725 #[inline]
726 fn render_to_output<O>(
727 &self,
728 name: &str,
729 ctx: &Context,
730 output: &mut O,
731 ) -> Result<(), RenderError>
732 where
733 O: Output,
734 {
735 self.render_resolved_template_to_output(
736 Some(name),
737 self.get_or_load_template(name)?,
738 ctx,
739 output,
740 )
741 }
742
743 pub fn render<T>(&self, name: &str, data: &T) -> Result<String, RenderError>
750 where
751 T: Serialize,
752 {
753 let mut output = StringOutput::new();
754 let ctx = Context::wraps(data)?;
755 self.render_to_output(name, &ctx, &mut output)?;
756 output.into_string().map_err(RenderError::from)
757 }
758
759 pub fn render_with_context(&self, name: &str, ctx: &Context) -> Result<String, RenderError> {
761 let mut output = StringOutput::new();
762 self.render_to_output(name, ctx, &mut output)?;
763 output.into_string().map_err(RenderError::from)
764 }
765
766 pub fn render_to_write<T, W>(&self, name: &str, data: &T, writer: W) -> Result<(), RenderError>
768 where
769 T: Serialize,
770 W: Write,
771 {
772 let mut output = WriteOutput::new(writer);
773 let ctx = Context::wraps(data)?;
774 self.render_to_output(name, &ctx, &mut output)
775 }
776
777 pub fn render_with_context_to_write<W>(
780 &self,
781 name: &str,
782 ctx: &Context,
783 writer: W,
784 ) -> Result<(), RenderError>
785 where
786 W: Write,
787 {
788 let mut output = WriteOutput::new(writer);
789 self.render_to_output(name, ctx, &mut output)
790 }
791
792 pub fn render_template<T>(&self, template_string: &str, data: &T) -> Result<String, RenderError>
794 where
795 T: Serialize,
796 {
797 let mut writer = StringWriter::new();
798 self.render_template_to_write(template_string, data, &mut writer)?;
799 Ok(writer.into_string())
800 }
801
802 pub fn render_template_with_context(
804 &self,
805 template_string: &str,
806 ctx: &Context,
807 ) -> Result<String, RenderError> {
808 let tpl = Template::compile2(
809 template_string,
810 TemplateOptions {
811 prevent_indent: self.prevent_indent,
812 ..Default::default()
813 },
814 )
815 .map_err(RenderError::from)?;
816
817 let mut out = StringOutput::new();
818 self.render_resolved_template_to_output(None, Cow::Owned(tpl), ctx, &mut out)?;
819
820 out.into_string().map_err(RenderError::from)
821 }
822
823 pub fn render_template_with_context_to_write<W>(
826 &self,
827 template_string: &str,
828 ctx: &Context,
829 writer: W,
830 ) -> Result<(), RenderError>
831 where
832 W: Write,
833 {
834 let tpl = Template::compile2(
835 template_string,
836 TemplateOptions {
837 prevent_indent: self.prevent_indent,
838 ..Default::default()
839 },
840 )
841 .map_err(RenderError::from)?;
842 let mut out = WriteOutput::new(writer);
843
844 self.render_resolved_template_to_output(None, Cow::Owned(tpl), ctx, &mut out)
845 }
846
847 pub fn render_template_to_write<T, W>(
849 &self,
850 template_string: &str,
851 data: &T,
852 writer: W,
853 ) -> Result<(), RenderError>
854 where
855 T: Serialize,
856 W: Write,
857 {
858 let ctx = Context::wraps(data)?;
859 self.render_template_with_context_to_write(template_string, &ctx, writer)
860 }
861
862 #[cfg(feature = "string_helpers")]
863 #[inline]
864 fn register_string_helpers(&mut self) {
865 use helpers::string_helpers::{
866 kebab_case, lower_camel_case, shouty_kebab_case, shouty_snake_case, snake_case,
867 title_case, train_case, upper_camel_case,
868 };
869
870 self.register_helper("lowerCamelCase", Box::new(lower_camel_case));
871 self.register_helper("upperCamelCase", Box::new(upper_camel_case));
872 self.register_helper("snakeCase", Box::new(snake_case));
873 self.register_helper("kebabCase", Box::new(kebab_case));
874 self.register_helper("shoutySnakeCase", Box::new(shouty_snake_case));
875 self.register_helper("shoutyKebabCase", Box::new(shouty_kebab_case));
876 self.register_helper("titleCase", Box::new(title_case));
877 self.register_helper("trainCase", Box::new(train_case));
878 }
879}
880
881#[cfg(test)]
882mod test {
883 use crate::context::Context;
884 use crate::error::{RenderError, RenderErrorReason};
885 use crate::helpers::HelperDef;
886 use crate::output::Output;
887 use crate::registry::Registry;
888 use crate::render::{Helper, RenderContext, Renderable};
889 use crate::support::str::StringWriter;
890 use crate::template::Template;
891 use std::fs::File;
892 use std::io::Write;
893 use tempfile::tempdir;
894
895 #[derive(Clone, Copy)]
896 struct DummyHelper;
897
898 impl HelperDef for DummyHelper {
899 fn call<'reg: 'rc, 'rc>(
900 &self,
901 h: &Helper<'rc>,
902 r: &'reg Registry<'reg>,
903 ctx: &'rc Context,
904 rc: &mut RenderContext<'reg, 'rc>,
905 out: &mut dyn Output,
906 ) -> Result<(), RenderError> {
907 h.template().unwrap().render(r, ctx, rc, out)
908 }
909 }
910
911 static DUMMY_HELPER: DummyHelper = DummyHelper;
912
913 #[test]
914 fn test_registry_operations() {
915 let mut r = Registry::new();
916
917 assert!(r.register_template_string("index", "<h1></h1>").is_ok());
918
919 let tpl = Template::compile("<h2></h2>").unwrap();
920 r.register_template("index2", tpl);
921
922 assert_eq!(r.templates.len(), 2);
923
924 r.unregister_template("index");
925 assert_eq!(r.templates.len(), 1);
926
927 r.clear_templates();
928 assert_eq!(r.templates.len(), 0);
929
930 r.register_helper("dummy", Box::new(DUMMY_HELPER));
931
932 let num_helpers = 7;
934 let num_boolean_helpers = 10; let num_custom_helpers = 1; #[cfg(feature = "string_helpers")]
937 let string_helpers = 8;
938 #[cfg(not(feature = "string_helpers"))]
939 let string_helpers = 0;
940 assert_eq!(
941 r.helpers.len(),
942 num_helpers + num_boolean_helpers + num_custom_helpers + string_helpers
943 );
944 }
945
946 #[test]
947 #[cfg(feature = "dir_source")]
948 fn test_register_templates_directory() {
949 use std::fs::DirBuilder;
950
951 use crate::registry::DirectorySourceOptions;
952
953 let mut r = Registry::new();
954 {
955 let dir = tempdir().unwrap();
956
957 assert_eq!(r.templates.len(), 0);
958
959 let file1_path = dir.path().join("t1.hbs");
960 let mut file1: File = File::create(file1_path).unwrap();
961 writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();
962
963 let file2_path = dir.path().join("t2.hbs");
964 let mut file2: File = File::create(file2_path).unwrap();
965 writeln!(file2, "<h1>Hola {{world}}!</h1>").unwrap();
966
967 let file3_path = dir.path().join("t3.hbs");
968 let mut file3: File = File::create(file3_path).unwrap();
969 writeln!(file3, "<h1>Hallo {{world}}!</h1>").unwrap();
970
971 let file4_path = dir.path().join(".t4.hbs");
972 let mut file4: File = File::create(file4_path).unwrap();
973 writeln!(file4, "<h1>Hallo {{world}}!</h1>").unwrap();
974
975 r.register_templates_directory(dir.path(), DirectorySourceOptions::default())
976 .unwrap();
977
978 assert_eq!(r.templates.len(), 3);
979 assert!(r.templates.contains_key("t1"));
980 assert!(r.templates.contains_key("t2"));
981 assert!(r.templates.contains_key("t3"));
982 assert!(!r.templates.contains_key("t4"));
983
984 drop(file1);
985 drop(file2);
986 drop(file3);
987
988 dir.close().unwrap();
989 }
990
991 {
992 let dir = tempdir().unwrap();
993
994 let file1_path = dir.path().join("t4.hbs");
995 let mut file1: File = File::create(file1_path).unwrap();
996 writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();
997
998 let file2_path = dir.path().join("t5.erb");
999 let mut file2: File = File::create(file2_path).unwrap();
1000 writeln!(file2, "<h1>Hello {{% world %}}!</h1>").unwrap();
1001
1002 let file3_path = dir.path().join("t6.html");
1003 let mut file3: File = File::create(file3_path).unwrap();
1004 writeln!(file3, "<h1>Hello world!</h1>").unwrap();
1005
1006 r.register_templates_directory(dir.path(), DirectorySourceOptions::default())
1007 .unwrap();
1008
1009 assert_eq!(r.templates.len(), 4);
1010 assert!(r.templates.contains_key("t4"));
1011
1012 drop(file1);
1013 drop(file2);
1014 drop(file3);
1015
1016 dir.close().unwrap();
1017 }
1018
1019 {
1020 let dir = tempdir().unwrap();
1021
1022 DirBuilder::new().create(dir.path().join("french")).unwrap();
1023 DirBuilder::new()
1024 .create(dir.path().join("portugese"))
1025 .unwrap();
1026 DirBuilder::new()
1027 .create(dir.path().join("italian"))
1028 .unwrap();
1029
1030 let file1_path = dir.path().join("french/t7.hbs");
1031 let mut file1: File = File::create(file1_path).unwrap();
1032 writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();
1033
1034 let file2_path = dir.path().join("portugese/t8.hbs");
1035 let mut file2: File = File::create(file2_path).unwrap();
1036 writeln!(file2, "<h1>Ola {{world}}!</h1>").unwrap();
1037
1038 let file3_path = dir.path().join("italian/t9.hbs");
1039 let mut file3: File = File::create(file3_path).unwrap();
1040 writeln!(file3, "<h1>Ciao {{world}}!</h1>").unwrap();
1041
1042 r.register_templates_directory(dir.path(), DirectorySourceOptions::default())
1043 .unwrap();
1044
1045 assert_eq!(r.templates.len(), 7);
1046 assert!(r.templates.contains_key("french/t7"));
1047 assert!(r.templates.contains_key("portugese/t8"));
1048 assert!(r.templates.contains_key("italian/t9"));
1049
1050 drop(file1);
1051 drop(file2);
1052 drop(file3);
1053
1054 dir.close().unwrap();
1055 }
1056
1057 {
1058 let dir = tempdir().unwrap();
1059
1060 let file1_path = dir.path().join("t10.hbs");
1061 let mut file1: File = File::create(file1_path).unwrap();
1062 writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();
1063
1064 let mut dir_path = dir
1065 .path()
1066 .to_string_lossy()
1067 .replace(std::path::MAIN_SEPARATOR, "/");
1068 if !dir_path.ends_with('/') {
1069 dir_path.push('/');
1070 }
1071 r.register_templates_directory(dir_path, DirectorySourceOptions::default())
1072 .unwrap();
1073
1074 assert_eq!(r.templates.len(), 8);
1075 assert!(r.templates.contains_key("t10"));
1076
1077 drop(file1);
1078 dir.close().unwrap();
1079 }
1080
1081 {
1082 let dir = tempdir().unwrap();
1083 let mut r = Registry::new();
1084
1085 let file1_path = dir.path().join("t11.hbs.html");
1086 let mut file1: File = File::create(file1_path).unwrap();
1087 writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();
1088
1089 let mut dir_path = dir
1090 .path()
1091 .to_string_lossy()
1092 .replace(std::path::MAIN_SEPARATOR, "/");
1093 if !dir_path.ends_with('/') {
1094 dir_path.push('/');
1095 }
1096 r.register_templates_directory(
1097 dir_path,
1098 DirectorySourceOptions {
1099 tpl_extension: ".hbs.html".to_owned(),
1100 ..Default::default()
1101 },
1102 )
1103 .unwrap();
1104
1105 assert_eq!(r.templates.len(), 1);
1106 assert!(r.templates.contains_key("t11"));
1107
1108 drop(file1);
1109 dir.close().unwrap();
1110 }
1111
1112 {
1113 let dir = tempdir().unwrap();
1114 let mut r = Registry::new();
1115
1116 assert_eq!(r.templates.len(), 0);
1117
1118 let file1_path = dir.path().join(".t12.hbs");
1119 let mut file1: File = File::create(file1_path).unwrap();
1120 writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();
1121
1122 r.register_templates_directory(
1123 dir.path(),
1124 DirectorySourceOptions {
1125 hidden: true,
1126 ..Default::default()
1127 },
1128 )
1129 .unwrap();
1130
1131 assert_eq!(r.templates.len(), 1);
1132 assert!(r.templates.contains_key(".t12"));
1133
1134 drop(file1);
1135
1136 dir.close().unwrap();
1137 }
1138 }
1139
1140 #[test]
1141 fn test_render_to_write() {
1142 let mut r = Registry::new();
1143
1144 assert!(r.register_template_string("index", "<h1></h1>").is_ok());
1145
1146 let mut sw = StringWriter::new();
1147 {
1148 r.render_to_write("index", &(), &mut sw).ok().unwrap();
1149 }
1150
1151 assert_eq!("<h1></h1>".to_string(), sw.into_string());
1152 }
1153
1154 #[test]
1155 fn test_escape_fn() {
1156 let mut r = Registry::new();
1157
1158 let input = String::from("\"<>&");
1159
1160 r.register_template_string("test", String::from("{{this}}"))
1161 .unwrap();
1162
1163 assert_eq!(""<>&", r.render("test", &input).unwrap());
1164
1165 r.register_escape_fn(|s| s.into());
1166
1167 assert_eq!("\"<>&", r.render("test", &input).unwrap());
1168
1169 r.unregister_escape_fn();
1170
1171 assert_eq!(""<>&", r.render("test", &input).unwrap());
1172 }
1173
1174 #[test]
1175 fn test_escape() {
1176 let r = Registry::new();
1177 let data = json!({"hello": "world"});
1178
1179 assert_eq!(
1180 "{{hello}}",
1181 r.render_template(r"\{{hello}}", &data).unwrap()
1182 );
1183
1184 assert_eq!(
1185 " {{hello}}",
1186 r.render_template(r" \{{hello}}", &data).unwrap()
1187 );
1188
1189 assert_eq!(r"\world", r.render_template(r"\\{{hello}}", &data).unwrap());
1190 }
1191
1192 #[test]
1193 fn test_strict_mode() {
1194 let mut r = Registry::new();
1195 assert!(!r.strict_mode());
1196
1197 r.set_strict_mode(true);
1198 assert!(r.strict_mode());
1199
1200 let data = json!({
1201 "the_only_key": "the_only_value"
1202 });
1203
1204 assert!(r
1205 .render_template("accessing the_only_key {{the_only_key}}", &data)
1206 .is_ok());
1207 assert!(r
1208 .render_template("accessing non-exists key {{the_key_never_exists}}", &data)
1209 .is_err());
1210
1211 let render_error = r
1212 .render_template("accessing non-exists key {{the_key_never_exists}}", &data)
1213 .unwrap_err();
1214 assert_eq!(render_error.column_no.unwrap(), 26);
1215 assert_eq!(
1216 match render_error.reason() {
1217 RenderErrorReason::MissingVariable(path) => path.as_ref().unwrap(),
1218 _ => unreachable!(),
1219 },
1220 "the_key_never_exists"
1221 );
1222
1223 let data2 = json!([1, 2, 3]);
1224 assert!(r
1225 .render_template("accessing valid array index {{this.[2]}}", &data2)
1226 .is_ok());
1227 assert!(r
1228 .render_template("accessing invalid array index {{this.[3]}}", &data2)
1229 .is_err());
1230 let render_error2 = r
1231 .render_template("accessing invalid array index {{this.[3]}}", &data2)
1232 .unwrap_err();
1233 assert_eq!(render_error2.column_no.unwrap(), 31);
1234 assert_eq!(
1235 match render_error2.reason() {
1236 RenderErrorReason::MissingVariable(path) => path.as_ref().unwrap(),
1237 _ => unreachable!(),
1238 },
1239 "this.[3]"
1240 );
1241 }
1242
1243 use crate::json::value::ScopedJson;
1244 struct GenMissingHelper;
1245 impl HelperDef for GenMissingHelper {
1246 fn call_inner<'reg: 'rc, 'rc>(
1247 &self,
1248 _: &Helper<'rc>,
1249 _: &'reg Registry<'reg>,
1250 _: &'rc Context,
1251 _: &mut RenderContext<'reg, 'rc>,
1252 ) -> Result<ScopedJson<'rc>, RenderError> {
1253 Ok(ScopedJson::Missing)
1254 }
1255 }
1256
1257 #[test]
1258 fn test_strict_mode_in_helper() {
1259 let mut r = Registry::new();
1260 r.set_strict_mode(true);
1261
1262 r.register_helper(
1263 "check_missing",
1264 Box::new(
1265 |h: &Helper<'_>,
1266 _: &Registry<'_>,
1267 _: &Context,
1268 _: &mut RenderContext<'_, '_>,
1269 _: &mut dyn Output|
1270 -> Result<(), RenderError> {
1271 let value = h.param(0).unwrap();
1272 assert!(value.is_value_missing());
1273 Ok(())
1274 },
1275 ),
1276 );
1277
1278 r.register_helper("generate_missing_value", Box::new(GenMissingHelper));
1279
1280 let data = json!({
1281 "the_key_we_have": "the_value_we_have"
1282 });
1283 assert!(r
1284 .render_template("accessing non-exists key {{the_key_we_dont_have}}", &data)
1285 .is_err());
1286 assert!(r
1287 .render_template(
1288 "accessing non-exists key from helper {{check_missing the_key_we_dont_have}}",
1289 &data
1290 )
1291 .is_ok());
1292 assert!(r
1293 .render_template(
1294 "accessing helper that generates missing value {{generate_missing_value}}",
1295 &data
1296 )
1297 .is_err());
1298 }
1299
1300 #[test]
1301 fn test_html_expression() {
1302 let reg = Registry::new();
1303 assert_eq!(
1304 reg.render_template("{{{ a }}}", &json!({"a": "<b>bold</b>"}))
1305 .unwrap(),
1306 "<b>bold</b>"
1307 );
1308 assert_eq!(
1309 reg.render_template("{{ &a }}", &json!({"a": "<b>bold</b>"}))
1310 .unwrap(),
1311 "<b>bold</b>"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_render_context() {
1317 let mut reg = Registry::new();
1318
1319 let data = json!([0, 1, 2, 3]);
1320
1321 assert_eq!(
1322 "0123",
1323 reg.render_template_with_context(
1324 "{{#each this}}{{this}}{{/each}}",
1325 &Context::wraps(&data).unwrap()
1326 )
1327 .unwrap()
1328 );
1329
1330 reg.register_template_string("t0", "{{#each this}}{{this}}{{/each}}")
1331 .unwrap();
1332 assert_eq!(
1333 "0123",
1334 reg.render_with_context("t0", &Context::from(data)).unwrap()
1335 );
1336 }
1337
1338 #[test]
1339 fn test_keys_starts_with_null() {
1340 env_logger::init();
1341 let reg = Registry::new();
1342 let data = json!({
1343 "optional": true,
1344 "is_null": true,
1345 "nullable": true,
1346 "null": true,
1347 "falsevalue": true,
1348 });
1349 assert_eq!(
1350 "optional: true --> true",
1351 reg.render_template(
1352 "optional: {{optional}} --> {{#if optional }}true{{else}}false{{/if}}",
1353 &data
1354 )
1355 .unwrap()
1356 );
1357 assert_eq!(
1358 "is_null: true --> true",
1359 reg.render_template(
1360 "is_null: {{is_null}} --> {{#if is_null }}true{{else}}false{{/if}}",
1361 &data
1362 )
1363 .unwrap()
1364 );
1365 assert_eq!(
1366 "nullable: true --> true",
1367 reg.render_template(
1368 "nullable: {{nullable}} --> {{#if nullable }}true{{else}}false{{/if}}",
1369 &data
1370 )
1371 .unwrap()
1372 );
1373 assert_eq!(
1374 "falsevalue: true --> true",
1375 reg.render_template(
1376 "falsevalue: {{falsevalue}} --> {{#if falsevalue }}true{{else}}false{{/if}}",
1377 &data
1378 )
1379 .unwrap()
1380 );
1381 assert_eq!(
1382 "null: true --> false",
1383 reg.render_template(
1384 "null: {{null}} --> {{#if null }}true{{else}}false{{/if}}",
1385 &data
1386 )
1387 .unwrap()
1388 );
1389 assert_eq!(
1390 "null: true --> true",
1391 reg.render_template(
1392 "null: {{null}} --> {{#if this.[null]}}true{{else}}false{{/if}}",
1393 &data
1394 )
1395 .unwrap()
1396 );
1397 }
1398
1399 #[test]
1400 fn test_dev_mode_template_reload() {
1401 let mut reg = Registry::new();
1402 reg.set_dev_mode(true);
1403 assert!(reg.dev_mode());
1404
1405 let dir = tempdir().unwrap();
1406 let file1_path = dir.path().join("t1.hbs");
1407 {
1408 let mut file1: File = File::create(&file1_path).unwrap();
1409 write!(file1, "<h1>Hello {{{{name}}}}!</h1>").unwrap();
1410 }
1411
1412 reg.register_template_file("t1", &file1_path).unwrap();
1413
1414 assert_eq!(
1415 reg.render("t1", &json!({"name": "Alex"})).unwrap(),
1416 "<h1>Hello Alex!</h1>"
1417 );
1418
1419 {
1420 let mut file1: File = File::create(&file1_path).unwrap();
1421 write!(file1, "<h1>Privet {{{{name}}}}!</h1>").unwrap();
1422 }
1423
1424 assert_eq!(
1425 reg.render("t1", &json!({"name": "Alex"})).unwrap(),
1426 "<h1>Privet Alex!</h1>"
1427 );
1428
1429 dir.close().unwrap();
1430 }
1431
1432 #[test]
1433 #[cfg(feature = "script_helper")]
1434 fn test_script_helper() {
1435 let mut reg = Registry::new();
1436
1437 reg.register_script_helper("acc", "params.reduce(|sum, x| x + sum, 0)")
1438 .unwrap();
1439
1440 assert_eq!(
1441 reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(),
1442 "10"
1443 );
1444 }
1445
1446 #[test]
1447 #[cfg(feature = "script_helper")]
1448 fn test_script_helper_dev_mode() {
1449 let mut reg = Registry::new();
1450 reg.set_dev_mode(true);
1451
1452 let dir = tempdir().unwrap();
1453 let file1_path = dir.path().join("acc.rhai");
1454 {
1455 let mut file1: File = File::create(&file1_path).unwrap();
1456 write!(file1, "params.reduce(|sum, x| x + sum, 0)").unwrap();
1457 }
1458
1459 reg.register_script_helper_file("acc", &file1_path).unwrap();
1460
1461 assert_eq!(
1462 reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(),
1463 "10"
1464 );
1465
1466 {
1467 let mut file1: File = File::create(&file1_path).unwrap();
1468 write!(file1, "params.reduce(|sum, x| x * sum, 1)").unwrap();
1469 }
1470
1471 assert_eq!(
1472 reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(),
1473 "24"
1474 );
1475
1476 dir.close().unwrap();
1477 }
1478
1479 #[test]
1480 #[cfg(feature = "script_helper")]
1481 fn test_engine_access() {
1482 use rhai::Engine;
1483
1484 let mut registry = Registry::new();
1485 let mut eng = Engine::new();
1486 eng.set_max_string_size(1000);
1487 registry.set_engine(eng);
1488
1489 assert_eq!(1000, registry.engine().max_string_size());
1490 }
1491}