use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use std::path::Path; 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, TemplateFileError, TemplateRenderError}; use crate::helpers::{self, HelperDef}; use crate::output::{Output, StringOutput, WriteOutput}; use crate::render::{RenderContext, Renderable}; use crate::support::str::{self, StringWriter}; use crate::template::Template; #[cfg(feature = "dir_source")] use std::path; #[cfg(feature = "dir_source")] use walkdir::{DirEntry, WalkDir}; #[cfg(feature = "script_helper")] use rhai::Engine; #[cfg(feature = "script_helper")] use crate::helpers::scripting::ScriptHelper; /// 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 = Box 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. pub struct Registry<'reg> { templates: HashMap, helpers: HashMap>, decorators: HashMap>, escape_fn: EscapeFn, source_map: bool, strict_mode: bool, #[cfg(feature = "script_helper")] pub(crate) engine: Engine, } 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("source_map", &self.source_map) .finish() } } impl<'reg> Default for Registry<'reg> { fn default() -> Self { Self::new() } } #[cfg(feature = "dir_source")] fn filter_file(entry: &DirEntry, suffix: &str) -> bool { let path = entry.path(); // ignore hidden files, emacs buffers and files with wrong suffix !path.is_file() || path .file_name() .map(|s| { let ds = s.to_string_lossy(); ds.starts_with('.') || ds.starts_with('#') || !ds.ends_with(suffix) }) .unwrap_or(true) } #[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(), helpers: HashMap::new(), decorators: HashMap::new(), escape_fn: Box::new(html_escape), source_map: true, strict_mode: false, #[cfg(feature = "script_helper")] engine: rhai_engine(), }; 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_boolean::eq)); self.register_helper("ne", Box::new(helpers::helper_boolean::ne)); self.register_helper("gt", Box::new(helpers::helper_boolean::gt)); self.register_helper("gte", Box::new(helpers::helper_boolean::gte)); self.register_helper("lt", Box::new(helpers::helper_boolean::lt)); self.register_helper("lte", Box::new(helpers::helper_boolean::lte)); self.register_helper("and", Box::new(helpers::helper_boolean::and)); self.register_helper("or", Box::new(helpers::helper_boolean::or)); self.register_helper("not", Box::new(helpers::helper_boolean::not)); self.register_decorator("inline", Box::new(decorators::INLINE_DECORATOR)); self } /// Enable handlebars template source map /// /// Source map provides line/col reporting on error. It uses slightly /// more memory to maintain the data. /// /// Default is true. pub fn source_map_enabled(&mut self, enable: bool) { self.source_map = enable; } /// Enable 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, enable: bool) { self.strict_mode = enable; } /// 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 } /// 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. 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::compile_with_name(tpl_str, name.to_owned(), self.source_map)?; 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 pub fn register_template_file

( &mut self, name: &str, tpl_path: P, ) -> Result<(), TemplateFileError> where P: AsRef, { let mut reader = BufReader::new( File::open(tpl_path).map_err(|e| TemplateFileError::IOError(e, name.to_owned()))?, ); self.register_template_source(name, &mut reader) } /// 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. #[cfg(feature = "dir_source")] pub fn register_templates_directory

