use std::borrow::Cow; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::io::{Error as IoError, Write}; use std::path::Path; use std::sync::Arc; use serde::Serialize; use crate::context::Context; use crate::decorators::{self, DecoratorDef}; #[cfg(feature = "script_helper")] use crate::error::ScriptError; use crate::error::{RenderError, TemplateError}; use crate::helpers::{self, HelperDef}; use crate::output::{Output, StringOutput, WriteOutput}; use crate::render::{RenderContext, Renderable}; use crate::sources::{FileSource, Source}; use crate::support::str::{self, StringWriter}; use crate::template::{Template, TemplateOptions}; #[cfg(feature = "dir_source")] use walkdir::WalkDir; #[cfg(feature = "script_helper")] use rhai::Engine; #[cfg(feature = "script_helper")] use crate::helpers::scripting::ScriptHelper; #[cfg(feature = "rust-embed")] use rust_embed::RustEmbed; /// This type represents an *escape fn*, that is a function whose purpose it is /// to escape potentially problematic characters in a string. /// /// An *escape fn* is represented as a `Box` to avoid unnecessary type /// parameters (and because traits cannot be aliased using `type`). pub type EscapeFn = Arc String + Send + Sync>; /// The default *escape fn* replaces the characters `&"<>` /// with the equivalent html / xml entities. pub fn html_escape(data: &str) -> String { str::escape_html(data) } /// `EscapeFn` that does not change anything. Useful when using in a non-html /// environment. pub fn no_escape(data: &str) -> String { data.to_owned() } /// The single entry point of your Handlebars templates /// /// It maintains compiled templates and registered helpers. #[derive(Clone)] pub struct Registry<'reg> { templates: HashMap, helpers: HashMap>, decorators: HashMap>, escape_fn: EscapeFn, strict_mode: bool, dev_mode: bool, prevent_indent: bool, #[cfg(feature = "script_helper")] pub(crate) engine: Arc, template_sources: HashMap + Send + Sync + 'reg>>, #[cfg(feature = "script_helper")] script_sources: HashMap + Send + Sync + 'reg>>, } impl<'reg> Debug for Registry<'reg> { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { f.debug_struct("Handlebars") .field("templates", &self.templates) .field("helpers", &self.helpers.keys()) .field("decorators", &self.decorators.keys()) .field("strict_mode", &self.strict_mode) .field("dev_mode", &self.dev_mode) .finish() } } impl<'reg> Default for Registry<'reg> { fn default() -> Self { Self::new() } } #[cfg(feature = "script_helper")] fn rhai_engine() -> Engine { Engine::new() } impl<'reg> Registry<'reg> { pub fn new() -> Registry<'reg> { let r = Registry { templates: HashMap::new(), template_sources: HashMap::new(), helpers: HashMap::new(), decorators: HashMap::new(), escape_fn: Arc::new(html_escape), strict_mode: false, dev_mode: false, prevent_indent: false, #[cfg(feature = "script_helper")] engine: Arc::new(rhai_engine()), #[cfg(feature = "script_helper")] script_sources: HashMap::new(), }; r.setup_builtins() } fn setup_builtins(mut self) -> Registry<'reg> { self.register_helper("if", Box::new(helpers::IF_HELPER)); self.register_helper("unless", Box::new(helpers::UNLESS_HELPER)); self.register_helper("each", Box::new(helpers::EACH_HELPER)); self.register_helper("with", Box::new(helpers::WITH_HELPER)); self.register_helper("lookup", Box::new(helpers::LOOKUP_HELPER)); self.register_helper("raw", Box::new(helpers::RAW_HELPER)); self.register_helper("log", Box::new(helpers::LOG_HELPER)); self.register_helper("eq", Box::new(helpers::helper_extras::eq)); self.register_helper("ne", Box::new(helpers::helper_extras::ne)); self.register_helper("gt", Box::new(helpers::helper_extras::gt)); self.register_helper("gte", Box::new(helpers::helper_extras::gte)); self.register_helper("lt", Box::new(helpers::helper_extras::lt)); self.register_helper("lte", Box::new(helpers::helper_extras::lte)); self.register_helper("and", Box::new(helpers::helper_extras::and)); self.register_helper("or", Box::new(helpers::helper_extras::or)); self.register_helper("not", Box::new(helpers::helper_extras::not)); self.register_helper("len", Box::new(helpers::helper_extras::len)); self.register_decorator("inline", Box::new(decorators::INLINE_DECORATOR)); self } /// Enable or disable handlebars strict mode /// /// By default, handlebars renders empty string for value that /// undefined or never exists. Since rust is a static type /// language, we offer strict mode in handlebars-rust. In strict /// mode, if you were to render a value that doesn't exist, a /// `RenderError` will be raised. pub fn set_strict_mode(&mut self, enabled: bool) { self.strict_mode = enabled; } /// Return strict mode state, default is false. /// /// By default, handlebars renders empty string for value that /// undefined or never exists. Since rust is a static type /// language, we offer strict mode in handlebars-rust. In strict /// mode, if you were access a value that doesn't exist, a /// `RenderError` will be raised. pub fn strict_mode(&self) -> bool { self.strict_mode } /// Return dev mode state, default is false /// /// With dev mode turned on, handlebars enables a set of development /// friendly features, that may affect its performance. pub fn dev_mode(&self) -> bool { self.dev_mode } /// Enable or disable dev mode /// /// With dev mode turned on, handlebars enables a set of development /// friendly features, that may affect its performance. /// /// **Note that you have to enable dev mode before adding templates to /// the registry**. Otherwise it won't take effect at all. pub fn set_dev_mode(&mut self, enabled: bool) { self.dev_mode = enabled; // clear template source when disabling dev mode if !enabled { self.template_sources.clear(); } } /// Enable or disable indent for partial include tag `{{>}}` /// /// By default handlebars keeps indent whitespaces for partial /// include tag, to change this behaviour, set this toggle to `true`. pub fn set_prevent_indent(&mut self, enable: bool) { self.prevent_indent = enable; } /// Return state for `prevent_indent` option, default to `false`. pub fn prevent_indent(&self) -> bool { self.prevent_indent } /// Register a `Template` /// /// This is infallible since the template has already been parsed and /// insert cannot fail. If there is an existing template with this name it /// will be overwritten. /// /// Dev mode doesn't apply for pre-compiled template because it's lifecycle /// is not managed by the registry. pub fn register_template(&mut self, name: &str, tpl: Template) { self.templates.insert(name.to_string(), tpl); } /// Register a template string /// /// Returns `TemplateError` if there is syntax error on parsing the template. pub fn register_template_string( &mut self, name: &str, tpl_str: S, ) -> Result<(), TemplateError> where S: AsRef, { let template = Template::compile2( tpl_str.as_ref(), TemplateOptions { name: Some(name.to_owned()), prevent_indent: self.prevent_indent, }, )?; self.register_template(name, template); Ok(()) } /// Register a partial string /// /// A named partial will be added to the registry. It will overwrite template with /// same name. Currently a registered partial is just identical to a template. pub fn register_partial(&mut self, name: &str, partial_str: S) -> Result<(), TemplateError> where S: AsRef, { self.register_template_string(name, partial_str) } /// Register a template from a path on file system /// /// If dev mode is enabled, the registry will keep reading the template file /// from file system everytime it's visited. pub fn register_template_file

