diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5969d892..bbe935b88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,16 @@
## [Unreleased](https://github.com/sunng87/handlebars-rust/compare/3.4.0...Unreleased) - ReleaseDate
+* [Added] `dev_mode` for registry: templates and scripts loaded from file are always
+ reloaded when dev mode enabled [#395]
+* [Added] Registry is now `Clone` [#395]
* [Changed] `#each` helper now renders else block for non-iterable data [#380]
+* [Changed] `TemplateError` and `ScriptError` is now a cause of `RenderError` [#395]
* [Fixed] reference starts with `null`, `true` and `false` were parsed incorrectly [#382]
* [Fixed] dir source path separator bug on windows [#389]
+* [Removed] option to disable source map is removed [#395]
+* [Removed] `TemplateFileError` and `TemplateRenderError` are removed and merged into
+ `TemplateError` and `RenderError` [#395]
## [3.4.0](https://github.com/sunng87/handlebars-rust/compare/3.3.0...3.4.0) - 2020-08-14
diff --git a/Cargo.toml b/Cargo.toml
index af73baec3..86e735b7c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ pest_derive = "2.1.0"
serde = "1.0.0"
serde_json = "1.0.39"
walkdir = { version = "2.2.3", optional = true }
-rhai = { version = "0.19.4", optional = true, features = ["sync", "serde"] }
+rhai = { version = "0.19.6", optional = true, features = ["sync", "serde"] }
[dev-dependencies]
env_logger = "0.7.1"
@@ -36,6 +36,8 @@ maplit = "1.0.0"
serde_derive = "1.0.75"
tempfile = "3.0.0"
criterion = "0.3"
+warp = "0.2"
+tokio = { version = "0.2", features = ["macros"] }
[target.'cfg(unix)'.dev-dependencies]
pprof = { version = "0.3.13", features = ["flamegraph", "protobuf"] }
diff --git a/README.md b/README.md
index 2e789ab37..824a9dd2c 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,9 @@ Examples are provided in source tree to demo usage of various api.
just like using javascript for handlebarsjs
* [error](https://github.com/sunng87/handlebars-rust/blob/master/examples/error.rs)
simple case for error
+* [dev_mode](https://github.com/sunng87/handlebars-rust/blob/master/examples/dev_mode.rs)
+ a web server hosts handlebars in `dev_mode`, you can edit the template and see the change
+ without restarting your server.
## Minimum Rust Version Policy
@@ -163,6 +166,11 @@ embed your page into this parent.
You can find a real example of template inheritance in
`examples/partials.rs` and templates used by this file.
+#### Auto-reload in dev mode
+
+By turning on `dev_mode`, handlebars auto reloads any template and scripts that
+loaded from files or directory. This can be handy for template development.
+
#### WebAssembly compatible
Handlebars 3.0 can be used in WebAssembly projects.
diff --git a/examples/dev_mode.rs b/examples/dev_mode.rs
new file mode 100644
index 000000000..99017474f
--- /dev/null
+++ b/examples/dev_mode.rs
@@ -0,0 +1,27 @@
+use std::sync::Arc;
+
+use handlebars::Handlebars;
+use serde_json::json;
+use warp::{self, Filter};
+
+#[tokio::main]
+async fn main() {
+ let mut reg = Handlebars::new();
+ // enable dev mode for template reloading
+ reg.set_dev_mode(true);
+ // register a template from the file
+ // modified the file after the server starts to see things changing
+ reg.register_template_file("tpl", "./examples/dev_mode/template.hbs")
+ .unwrap();
+
+ let hbs = Arc::new(reg);
+ let route = warp::get().map(move || {
+ let result = hbs
+ .render("tpl", &json!({"model": "t14s", "brand": "Thinkpad"}))
+ .unwrap_or_else(|e| e.to_string());
+ warp::reply::html(result)
+ });
+
+ println!("Edit ./examples/dev_mode/template.hbs and request http://localhost:3030 to see the change on the run.");
+ warp::serve(route).run(([127, 0, 0, 1], 3030)).await;
+}
diff --git a/examples/dev_mode/template.hbs b/examples/dev_mode/template.hbs
new file mode 100644
index 000000000..19bb80657
--- /dev/null
+++ b/examples/dev_mode/template.hbs
@@ -0,0 +1,8 @@
+
+
+ My Laptop
+
+
+
My current laptop is {{brand}}: {{model}}
+
+
diff --git a/examples/render_file.rs b/examples/render_file.rs
index c0a39c17d..7b7599672 100644
--- a/examples/render_file.rs
+++ b/examples/render_file.rs
@@ -129,9 +129,12 @@ fn main() -> Result<(), Box> {
let data = make_data();
- let mut source_template = File::open(&"./examples/render_file/template.hbs")?;
+ handlebars
+ .register_template_file("template", "./examples/render_file/template.hbs")
+ .unwrap();
+
let mut output_file = File::create("target/table.html")?;
- handlebars.render_template_source_to_write(&mut source_template, &data, &mut output_file)?;
+ handlebars.render_to_write("template", &data, &mut output_file)?;
println!("target/table.html generated");
Ok(())
}
diff --git a/examples/script/goals.rhai b/examples/script/goals.rhai
index e53373b08..65828202e 100644
--- a/examples/script/goals.rhai
+++ b/examples/script/goals.rhai
@@ -1,3 +1,3 @@
let goals = params[0];
-len(goals)
+goals.len()
diff --git a/src/context.rs b/src/context.rs
index b1c6bccdd..6aa8d96c2 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -36,7 +36,7 @@ fn parse_json_visitor<'a, 'reg>(
relative_path: &[PathSeg],
block_contexts: &'a VecDeque>,
always_for_absolute_path: bool,
-) -> Result, RenderError> {
+) -> ResolvedPath<'a> {
let mut path_context_depth: i64 = 0;
let mut with_block_param = None;
let mut from_root = false;
@@ -65,7 +65,7 @@ fn parse_json_visitor<'a, 'reg>(
match with_block_param {
Some((BlockParamHolder::Value(ref value), _)) => {
merge_json_path(&mut path_stack, &relative_path[1..]);
- Ok(ResolvedPath::BlockParamValue(path_stack, value))
+ ResolvedPath::BlockParamValue(path_stack, value)
}
Some((BlockParamHolder::Path(ref paths), base_path)) => {
extend(&mut path_stack, base_path);
@@ -74,7 +74,7 @@ fn parse_json_visitor<'a, 'reg>(
}
merge_json_path(&mut path_stack, &relative_path[1..]);
- Ok(ResolvedPath::AbsolutePath(path_stack))
+ ResolvedPath::AbsolutePath(path_stack)
}
None => {
if path_context_depth > 0 {
@@ -90,24 +90,24 @@ fn parse_json_visitor<'a, 'reg>(
}
}
merge_json_path(&mut path_stack, relative_path);
- Ok(ResolvedPath::AbsolutePath(path_stack))
+ ResolvedPath::AbsolutePath(path_stack)
} else if from_root {
merge_json_path(&mut path_stack, relative_path);
- Ok(ResolvedPath::AbsolutePath(path_stack))
+ ResolvedPath::AbsolutePath(path_stack)
} else if always_for_absolute_path {
if let Some(base_value) = block_contexts.front().and_then(|blk| blk.base_value()) {
merge_json_path(&mut path_stack, relative_path);
- Ok(ResolvedPath::LocalValue(path_stack, base_value))
+ ResolvedPath::LocalValue(path_stack, base_value)
} else {
if let Some(base_path) = block_contexts.front().map(|blk| blk.base_path()) {
extend(&mut path_stack, base_path);
}
merge_json_path(&mut path_stack, relative_path);
- Ok(ResolvedPath::AbsolutePath(path_stack))
+ ResolvedPath::AbsolutePath(path_stack)
}
} else {
merge_json_path(&mut path_stack, relative_path);
- Ok(ResolvedPath::RelativePath(path_stack))
+ ResolvedPath::RelativePath(path_stack)
}
}
}
@@ -170,7 +170,7 @@ impl Context {
block_contexts: &VecDeque>,
) -> Result, RenderError> {
// always use absolute at the moment until we get base_value lifetime issue fixed
- let resolved_visitor = parse_json_visitor(&relative_path, block_contexts, true)?;
+ let resolved_visitor = parse_json_visitor(&relative_path, block_contexts, true);
// debug logging
debug!("Accessing context value: {:?}", resolved_visitor);
diff --git a/src/error.rs b/src/error.rs
index b621577a9..fac6bba30 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -69,6 +69,12 @@ impl From for RenderError {
}
}
+impl From for RenderError {
+ fn from(e: TemplateError) -> RenderError {
+ RenderError::from_error("Error with parsing template.", e)
+ }
+}
+
#[cfg(feature = "script_helper")]
impl From> for RenderError {
fn from(e: Box) -> RenderError {
@@ -76,6 +82,13 @@ impl From> for RenderError {
}
}
+#[cfg(feature = "script_helper")]
+impl From for RenderError {
+ fn from(e: ScriptError) -> RenderError {
+ RenderError::from_error("Error loading rhai script.", e)
+ }
+}
+
impl RenderError {
pub fn new>(desc: T) -> RenderError {
RenderError {
@@ -95,17 +108,6 @@ impl RenderError {
RenderError::new(&msg)
}
- #[deprecated]
- pub fn with(cause: E) -> RenderError
- where
- E: Error + Send + Sync + 'static,
- {
- let mut e = RenderError::new(cause.to_string());
- e.cause = Some(Box::new(cause));
-
- e
- }
-
pub fn from_error(error_kind: &str, cause: E) -> RenderError
where
E: Error + Send + Sync + 'static,
@@ -119,7 +121,7 @@ impl RenderError {
quick_error! {
/// Template parsing error
- #[derive(PartialEq, Debug, Clone)]
+ #[derive(Debug)]
pub enum TemplateErrorReason {
MismatchingClosedHelper(open: String, closed: String) {
display("helper {:?} was opened, but {:?} is closing",
@@ -138,11 +140,18 @@ quick_error! {
NestedSubexpression {
display("nested subexpression is not supported")
}
+ IoError(err: IOError, name: String) {
+ display("Template \"{}\": {}", name, err)
+ }
+ #[cfg(feature = "dir_source")]
+ WalkdirError(err: WalkdirError) {
+ display("Walk dir error: {}", err)
+ }
}
}
/// Error on parsing template.
-#[derive(Debug, PartialEq)]
+#[derive(Debug)]
pub struct TemplateError {
pub reason: TemplateErrorReason,
pub template_name: Option,
@@ -177,6 +186,20 @@ impl TemplateError {
impl Error for TemplateError {}
+impl From<(IOError, String)> for TemplateError {
+ fn from(err_info: (IOError, String)) -> TemplateError {
+ let (e, name) = err_info;
+ TemplateError::of(TemplateErrorReason::IoError(e, name))
+ }
+}
+
+#[cfg(feature = "dir_source")]
+impl From for TemplateError {
+ fn from(e: WalkdirError) -> TemplateError {
+ TemplateError::of(TemplateErrorReason::WalkdirError(e))
+ }
+}
+
fn template_segment(template_str: &str, line: usize, col: usize) -> String {
let range = 3;
let line_start = if line >= range { line - range } else { 0 };
@@ -223,64 +246,6 @@ impl fmt::Display for TemplateError {
}
}
-quick_error! {
- /// A combined error type for `TemplateError` and `IOError`
- #[derive(Debug)]
- pub enum TemplateFileError {
- TemplateError(err: TemplateError) {
- from()
- source(err)
- display("{}", err)
- }
- IOError(err: IOError, name: String) {
- source(err)
- display("Template \"{}\": {}", name, err)
- }
- }
-}
-
-#[cfg(feature = "dir_source")]
-impl From for TemplateFileError {
- fn from(error: WalkdirError) -> TemplateFileError {
- let path_string: String = error
- .path()
- .map(|p| p.to_string_lossy().to_string())
- .unwrap_or_default();
- TemplateFileError::IOError(IOError::from(error), path_string)
- }
-}
-
-quick_error! {
- /// A combined error type for `TemplateError`, `IOError` and `RenderError`
- #[derive(Debug)]
- pub enum TemplateRenderError {
- TemplateError(err: TemplateError) {
- from()
- source(err)
- display("{}", err)
- }
- RenderError(err: RenderError) {
- from()
- source(err)
- display("{}", err)
- }
- IOError(err: IOError, name: String) {
- source(err)
- display("Template \"{}\": {}", name, err)
- }
- }
-}
-
-impl TemplateRenderError {
- pub fn as_render_error(&self) -> Option<&RenderError> {
- if let TemplateRenderError::RenderError(ref e) = *self {
- Some(&e)
- } else {
- None
- }
- }
-}
-
#[cfg(feature = "script_helper")]
quick_error! {
#[derive(Debug)]
diff --git a/src/helpers/block_util.rs b/src/helpers/block_util.rs
index b1ac70d37..6971fdd8a 100644
--- a/src/helpers/block_util.rs
+++ b/src/helpers/block_util.rs
@@ -1,10 +1,9 @@
use crate::block::BlockContext;
-use crate::error::RenderError;
use crate::json::value::PathAndJson;
pub(crate) fn create_block<'reg: 'rc, 'rc>(
param: &'rc PathAndJson<'reg, 'rc>,
-) -> Result, RenderError> {
+) -> BlockContext<'reg> {
let mut block = BlockContext::new();
if let Some(new_path) = param.context_path() {
@@ -14,5 +13,5 @@ pub(crate) fn create_block<'reg: 'rc, 'rc>(
block.set_base_value(param.value().clone());
}
- Ok(block)
+ block
}
diff --git a/src/helpers/helper_each.rs b/src/helpers/helper_each.rs
index f2c443240..9c525d1d8 100644
--- a/src/helpers/helper_each.rs
+++ b/src/helpers/helper_each.rs
@@ -81,7 +81,7 @@ impl HelperDef for EachHelper {
match template {
Some(t) => match *value.value() {
Json::Array(ref list) if !list.is_empty() => {
- let block_context = create_block(&value)?;
+ let block_context = create_block(&value);
rc.push_block(block_context);
let len = list.len();
@@ -109,7 +109,7 @@ impl HelperDef for EachHelper {
Ok(())
}
Json::Object(ref obj) if !obj.is_empty() => {
- let block_context = create_block(&value)?;
+ let block_context = create_block(&value);
rc.push_block(block_context);
let mut is_first = true;
diff --git a/src/helpers/helper_with.rs b/src/helpers/helper_with.rs
index 9f4a8b834..c4d31cd0e 100644
--- a/src/helpers/helper_with.rs
+++ b/src/helpers/helper_with.rs
@@ -25,7 +25,7 @@ impl HelperDef for WithHelper {
.ok_or_else(|| RenderError::new("Param not found for helper \"with\""))?;
if param.value().is_truthy(false) {
- let mut block = create_block(¶m)?;
+ let mut block = create_block(¶m);
if let Some(block_param) = h.block_param() {
let mut params = BlockParams::new();
diff --git a/src/lib.rs b/src/lib.rs
index ddf1e6b6e..eaa3f8fbc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -53,13 +53,20 @@
//!
//! ### Extensible helper system
//!
-//! You can write your own helper with Rust! It can be a block helper or
-//! inline helper. Put your logic into the helper and don't repeat
-//! yourself.
+//! Helper is the control system of handlebars language. In the original JavaScript
+//! version, you can implement your own helper with JavaScript.
+//!
+//! Handlebars-rust offers similar mechanism that custom helper can be defined with
+//! rust function, or [rhai](https://github.com/jonathandturner/rhai) script.
//!
//! The built-in helpers like `if` and `each` were written with these
//! helper APIs and the APIs are fully available to developers.
//!
+//! ### Auto-reload in dev mode
+//!
+//! By turning on `dev_mode`, handlebars auto reloads any template and scripts that
+//! loaded from files or directory. This can be handy for template development.
+//!
//! ### Template inheritance
//!
//! Every time I look into a templating system, I will investigate its
@@ -371,7 +378,7 @@ extern crate serde_json;
pub use self::block::{BlockContext, BlockParams};
pub use self::context::Context;
pub use self::decorators::DecoratorDef;
-pub use self::error::{RenderError, TemplateError, TemplateFileError, TemplateRenderError};
+pub use self::error::{RenderError, TemplateError};
pub use self::helpers::{HelperDef, HelperResult};
pub use self::json::path::Path;
pub use self::json::value::{to_json, JsonRender, PathAndJson, ScopedJson};
@@ -397,6 +404,7 @@ mod output;
mod partial;
mod registry;
mod render;
+mod sources;
mod support;
pub mod template;
mod util;
diff --git a/src/registry.rs b/src/registry.rs
index 37b3b26fa..5252622bd 100644
--- a/src/registry.rs
+++ b/src/registry.rs
@@ -1,9 +1,9 @@
+use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
-use std::fs::File;
-use std::io::prelude::*;
-use std::io::BufReader;
+use std::io::{Error as IoError, Write};
use std::path::Path;
+use std::sync::Arc;
use serde::Serialize;
@@ -11,10 +11,11 @@ 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::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;
@@ -34,7 +35,7 @@ use crate::helpers::scripting::ScriptHelper;
///
/// 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>;
+pub type EscapeFn = Arc String + Send + Sync>;
/// The default *escape fn* replaces the characters `&"<>`
/// with the equivalent html / xml entities.
@@ -51,15 +52,24 @@ pub fn no_escape(data: &str) -> String {
/// 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>,
+
+ helpers: HashMap>,
+ decorators: HashMap>,
+
escape_fn: EscapeFn,
- source_map: bool,
strict_mode: bool,
+ dev_mode: bool,
+ #[cfg(feature = "script_helper")]
+ pub(crate) engine: Arc,
+
+ template_sources:
+ HashMap + Send + Sync + 'reg>>,
#[cfg(feature = "script_helper")]
- pub(crate) engine: Engine,
+ script_sources:
+ HashMap + Send + Sync + 'reg>>,
}
impl<'reg> Debug for Registry<'reg> {
@@ -68,7 +78,8 @@ impl<'reg> Debug for Registry<'reg> {
.field("templates", &self.templates)
.field("helpers", &self.helpers.keys())
.field("decorators", &self.decorators.keys())
- .field("source_map", &self.source_map)
+ .field("strict_mode", &self.strict_mode)
+ .field("dev_mode", &self.dev_mode)
.finish()
}
}
@@ -103,13 +114,16 @@ 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: Box::new(html_escape),
- source_map: true,
+ escape_fn: Arc::new(html_escape),
strict_mode: false,
+ dev_mode: false,
#[cfg(feature = "script_helper")]
- engine: rhai_engine(),
+ engine: Arc::new(rhai_engine()),
+ #[cfg(feature = "script_helper")]
+ script_sources: HashMap::new(),
};
r.setup_builtins()
@@ -138,25 +152,15 @@ impl<'reg> Registry<'reg> {
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
+ /// 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, enable: bool) {
- self.strict_mode = enable;
+ pub fn set_strict_mode(&mut self, enabled: bool) {
+ self.strict_mode = enabled;
}
/// Return strict mode state, default is false.
@@ -170,6 +174,22 @@ impl<'reg> Registry<'reg> {
self.strict_mode
}
+ /// Return dev mode state, default is false
+ ///
+ /// With dev mode turned on, handlebars enables a set of development
+ /// firendly 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
+ /// firendly features, that may affect its performance.
+ pub fn set_dev_mode(&mut self, enabled: bool) {
+ self.dev_mode = enabled;
+ }
+
/// Register a `Template`
///
/// This is infallible since the template has already been parsed and
@@ -190,7 +210,7 @@ impl<'reg> Registry<'reg> {
where
S: AsRef,
{
- let template = Template::compile_with_name(tpl_str, name.to_owned(), self.source_map)?;
+ let template = Template::compile_with_name(tpl_str, name.to_owned())?;
self.register_template(name, template);
Ok(())
}
@@ -211,14 +231,22 @@ impl<'reg> Registry<'reg> {
&mut self,
name: &str,
tpl_path: P,
- ) -> Result<(), TemplateFileError>
+ ) -> Result<(), TemplateError>
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)
+ 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
@@ -238,7 +266,7 @@ impl<'reg> Registry<'reg> {
&mut self,
tpl_extension: &'static str,
dir_path: P,
- ) -> Result<(), TemplateFileError>
+ ) -> Result<(), TemplateError>
where
P: AsRef,
{
@@ -271,35 +299,14 @@ impl<'reg> Registry<'reg> {
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)
+ 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
@@ -324,16 +331,12 @@ impl<'reg> Registry<'reg> {
///
///
#[cfg(feature = "script_helper")]
- pub fn register_script_helper(
- &mut self,
- name: &str,
- script: String,
- ) -> Result