( &mut self, tpl_extension: &'static str, dir_path: P, ) -> Result<(), TemplateFileError> where P: AsRef, { let dir_path = dir_path.as_ref(); let prefix_len = if dir_path .to_string_lossy() .ends_with(|c| c == '\\' || c == '/') // `/` will work on windows too so we still need to check { dir_path.to_string_lossy().len() } else { dir_path.to_string_lossy().len() + 1 }; let walker = WalkDir::new(dir_path); let dir_iter = walker .min_depth(1) .into_iter() .filter(|e| e.is_ok() && !filter_file(e.as_ref().unwrap(), tpl_extension)); for entry in dir_iter { let entry = entry?; let tpl_path = entry.path(); let tpl_file_path = entry.path().to_string_lossy(); let tpl_name = &tpl_file_path[prefix_len..tpl_file_path.len() - tpl_extension.len()]; // replace platform path separator with our internal one let tpl_canonical_name = tpl_name.replace(path::MAIN_SEPARATOR, "/"); self.register_template_file(&tpl_canonical_name, &tpl_path)?; } Ok(()) } /// Register a template from `std::io::Read` source pub fn register_template_source( &mut self, name: &str, tpl_source: &mut R, ) -> Result<(), TemplateFileError> where R: Read, { let mut buf = String::new(); tpl_source .read_to_string(&mut buf) .map_err(|e| TemplateFileError::IOError(e, name.to_owned()))?; self.register_template_string(name, buf)?; Ok(()) } /// Remove a template from the registry pub fn unregister_template(&mut self, name: &str) { self.templates.remove(name); } /// Register a helper pub fn register_helper( &mut self, name: &str, def: Box, ) -> Option> { self.helpers.insert(name.to_string(), def) } /// 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")] pub fn register_script_helper( &mut self, name: &str, script: String, ) -> Result>, ScriptError> { let compiled = self.engine.compile(&script)?; let script_helper = ScriptHelper { script: compiled }; Ok(self .helpers .insert(name.to_string(), Box::new(script_helper))) } /// Register a [rhai](https://docs.rs/rhai/) script from file #[cfg(feature = "script_helper")] pub fn register_script_helper_file

( &mut self, name: &str, script_path: P, ) -> Result>, ScriptError> where P: AsRef, { let mut script = String::new(); { let mut file = File::open(script_path)?; file.read_to_string(&mut script)?; } self.register_script_helper(name, script) } /// Register a decorator pub fn register_decorator( &mut self, name: &str, def: Box, ) -> Option> { self.decorators.insert(name.to_string(), def) } /// 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 = Box::new(escape_fn); } /// Restore the default *escape fn*. pub fn unregister_escape_fn(&mut self) { self.escape_fn = Box::new(html_escape); } /// Get a reference to the current *escape fn*. pub fn get_escape_fn(&self) -> &dyn Fn(&str) -> String { &*self.escape_fn } /// 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) } /// Return a registered helper pub fn get_helper(&self, name: &str) -> Option<&(dyn HelperDef + Send + Sync + 'reg)> { self.helpers.get(name).map(|v| v.as_ref()) } #[inline] pub(crate) fn has_helper(&self, name: &str) -> bool { self.helpers.contains_key(name) } /// Return a registered decorator pub 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 pub fn get_templates(&self) -> &HashMap { &self.templates } /// Unregister all templates pub fn clear_templates(&mut self) { self.templates.clear(); } fn render_to_output( &self, name: &str, ctx: &Context, output: &mut O, ) -> Result<(), RenderError> where O: Output, { self.get_template(name) .ok_or_else(|| RenderError::new(format!("Template not found: {}", 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 some 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 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 reused context data pub fn render_template_with_context( &self, template_string: &str, ctx: &Context, ) -> Result { let tpl = Template::compile2(template_string, self.source_map)?; 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(|e| TemplateRenderError::from(RenderError::from(e))) } /// 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<(), TemplateRenderError> where T: Serialize, W: Write, { let tpl = Template::compile2(template_string, self.source_map)?; let ctx = Context::wraps(data)?; let mut render_context = RenderContext::new(None); let mut out = WriteOutput::new(writer); tpl.render(self, &ctx, &mut render_context, &mut out) .map_err(TemplateRenderError::from) } /// Render a template source using current registry without registering it pub fn render_template_source_to_write( &self, template_source: &mut R, data: &T, writer: W, ) -> Result<(), TemplateRenderError> where T: Serialize, W: Write, R: Read, { let mut tpl_str = String::new(); template_source .read_to_string(&mut tpl_str) .map_err(|e| TemplateRenderError::IOError(e, "Unnamed template source".to_owned()))?; self.render_template_to_write(&tpl_str, data, 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; #[cfg(feature = "dir_source")] use std::fs::{DirBuilder, File}; #[cfg(feature = "dir_source")] use std::io::Write; #[cfg(feature = "dir_source")] 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 = 9; // 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() { 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.as_render_error().unwrap().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.as_render_error().unwrap().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(Some(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::wraps(&data).unwrap()) .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() ); } }