blob: 1f4feab63862293cd10958bf712b52ccd3774397 [file] [log] [blame]
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, RenderErrorReason, 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 crate::sources::LazySource;
#[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<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.
#[derive(Clone)]
pub struct Registry<'reg> {
templates: HashMap<String, Template>,
helpers: HashMap<String, Arc<dyn HelperDef + Send + Sync + 'reg>>,
decorators: HashMap<String, Arc<dyn DecoratorDef + Send + Sync + 'reg>>,
escape_fn: EscapeFn,
strict_mode: bool,
dev_mode: bool,
prevent_indent: bool,
#[cfg(feature = "script_helper")]
pub(crate) engine: Arc<Engine>,
template_sources:
HashMap<String, Arc<dyn Source<Item = String, Error = IoError> + Send + Sync + 'reg>>,
#[cfg(feature = "script_helper")]
script_sources:
HashMap<String, Arc<dyn Source<Item = String, Error = IoError> + 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()
}
/// Options for importing template files from a directory.
#[cfg(feature = "dir_source")]
pub struct DirectorySourceOptions {
/// The name extension for template files
pub tpl_extension: String,
/// Whether to include hidden files (file name that starts with `.`)
pub hidden: bool,
/// Whether to include temporary files (file name that starts with `#`)
pub temporary: bool,
}
#[cfg(feature = "dir_source")]
impl DirectorySourceOptions {
fn ignore_file(&self, name: &str) -> bool {
self.ignored_as_hidden_file(name) || self.ignored_as_temporary_file(name)
}
#[inline]
fn ignored_as_hidden_file(&self, name: &str) -> bool {
!self.hidden && name.starts_with('.')
}
#[inline]
fn ignored_as_temporary_file(&self, name: &str) -> bool {
!self.temporary && name.starts_with('#')
}
}
#[cfg(feature = "dir_source")]
impl Default for DirectorySourceOptions {
fn default() -> Self {
DirectorySourceOptions {
tpl_extension: ".hbs".to_owned(),
hidden: false,
temporary: false,
}
}
}
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));
#[cfg(feature = "string_helpers")]
self.register_string_helpers();
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<S>(
&mut self,
name: &str,
tpl_str: S,
) -> Result<(), TemplateError>
where
S: AsRef<str>,
{
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<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 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<P>(
&mut self,
name: &str,
tpl_path: P,
) -> Result<(), TemplateError>
where
P: AsRef<Path>,
{
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 by default.
/// Set `DirectorySourceOptions` to something other than `DirectorySourceOptions::default()` to adjust this.
/// All registered templates will use their relative path to determine their template name.
/// For example, when `dir_path` is `templates/` and `DirectorySourceOptions.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 is enabled, like with `register_template_file`, templates are reloaded
/// from the file system every time they're visited.
#[cfg(feature = "dir_source")]
#[cfg_attr(docsrs, doc(cfg(feature = "dir_source")))]
pub fn register_templates_directory<P>(
&mut self,
dir_path: P,
options: DirectorySourceOptions,
) -> Result<(), TemplateError>
where
P: AsRef<Path>,
{
let dir_path = dir_path.as_ref();
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
.to_string_lossy()
.ends_with(options.tpl_extension.as_str())
})
// Rejects any hidden or temporary files.
.filter(|tpl_path| {
tpl_path
.file_stem()
.map(|stem| !options.ignore_file(&stem.to_string_lossy()))
.unwrap_or(false)
})
.filter_map(|tpl_path| {
tpl_path
.strip_prefix(dir_path)
.ok()
.map(|tpl_canonical_name| {
let tpl_name = tpl_canonical_name
.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
tpl_name
.strip_suffix(options.tpl_extension.as_str())
.map(|s| s.to_owned())
.unwrap_or(tpl_name)
})
.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
/// Calls register_embed_templates_with_extension with empty extension.
///
/// File names from embed struct are used as template name.
///
/// ```skip
/// #[derive(RustEmbed)]
/// #[folder = "templates"]
/// #[include = "*.hbs"]
/// struct Assets;
///
/// let mut hbs = Handlebars::new();
/// hbs.register_embed_templates::<Assets>();
/// ```
///
#[cfg(feature = "rust-embed")]
#[cfg_attr(docsrs, doc(cfg(feature = "rust-embed")))]
pub fn register_embed_templates<E>(&mut self) -> Result<(), TemplateError>
where
E: RustEmbed,
{
self.register_embed_templates_with_extension::<E>("")
}
/// Register templates using a
/// [RustEmbed](https://github.com/pyros2097/rust-embed) type
/// * `tpl_extension`: the template file extension
///
/// File names from embed struct are used as template name, but extension is stripped.
///
/// When dev_mode enabled templates is reloaded
/// from embed struct everytime it's visied.
///
/// ```skip
/// #[derive(RustEmbed)]
/// #[folder = "templates"]
/// struct Assets;
///
/// let mut hbs = Handlebars::new();
/// hbs.register_embed_templates_with_extension::<Assets>(".hbs");
/// ```
///
#[cfg(feature = "rust-embed")]
#[cfg_attr(docsrs, doc(cfg(feature = "rust-embed")))]
pub fn register_embed_templates_with_extension<E>(
&mut self,
tpl_extension: &str,
) -> Result<(), TemplateError>
where
E: RustEmbed,
{
for file_name in E::iter().filter(|x| x.ends_with(tpl_extension)) {
let tpl_name = file_name
.strip_suffix(tpl_extension)
.unwrap_or(&file_name)
.to_owned();
let source = LazySource::new(move || {
E::get(&file_name)
.map(|file| file.data.to_vec())
.and_then(|data| String::from_utf8(data).ok())
});
let tpl_content = source
.load()
.map_err(|e| (e, "Template load error".to_owned()))?;
self.register_template_string(&tpl_name, &tpl_content)?;
if self.dev_mode {
self.template_sources.insert(tpl_name, Arc::new(source));
}
}
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<dyn HelperDef + Send + Sync + 'reg>) {
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<P>(
&mut self,
name: &str,
script_path: P,
) -> Result<(), ScriptError>
where
P: AsRef<Path>,
{
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<dyn DecoratorDef + Send + Sync + 'reg>,
) {
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<F: 'static + Fn(&str) -> 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<Result<Cow<'reg, Template>, 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<Cow<'reg, Template>, RenderError> {
if let Some(result) = self.get_or_load_template_optional(name) {
result
} else {
Err(RenderErrorReason::TemplateNotFound(name.to_owned()).into())
}
}
/// Return a registered helper
#[inline]
pub(crate) fn get_or_load_helper(
&'reg self,
name: &str,
) -> Result<Option<Arc<dyn HelperDef + Send + Sync + 'reg>>, 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<dyn HelperDef + Send + Sync>;
Ok(Some(helper.into()))
})
.map_err(|e| RenderError::from(RenderErrorReason::from(e)));
}
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<String, Template> {
&self.templates
}
/// Unregister all templates
pub fn clear_templates(&mut self) {
self.templates.clear();
self.template_sources.clear();
}
#[inline]
fn render_to_output<O>(
&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<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 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 registered template using reusable `Context`, and write data to
/// the `std::io::Write`
pub fn render_with_context_to_write<W>(
&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<T>(&self, template_string: &str, data: &T) -> Result<String, RenderError>
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<String, RenderError> {
let tpl = Template::compile2(
template_string,
TemplateOptions {
prevent_indent: self.prevent_indent,
..Default::default()
},
)
.map_err(RenderError::from)?;
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<W>(
&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()
},
)
.map_err(RenderError::from)?;
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<T, W>(
&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(feature = "string_helpers")]
#[inline]
fn register_string_helpers(&mut self) {
use helpers::string_helpers::{
kebab_case, lower_camel_case, shouty_kebab_case, shouty_snake_case, snake_case,
title_case, train_case, upper_camel_case,
};
self.register_helper("lowerCamelCase", Box::new(lower_camel_case));
self.register_helper("upperCamelCase", Box::new(upper_camel_case));
self.register_helper("snakeCase", Box::new(snake_case));
self.register_helper("kebabCase", Box::new(kebab_case));
self.register_helper("shoutySnakeCase", Box::new(shouty_snake_case));
self.register_helper("shoutyKebabCase", Box::new(shouty_kebab_case));
self.register_helper("titleCase", Box::new(title_case));
self.register_helper("trainCase", Box::new(train_case));
}
}
#[cfg(test)]
mod test {
use crate::context::Context;
use crate::error::{RenderError, RenderErrorReason};
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<'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 = 10; // stuff like gt and lte
let num_custom_helpers = 1; // dummy from above
#[cfg(feature = "string_helpers")]
let string_helpers = 8;
#[cfg(not(feature = "string_helpers"))]
let string_helpers = 0;
assert_eq!(
r.helpers.len(),
num_helpers + num_boolean_helpers + num_custom_helpers + string_helpers
);
}
#[test]
#[cfg(feature = "dir_source")]
fn test_register_templates_directory() {
use std::fs::DirBuilder;
use crate::registry::DirectorySourceOptions;
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(dir.path(), DirectorySourceOptions::default())
.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(dir.path(), DirectorySourceOptions::default())
.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(dir.path(), DirectorySourceOptions::default())
.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(dir_path, DirectorySourceOptions::default())
.unwrap();
assert_eq!(r.templates.len(), 8);
assert_eq!(r.templates.contains_key("t10"), true);
drop(file1);
dir.close().unwrap();
}
{
let dir = tempdir().unwrap();
let mut r = Registry::new();
let file1_path = dir.path().join("t11.hbs.html");
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(
dir_path,
DirectorySourceOptions {
tpl_extension: ".hbs.html".to_owned(),
..Default::default()
},
)
.unwrap();
assert_eq!(r.templates.len(), 1);
assert_eq!(r.templates.contains_key("t11"), true);
drop(file1);
dir.close().unwrap();
}
{
let dir = tempdir().unwrap();
let mut r = Registry::new();
assert_eq!(r.templates.len(), 0);
let file1_path = dir.path().join(".t12.hbs");
let mut file1: File = File::create(&file1_path).unwrap();
writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();
r.register_templates_directory(
dir.path(),
DirectorySourceOptions {
hidden: true,
..Default::default()
},
)
.unwrap();
assert_eq!(r.templates.len(), 1);
assert_eq!(r.templates.contains_key(".t12"), 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!("&quot;&lt;&gt;&amp;", 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!("&quot;&lt;&gt;&amp;", 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);
assert_eq!(
match render_error.reason() {
RenderErrorReason::MissingVariable(path) => path.as_ref().unwrap(),
_ => unreachable!(),
},
"the_key_never_exists"
);
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);
assert_eq!(
match render_error2.reason() {
RenderErrorReason::MissingVariable(path) => path.as_ref().unwrap(),
_ => unreachable!(),
},
"this.[3]"
)
}
use crate::json::value::ScopedJson;
struct GenMissingHelper;
impl HelperDef for GenMissingHelper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
_: &Helper<'rc>,
_: &'reg Registry<'reg>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<ScopedJson<'rc>, 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": "<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::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, "<h1>Hello {{{{name}}}}!</h1>").unwrap();
}
reg.register_template_file("t1", &file1_path).unwrap();
assert_eq!(
reg.render("t1", &json!({"name": "Alex"})).unwrap(),
"<h1>Hello Alex!</h1>"
);
{
let mut file1: File = File::create(&file1_path).unwrap();
write!(file1, "<h1>Privet {{{{name}}}}!</h1>").unwrap();
}
assert_eq!(
reg.render("t1", &json!({"name": "Alex"})).unwrap(),
"<h1>Privet Alex!</h1>"
);
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());
}
}