( &mut self, name: &str, tpl_path: P, ) -> Result<(), TemplateError> where P: AsRef, { let source = FileSource::new(tpl_path.as_ref().into()); let template_string = source .load() .map_err(|err| TemplateError::from((err, name.to_owned())))?; self.register_template_string(name, template_string)?; if self.dev_mode { self.template_sources .insert(name.to_owned(), Arc::new(source)); } Ok(()) } /// Register templates from a directory /// /// * `tpl_extension`: the template file extension /// * `dir_path`: the path of directory /// /// Hidden files and tempfile (starts with `#`) will be ignored. All registered /// will use their relative name as template name. For example, when `dir_path` is /// `templates/` and `tpl_extension` is `.hbs`, the file /// `templates/some/path/file.hbs` will be registered as `some/path/file`. /// /// This method is not available by default. /// You will need to enable the `dir_source` feature to use it. /// /// When dev_mode enabled, like `register_template_file`, templates is reloaded /// from file system everytime it's visied. #[cfg(feature = "dir_source")] #[cfg_attr(docsrs, doc(cfg(feature = "dir_source")))] pub fn register_templates_directory

( &mut self, tpl_extension: &str, dir_path: P, ) -> Result<(), TemplateError> where P: AsRef, { let dir_path = dir_path.as_ref(); // Allowing dots at the beginning as to not break old // applications. let tpl_extension = tpl_extension.strip_prefix('.').unwrap_or(tpl_extension); let walker = WalkDir::new(dir_path); let dir_iter = walker .min_depth(1) .into_iter() .filter_map(|e| e.ok().map(|e| e.into_path())) // Checks if extension matches .filter(|tpl_path| { tpl_path .extension() .map(|extension| extension == tpl_extension) .unwrap_or(false) }) // Rejects any hidden or temporary files. .filter(|tpl_path| { tpl_path .file_stem() .map(|stem| stem.to_string_lossy()) .map(|stem| !(stem.starts_with('.') || stem.starts_with('#'))) .unwrap_or(false) }) .filter_map(|tpl_path| { tpl_path .strip_prefix(dir_path) .ok() .map(|tpl_canonical_name| { tpl_canonical_name .with_extension("") .components() .map(|component| component.as_os_str().to_string_lossy()) .collect::>() .join("/") }) .map(|tpl_canonical_name| (tpl_canonical_name, tpl_path)) }); for (tpl_canonical_name, tpl_path) in dir_iter { self.register_template_file(&tpl_canonical_name, &tpl_path)?; } Ok(()) } /// Register templates using a /// [RustEmbed](https://github.com/pyros2097/rust-embed) type /// /// File names from embed struct are used as template name. /// /// ```skip /// #[derive(RustEmbed)] /// #[folder = "templates"] /// struct Assets; /// /// let mut hbs = Handlebars::new(); /// hbs.register_embed_templates::(); /// ``` /// #[cfg(feature = "rust-embed")] #[cfg_attr(docsrs, doc(cfg(feature = "rust-embed")))] pub fn register_embed_templates(&mut self) -> Result<(), TemplateError> where E: RustEmbed, { for item in E::iter() { let file_name = item.as_ref(); if let Some(file) = E::get(file_name) { let data = file.data; let tpl_content = String::from_utf8_lossy(data.as_ref()); self.register_template_string(file_name, tpl_content)?; } } Ok(()) } /// Remove a template from the registry pub fn unregister_template(&mut self, name: &str) { self.templates.remove(name); self.template_sources.remove(name); } /// Register a helper pub fn register_helper(&mut self, name: &str, def: Box) { self.helpers.insert(name.to_string(), def.into()); } /// Register a [rhai](https://docs.rs/rhai/) script as handlebars helper /// /// Currently only simple helpers are supported. You can do computation or /// string formatting with rhai script. /// /// Helper parameters and hash are available in rhai script as array `params` /// and map `hash`. Example script: /// /// ```handlebars /// {{percent 0.34 label="%"}} /// ``` /// /// ```rhai /// // percent.rhai /// let value = params[0]; /// let label = hash["label"]; /// /// (value * 100).to_string() + label /// ``` /// #[cfg(feature = "script_helper")] #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] pub fn register_script_helper(&mut self, name: &str, script: &str) -> Result<(), ScriptError> { let compiled = self.engine.compile(script)?; let script_helper = ScriptHelper { script: compiled }; self.helpers .insert(name.to_string(), Arc::new(script_helper)); Ok(()) } /// Register a [rhai](https://docs.rs/rhai/) script from file /// /// When dev mode is enable, script file is reloaded from original file /// everytime it is called. #[cfg(feature = "script_helper")] #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] pub fn register_script_helper_file

( &mut self, name: &str, script_path: P, ) -> Result<(), ScriptError> where P: AsRef, { let source = FileSource::new(script_path.as_ref().into()); let script = source.load()?; self.script_sources .insert(name.to_owned(), Arc::new(source)); self.register_script_helper(name, &script) } /// Borrow a read-only reference to current rhai engine #[cfg(feature = "script_helper")] #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] pub fn engine(&self) -> &Engine { self.engine.as_ref() } /// Set a custom rhai engine for the registry. /// /// *Note that* you need to set custom engine before adding scripts. #[cfg(feature = "script_helper")] #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] pub fn set_engine(&mut self, engine: Engine) { self.engine = Arc::new(engine); } /// Register a decorator pub fn register_decorator( &mut self, name: &str, def: Box, ) { self.decorators.insert(name.to_string(), def.into()); } /// Register a new *escape fn* to be used from now on by this registry. pub fn register_escape_fn String + Send + Sync>( &mut self, escape_fn: F, ) { self.escape_fn = Arc::new(escape_fn); } /// Restore the default *escape fn*. pub fn unregister_escape_fn(&mut self) { self.escape_fn = Arc::new(html_escape); } /// Get a reference to the current *escape fn*. pub fn get_escape_fn(&self) -> &dyn Fn(&str) -> String { self.escape_fn.as_ref() } /// Return `true` if a template is registered for the given name pub fn has_template(&self, name: &str) -> bool { self.get_template(name).is_some() } /// Return a registered template, pub fn get_template(&self, name: &str) -> Option<&Template> { self.templates.get(name) } #[inline] pub(crate) fn get_or_load_template_optional( &'reg self, name: &str, ) -> Option, RenderError>> { if let (true, Some(source)) = (self.dev_mode, self.template_sources.get(name)) { let r = source .load() .map_err(|e| TemplateError::from((e, name.to_owned()))) .and_then(|tpl_str| { Template::compile2( tpl_str.as_ref(), TemplateOptions { name: Some(name.to_owned()), prevent_indent: self.prevent_indent, }, ) }) .map(Cow::Owned) .map_err(RenderError::from); Some(r) } else { self.templates.get(name).map(|t| Ok(Cow::Borrowed(t))) } } #[inline] pub(crate) fn get_or_load_template( &'reg self, name: &str, ) -> Result, RenderError> { if let Some(result) = self.get_or_load_template_optional(name) { result } else { Err(RenderError::new(format!("Template not found: {}", name))) } } /// Return a registered helper #[inline] pub(crate) fn get_or_load_helper( &'reg self, name: &str, ) -> Result>, RenderError> { #[cfg(feature = "script_helper")] if let (true, Some(source)) = (self.dev_mode, self.script_sources.get(name)) { return source .load() .map_err(ScriptError::from) .and_then(|s| { let helper = Box::new(ScriptHelper { script: self.engine.compile(&s)?, }) as Box; Ok(Some(helper.into())) }) .map_err(RenderError::from); } Ok(self.helpers.get(name).cloned()) } #[inline] pub(crate) fn has_helper(&self, name: &str) -> bool { self.helpers.contains_key(name) } /// Return a registered decorator #[inline] pub(crate) fn get_decorator( &self, name: &str, ) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> { self.decorators.get(name).map(|v| v.as_ref()) } /// Return all templates registered /// /// **Note that** in dev mode, the template returned from this method may /// not reflect its latest state. This method doesn't try to reload templates /// from its source. pub fn get_templates(&self) -> &HashMap { &self.templates } /// Unregister all templates pub fn clear_templates(&mut self) { self.templates.clear(); self.template_sources.clear(); } #[inline] fn render_to_output( &self, name: &str, ctx: &Context, output: &mut O, ) -> Result<(), RenderError> where O: Output, { self.get_or_load_template(name).and_then(|t| { let mut render_context = RenderContext::new(t.name.as_ref()); t.render(self, ctx, &mut render_context, output) }) } /// Render a registered template with some data into a string /// /// * `name` is the template name you registered previously /// * `data` is the data that implements `serde::Serialize` /// /// Returns rendered string or a struct with error information pub fn render(&self, name: &str, data: &T) -> Result where T: Serialize, { let mut output = StringOutput::new(); let ctx = Context::wraps(&data)?; self.render_to_output(name, &ctx, &mut output)?; output.into_string().map_err(RenderError::from) } /// Render a registered template with reused context pub fn render_with_context(&self, name: &str, ctx: &Context) -> Result { let mut output = StringOutput::new(); self.render_to_output(name, ctx, &mut output)?; output.into_string().map_err(RenderError::from) } /// Render a registered template and write data to the `std::io::Write` pub fn render_to_write(&self, name: &str, data: &T, writer: W) -> Result<(), RenderError> where T: Serialize, W: Write, { let mut output = WriteOutput::new(writer); let ctx = Context::wraps(data)?; self.render_to_output(name, &ctx, &mut output) } /// Render a registered template using reusable `Context`, and write data to /// the `std::io::Write` pub fn render_with_context_to_write( &self, name: &str, ctx: &Context, writer: W, ) -> Result<(), RenderError> where W: Write, { let mut output = WriteOutput::new(writer); self.render_to_output(name, ctx, &mut output) } /// Render a template string using current registry without registering it pub fn render_template(&self, template_string: &str, data: &T) -> Result where T: Serialize, { let mut writer = StringWriter::new(); self.render_template_to_write(template_string, data, &mut writer)?; Ok(writer.into_string()) } /// Render a template string using reusable context data pub fn render_template_with_context( &self, template_string: &str, ctx: &Context, ) -> Result { let tpl = Template::compile2( template_string, TemplateOptions { prevent_indent: self.prevent_indent, ..Default::default() }, )?; let mut out = StringOutput::new(); { let mut render_context = RenderContext::new(None); tpl.render(self, ctx, &mut render_context, &mut out)?; } out.into_string().map_err(RenderError::from) } /// Render a template string using resuable context, and write data into /// `std::io::Write` pub fn render_template_with_context_to_write( &self, template_string: &str, ctx: &Context, writer: W, ) -> Result<(), RenderError> where W: Write, { let tpl = Template::compile2( template_string, TemplateOptions { prevent_indent: self.prevent_indent, ..Default::default() }, )?; let mut render_context = RenderContext::new(None); let mut out = WriteOutput::new(writer); tpl.render(self, ctx, &mut render_context, &mut out) } /// Render a template string using current registry without registering it pub fn render_template_to_write( &self, template_string: &str, data: &T, writer: W, ) -> Result<(), RenderError> where T: Serialize, W: Write, { let ctx = Context::wraps(data)?; self.render_template_with_context_to_write(template_string, &ctx, writer) } } #[cfg(test)] mod test { use crate::context::Context; use crate::error::RenderError; use crate::helpers::HelperDef; use crate::output::Output; use crate::registry::Registry; use crate::render::{Helper, RenderContext, Renderable}; use crate::support::str::StringWriter; use crate::template::Template; use std::fs::File; use std::io::Write; use tempfile::tempdir; #[derive(Clone, Copy)] struct DummyHelper; impl HelperDef for DummyHelper { fn call<'reg: 'rc, 'rc>( &self, h: &Helper<'reg, 'rc>, r: &'reg Registry<'reg>, ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output, ) -> Result<(), RenderError> { h.template().unwrap().render(r, ctx, rc, out) } } static DUMMY_HELPER: DummyHelper = DummyHelper; #[test] fn test_registry_operations() { let mut r = Registry::new(); assert!(r.register_template_string("index", "

").is_ok()); let tpl = Template::compile("

").unwrap(); r.register_template("index2", tpl); assert_eq!(r.templates.len(), 2); r.unregister_template("index"); assert_eq!(r.templates.len(), 1); r.clear_templates(); assert_eq!(r.templates.len(), 0); r.register_helper("dummy", Box::new(DUMMY_HELPER)); // built-in helpers plus 1 let num_helpers = 7; let num_boolean_helpers = 10; // stuff like gt and lte let num_custom_helpers = 1; // dummy from above assert_eq!( r.helpers.len(), num_helpers + num_boolean_helpers + num_custom_helpers ); } #[test] #[cfg(feature = "dir_source")] fn test_register_templates_directory() { use std::fs::DirBuilder; let mut r = Registry::new(); { let dir = tempdir().unwrap(); assert_eq!(r.templates.len(), 0); let file1_path = dir.path().join("t1.hbs"); let mut file1: File = File::create(&file1_path).unwrap(); writeln!(file1, "

Hello {{world}}!

").unwrap(); let file2_path = dir.path().join("t2.hbs"); let mut file2: File = File::create(&file2_path).unwrap(); writeln!(file2, "

Hola {{world}}!

").unwrap(); let file3_path = dir.path().join("t3.hbs"); let mut file3: File = File::create(&file3_path).unwrap(); writeln!(file3, "

Hallo {{world}}!

").unwrap(); let file4_path = dir.path().join(".t4.hbs"); let mut file4: File = File::create(&file4_path).unwrap(); writeln!(file4, "

Hallo {{world}}!

").unwrap(); r.register_templates_directory(".hbs", dir.path()).unwrap(); assert_eq!(r.templates.len(), 3); assert_eq!(r.templates.contains_key("t1"), true); assert_eq!(r.templates.contains_key("t2"), true); assert_eq!(r.templates.contains_key("t3"), true); assert_eq!(r.templates.contains_key("t4"), false); drop(file1); drop(file2); drop(file3); dir.close().unwrap(); } { let dir = tempdir().unwrap(); let file1_path = dir.path().join("t4.hbs"); let mut file1: File = File::create(&file1_path).unwrap(); writeln!(file1, "

Hello {{world}}!

").unwrap(); let file2_path = dir.path().join("t5.erb"); let mut file2: File = File::create(&file2_path).unwrap(); writeln!(file2, "

Hello {{% world %}}!

").unwrap(); let file3_path = dir.path().join("t6.html"); let mut file3: File = File::create(&file3_path).unwrap(); writeln!(file3, "

Hello world!

").unwrap(); r.register_templates_directory(".hbs", dir.path()).unwrap(); assert_eq!(r.templates.len(), 4); assert_eq!(r.templates.contains_key("t4"), true); drop(file1); drop(file2); drop(file3); dir.close().unwrap(); } { let dir = tempdir().unwrap(); let _ = DirBuilder::new().create(dir.path().join("french")).unwrap(); let _ = DirBuilder::new() .create(dir.path().join("portugese")) .unwrap(); let _ = DirBuilder::new() .create(dir.path().join("italian")) .unwrap(); let file1_path = dir.path().join("french/t7.hbs"); let mut file1: File = File::create(&file1_path).unwrap(); writeln!(file1, "

Bonjour {{world}}!

").unwrap(); let file2_path = dir.path().join("portugese/t8.hbs"); let mut file2: File = File::create(&file2_path).unwrap(); writeln!(file2, "

Ola {{world}}!

").unwrap(); let file3_path = dir.path().join("italian/t9.hbs"); let mut file3: File = File::create(&file3_path).unwrap(); writeln!(file3, "

Ciao {{world}}!

").unwrap(); r.register_templates_directory(".hbs", dir.path()).unwrap(); assert_eq!(r.templates.len(), 7); assert_eq!(r.templates.contains_key("french/t7"), true); assert_eq!(r.templates.contains_key("portugese/t8"), true); assert_eq!(r.templates.contains_key("italian/t9"), true); drop(file1); drop(file2); drop(file3); dir.close().unwrap(); } { let dir = tempdir().unwrap(); let file1_path = dir.path().join("t10.hbs"); let mut file1: File = File::create(&file1_path).unwrap(); writeln!(file1, "

Bonjour {{world}}!

").unwrap(); let mut dir_path = dir .path() .to_string_lossy() .replace(std::path::MAIN_SEPARATOR, "/"); if !dir_path.ends_with("/") { dir_path.push('/'); } r.register_templates_directory(".hbs", dir_path).unwrap(); assert_eq!(r.templates.len(), 8); assert_eq!(r.templates.contains_key("t10"), true); drop(file1); dir.close().unwrap(); } } #[test] fn test_render_to_write() { let mut r = Registry::new(); assert!(r.register_template_string("index", "

").is_ok()); let mut sw = StringWriter::new(); { r.render_to_write("index", &(), &mut sw).ok().unwrap(); } assert_eq!("

".to_string(), sw.into_string()); } #[test] fn test_escape_fn() { let mut r = Registry::new(); let input = String::from("\"<>&"); r.register_template_string("test", String::from("{{this}}")) .unwrap(); assert_eq!(""<>&", r.render("test", &input).unwrap()); r.register_escape_fn(|s| s.into()); assert_eq!("\"<>&", r.render("test", &input).unwrap()); r.unregister_escape_fn(); assert_eq!(""<>&", r.render("test", &input).unwrap()); } #[test] fn test_escape() { let r = Registry::new(); let data = json!({"hello": "world"}); assert_eq!( "{{hello}}", r.render_template(r"\{{hello}}", &data).unwrap() ); assert_eq!( " {{hello}}", r.render_template(r" \{{hello}}", &data).unwrap() ); assert_eq!(r"\world", r.render_template(r"\\{{hello}}", &data).unwrap()); } #[test] fn test_strict_mode() { let mut r = Registry::new(); assert!(!r.strict_mode()); r.set_strict_mode(true); assert!(r.strict_mode()); let data = json!({ "the_only_key": "the_only_value" }); assert!(r .render_template("accessing the_only_key {{the_only_key}}", &data) .is_ok()); assert!(r .render_template("accessing non-exists key {{the_key_never_exists}}", &data) .is_err()); let render_error = r .render_template("accessing non-exists key {{the_key_never_exists}}", &data) .unwrap_err(); assert_eq!(render_error.column_no.unwrap(), 26); let data2 = json!([1, 2, 3]); assert!(r .render_template("accessing valid array index {{this.[2]}}", &data2) .is_ok()); assert!(r .render_template("accessing invalid array index {{this.[3]}}", &data2) .is_err()); let render_error2 = r .render_template("accessing invalid array index {{this.[3]}}", &data2) .unwrap_err(); assert_eq!(render_error2.column_no.unwrap(), 31); } use crate::json::value::ScopedJson; struct GenMissingHelper; impl HelperDef for GenMissingHelper { fn call_inner<'reg: 'rc, 'rc>( &self, _: &Helper<'reg, 'rc>, _: &'reg Registry<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, ) -> Result, RenderError> { Ok(ScopedJson::Missing) } } #[test] fn test_strict_mode_in_helper() { let mut r = Registry::new(); r.set_strict_mode(true); r.register_helper( "check_missing", Box::new( |h: &Helper<'_, '_>, _: &Registry<'_>, _: &Context, _: &mut RenderContext<'_, '_>, _: &mut dyn Output| -> Result<(), RenderError> { let value = h.param(0).unwrap(); assert!(value.is_value_missing()); Ok(()) }, ), ); r.register_helper("generate_missing_value", Box::new(GenMissingHelper)); let data = json!({ "the_key_we_have": "the_value_we_have" }); assert!(r .render_template("accessing non-exists key {{the_key_we_dont_have}}", &data) .is_err()); assert!(r .render_template( "accessing non-exists key from helper {{check_missing the_key_we_dont_have}}", &data ) .is_ok()); assert!(r .render_template( "accessing helper that generates missing value {{generate_missing_value}}", &data ) .is_err()); } #[test] fn test_html_expression() { let reg = Registry::new(); assert_eq!( reg.render_template("{{{ a }}}", &json!({"a": "bold"})) .unwrap(), "bold" ); assert_eq!( reg.render_template("{{ &a }}", &json!({"a": "bold"})) .unwrap(), "bold" ); } #[test] fn test_render_context() { let mut reg = Registry::new(); let data = json!([0, 1, 2, 3]); assert_eq!( "0123", reg.render_template_with_context( "{{#each this}}{{this}}{{/each}}", &Context::wraps(&data).unwrap() ) .unwrap() ); reg.register_template_string("t0", "{{#each this}}{{this}}{{/each}}") .unwrap(); assert_eq!( "0123", reg.render_with_context("t0", &Context::from(data)).unwrap() ); } #[test] fn test_keys_starts_with_null() { env_logger::init(); let reg = Registry::new(); let data = json!({ "optional": true, "is_null": true, "nullable": true, "null": true, "falsevalue": true, }); assert_eq!( "optional: true --> true", reg.render_template( "optional: {{optional}} --> {{#if optional }}true{{else}}false{{/if}}", &data ) .unwrap() ); assert_eq!( "is_null: true --> true", reg.render_template( "is_null: {{is_null}} --> {{#if is_null }}true{{else}}false{{/if}}", &data ) .unwrap() ); assert_eq!( "nullable: true --> true", reg.render_template( "nullable: {{nullable}} --> {{#if nullable }}true{{else}}false{{/if}}", &data ) .unwrap() ); assert_eq!( "falsevalue: true --> true", reg.render_template( "falsevalue: {{falsevalue}} --> {{#if falsevalue }}true{{else}}false{{/if}}", &data ) .unwrap() ); assert_eq!( "null: true --> false", reg.render_template( "null: {{null}} --> {{#if null }}true{{else}}false{{/if}}", &data ) .unwrap() ); assert_eq!( "null: true --> true", reg.render_template( "null: {{null}} --> {{#if this.[null]}}true{{else}}false{{/if}}", &data ) .unwrap() ); } #[test] fn test_dev_mode_template_reload() { let mut reg = Registry::new(); reg.set_dev_mode(true); assert!(reg.dev_mode()); let dir = tempdir().unwrap(); let file1_path = dir.path().join("t1.hbs"); { let mut file1: File = File::create(&file1_path).unwrap(); write!(file1, "

Hello {{{{name}}}}!

").unwrap(); } reg.register_template_file("t1", &file1_path).unwrap(); assert_eq!( reg.render("t1", &json!({"name": "Alex"})).unwrap(), "

Hello Alex!

" ); { let mut file1: File = File::create(&file1_path).unwrap(); write!(file1, "

Privet {{{{name}}}}!

").unwrap(); } assert_eq!( reg.render("t1", &json!({"name": "Alex"})).unwrap(), "

Privet Alex!

" ); dir.close().unwrap(); } #[test] #[cfg(feature = "script_helper")] fn test_script_helper() { let mut reg = Registry::new(); reg.register_script_helper("acc", "params.reduce(|sum, x| x + sum, 0)") .unwrap(); assert_eq!( reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), "10" ); } #[test] #[cfg(feature = "script_helper")] fn test_script_helper_dev_mode() { let mut reg = Registry::new(); reg.set_dev_mode(true); let dir = tempdir().unwrap(); let file1_path = dir.path().join("acc.rhai"); { let mut file1: File = File::create(&file1_path).unwrap(); write!(file1, "params.reduce(|sum, x| x + sum, 0)").unwrap(); } reg.register_script_helper_file("acc", &file1_path).unwrap(); assert_eq!( reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), "10" ); { let mut file1: File = File::create(&file1_path).unwrap(); write!(file1, "params.reduce(|sum, x| x * sum, 1)").unwrap(); } assert_eq!( reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), "24" ); dir.close().unwrap(); } #[test] #[cfg(feature = "script_helper")] fn test_engine_access() { use rhai::Engine; let mut registry = Registry::new(); let mut eng = Engine::new(); eng.set_max_string_size(1000); registry.set_engine(eng); assert_eq!(1000, registry.engine().max_string_size()); } }