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
36pub type EscapeFn = Arc<dyn Fn(&str) -> String + Send + Sync>;
42
43pub fn html_escape(data: &str) -> String {
46 str::escape_html(data)
47}
48
49pub fn no_escape(data: &str) -> String {
52 data.to_owned()
53}
54
55#[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#[cfg(feature = "dir_source")]
104pub struct DirectorySourceOptions {
105 pub tpl_extension: String,
107 pub hidden: bool,
109 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 pub fn set_strict_mode(&mut self, enabled: bool) {
196 self.strict_mode = enabled;
197 }
198
199 pub fn strict_mode(&self) -> bool {
207 self.strict_mode
208 }
209
210 pub fn dev_mode(&self) -> bool {
215 self.dev_mode
216 }
217
218 pub fn set_dev_mode(&mut self, enabled: bool) {
226 self.dev_mode = enabled;
227
228 if !enabled {
230 self.template_sources.clear();
231 }
232 }
233
234 pub fn set_prevent_indent(&mut self, enable: bool) {
239 self.prevent_indent = enable;
240 }
241
242 pub fn prevent_indent(&self) -> bool {
244 self.prevent_indent
245 }
246
247 pub fn register_template(&mut self, name: &str, tpl: Template) {
256 self.templates.insert(name.to_string(), tpl);
257 }
258
259 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 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 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 #[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 .filter(|tpl_path| {
353 tpl_path
354 .to_string_lossy()
355 .ends_with(options.tpl_extension.as_str())
356 })
357 .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 #[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 #[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 pub fn unregister_template(&mut self, name: &str) {
466 self.templates.remove(name);
467 self.template_sources.remove(name);
468 }
469
470 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 #[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 #[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 #[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 #[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 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 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 pub fn unregister_escape_fn(&mut self) {
562 self.escape_fn = Arc::new(html_escape);
563 }
564
565 pub fn get_escape_fn(&self) -> &dyn Fn(&str) -> String {
567 self.escape_fn.as_ref()
568 }
569
570 pub fn has_template(&self, name: &str) -> bool {
572 self.get_template(name).is_some()
573 }
574
575 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 #[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 #[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 pub fn get_templates(&self) -> &HashMap<String, Template> {
661 &self.templates
662 }
663
664 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 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 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 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 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 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 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 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 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 let num_helpers = 7;
880 let num_boolean_helpers = 10; let num_custom_helpers = 1; #[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!(""<>&", 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!(""<>&", 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}