diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:35 +0000 |
commit | 7e5d7eea9c580ef4b41a765bde624af431942b96 (patch) | |
tree | 2c0d9ca12878fc4525650aa4e54d77a81a07cc09 /vendor/handlebars-3.5.5/src/registry.rs | |
parent | Adding debian version 1.70.0+dfsg1-9. (diff) | |
download | rustc-7e5d7eea9c580ef4b41a765bde624af431942b96.tar.xz rustc-7e5d7eea9c580ef4b41a765bde624af431942b96.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/handlebars-3.5.5/src/registry.rs')
-rw-r--r-- | vendor/handlebars-3.5.5/src/registry.rs | 989 |
1 files changed, 989 insertions, 0 deletions
diff --git a/vendor/handlebars-3.5.5/src/registry.rs b/vendor/handlebars-3.5.5/src/registry.rs new file mode 100644 index 000000000..67530de7c --- /dev/null +++ b/vendor/handlebars-3.5.5/src/registry.rs @@ -0,0 +1,989 @@ +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<dyn Fn(&str) -> 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<String, Template>, + helpers: HashMap<String, Box<dyn HelperDef + Send + Sync + 'reg>>, + decorators: HashMap<String, Box<dyn DecoratorDef + Send + Sync + 'reg>>, + 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<S>( + &mut self, + name: &str, + tpl_str: S, + ) -> Result<(), TemplateError> + where + S: AsRef<str>, + { + 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<S>(&mut self, name: &str, partial_str: S) -> Result<(), TemplateError> + where + S: AsRef<str>, + { + self.register_template_string(name, partial_str) + } + + /// Register a template from a path + pub fn register_template_file<P>( + &mut self, + name: &str, + tpl_path: P, + ) -> Result<(), TemplateFileError> + where + P: AsRef<Path>, + { + 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<P>( + &mut self, + tpl_extension: &'static str, + dir_path: P, + ) -> Result<(), TemplateFileError> + where + P: AsRef<Path>, + { + 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<R>( + &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<dyn HelperDef + Send + Sync + 'reg>, + ) -> Option<Box<dyn HelperDef + Send + Sync + 'reg>> { + 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<Option<Box<dyn HelperDef + Send + Sync + 'reg>>, 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<P>( + &mut self, + name: &str, + script_path: P, + ) -> Result<Option<Box<dyn HelperDef + Send + Sync + 'reg>>, ScriptError> + where + P: AsRef<Path>, + { + 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<dyn DecoratorDef + Send + Sync + 'reg>, + ) -> Option<Box<dyn DecoratorDef + Send + Sync + 'reg>> { + 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<F: 'static + Fn(&str) -> 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<String, Template> { + &self.templates + } + + /// Unregister all templates + pub fn clear_templates(&mut self) { + self.templates.clear(); + } + + fn render_to_output<O>( + &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<T>(&self, name: &str, data: &T) -> Result<String, RenderError> + 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<String, RenderError> { + 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<T, W>(&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<T>( + &self, + template_string: &str, + data: &T, + ) -> Result<String, TemplateRenderError> + 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<String, TemplateRenderError> { + 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<T, W>( + &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<T, R, W>( + &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", "<h1></h1>").is_ok()); + + let tpl = Template::compile("<h2></h2>").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, "<h1>Hello {{world}}!</h1>").unwrap(); + + let file2_path = dir.path().join("t2.hbs"); + let mut file2: File = File::create(&file2_path).unwrap(); + writeln!(file2, "<h1>Hola {{world}}!</h1>").unwrap(); + + let file3_path = dir.path().join("t3.hbs"); + let mut file3: File = File::create(&file3_path).unwrap(); + writeln!(file3, "<h1>Hallo {{world}}!</h1>").unwrap(); + + let file4_path = dir.path().join(".t4.hbs"); + let mut file4: File = File::create(&file4_path).unwrap(); + writeln!(file4, "<h1>Hallo {{world}}!</h1>").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, "<h1>Hello {{world}}!</h1>").unwrap(); + + let file2_path = dir.path().join("t5.erb"); + let mut file2: File = File::create(&file2_path).unwrap(); + writeln!(file2, "<h1>Hello {{% world %}}!</h1>").unwrap(); + + let file3_path = dir.path().join("t6.html"); + let mut file3: File = File::create(&file3_path).unwrap(); + writeln!(file3, "<h1>Hello world!</h1>").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, "<h1>Bonjour {{world}}!</h1>").unwrap(); + + let file2_path = dir.path().join("portugese/t8.hbs"); + let mut file2: File = File::create(&file2_path).unwrap(); + writeln!(file2, "<h1>Ola {{world}}!</h1>").unwrap(); + + let file3_path = dir.path().join("italian/t9.hbs"); + let mut file3: File = File::create(&file3_path).unwrap(); + writeln!(file3, "<h1>Ciao {{world}}!</h1>").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, "<h1>Bonjour {{world}}!</h1>").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", "<h1></h1>").is_ok()); + + let mut sw = StringWriter::new(); + { + r.render_to_write("index", &(), &mut sw).ok().unwrap(); + } + + assert_eq!("<h1></h1>".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<Option<ScopedJson<'reg, 'rc>>, 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": "<b>bold</b>"})) + .unwrap(), + "<b>bold</b>" + ); + assert_eq!( + reg.render_template("{{ &a }}", &json!({"a": "<b>bold</b>"})) + .unwrap(), + "<b>bold</b>" + ); + } + + #[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() + ); + } +} |