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>, ScriptError> { - let compiled = self.engine.compile(&script)?; + 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 }; - Ok(self - .helpers - .insert(name.to_string(), Box::new(script_helper))) + self.helpers + .insert(name.to_string(), Arc::new(script_helper)); + Ok(()) } /// Register a [rhai](https://docs.rs/rhai/) script from file @@ -342,17 +345,16 @@ impl<'reg> Registry<'reg> { &mut self, name: &str, script_path: P, - ) -> Result>, ScriptError> + ) -> Result<(), ScriptError> where P: AsRef, { - let mut script = String::new(); - { - let mut file = File::open(script_path)?; - file.read_to_string(&mut script)?; - } + let source = FileSource::new(script_path.as_ref().into()); + let script = source.load()?; - self.register_script_helper(name, script) + self.script_sources + .insert(name.to_owned(), Arc::new(source)); + self.register_script_helper(name, &script) } /// Register a decorator @@ -360,8 +362,8 @@ impl<'reg> Registry<'reg> { &mut self, name: &str, def: Box, - ) -> Option> { - self.decorators.insert(name.to_string(), def) + ) { + self.decorators.insert(name.to_string(), def.into()); } /// Register a new *escape fn* to be used from now on by this registry. @@ -369,12 +371,12 @@ impl<'reg> Registry<'reg> { &mut self, escape_fn: F, ) { - self.escape_fn = Box::new(escape_fn); + self.escape_fn = Arc::new(escape_fn); } /// Restore the default *escape fn*. pub fn unregister_escape_fn(&mut self) { - self.escape_fn = Box::new(html_escape); + self.escape_fn = Arc::new(html_escape); } /// Get a reference to the current *escape fn*. @@ -392,9 +394,43 @@ impl<'reg> Registry<'reg> { self.templates.get(name) } + #[inline] + fn get_or_load_template(&'reg self, name: &str) -> Result, RenderError> { + if let (true, Some(source)) = (self.dev_mode, self.template_sources.get(name)) { + source + .load() + .map_err(|e| TemplateError::from((e, name.to_owned()))) + .and_then(|tpl_str| Template::compile_with_name(tpl_str, name.to_owned())) + .map(Cow::Owned) + .map_err(RenderError::from) + } else { + self.templates + .get(name) + .map(Cow::Borrowed) + .ok_or_else(|| RenderError::new(format!("Template not found: {}", 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()) + pub(crate) fn get_or_load_helper( + &'reg self, + name: &str, + ) -> Result>, RenderError> { + #[cfg(feature = "script_helper")] + if let (true, Some(source)) = (self.dev_mode, self.script_sources.get(name)) { + return source + .load() + .map_err(ScriptError::from) + .and_then(|s| { + let helper = Box::new(ScriptHelper { + script: self.engine.compile(&s)?, + }) as Box; + Ok(Some(helper.into())) + }) + .map_err(RenderError::from); + } + + Ok(self.helpers.get(name).cloned()) } #[inline] @@ -403,7 +439,10 @@ impl<'reg> Registry<'reg> { } /// Return a registered decorator - pub fn get_decorator(&self, name: &str) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> { + pub(crate) fn get_decorator( + &self, + name: &str, + ) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> { self.decorators.get(name).map(|v| v.as_ref()) } @@ -417,6 +456,7 @@ impl<'reg> Registry<'reg> { self.templates.clear(); } + #[inline] fn render_to_output( &self, name: &str, @@ -426,12 +466,10 @@ impl<'reg> Registry<'reg> { 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) - }) + 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 @@ -469,11 +507,7 @@ impl<'reg> Registry<'reg> { } /// Render a template string using current registry without registering it - pub fn render_template( - &self, - template_string: &str, - data: &T, - ) -> Result + pub fn render_template(&self, template_string: &str, data: &T) -> Result where T: Serialize, { @@ -487,8 +521,8 @@ impl<'reg> Registry<'reg> { &self, template_string: &str, ctx: &Context, - ) -> Result { - let tpl = Template::compile2(template_string, self.source_map)?; + ) -> Result { + let tpl = Template::compile(template_string)?; let mut out = StringOutput::new(); { @@ -496,8 +530,7 @@ impl<'reg> Registry<'reg> { tpl.render(self, &ctx, &mut render_context, &mut out)?; } - out.into_string() - .map_err(|e| TemplateRenderError::from(RenderError::from(e))) + out.into_string().map_err(RenderError::from) } /// Render a template string using current registry without registering it @@ -506,36 +539,16 @@ impl<'reg> Registry<'reg> { template_string: &str, data: &T, writer: W, - ) -> Result<(), TemplateRenderError> + ) -> Result<(), RenderError> where T: Serialize, W: Write, { - let tpl = Template::compile2(template_string, self.source_map)?; + let tpl = Template::compile(template_string)?; 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) } } @@ -549,11 +562,8 @@ mod test { 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::fs::File; use std::io::Write; - #[cfg(feature = "dir_source")] use tempfile::tempdir; #[derive(Clone, Copy)] @@ -606,6 +616,8 @@ mod test { #[test] #[cfg(feature = "dir_source")] fn test_register_templates_directory() { + use std::fs::DirBuilder; + let mut r = Registry::new(); { let dir = tempdir().unwrap(); @@ -782,10 +794,7 @@ mod test { 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 - ); + assert_eq!(render_error.column_no.unwrap(), 26); let data2 = json!([1, 2, 3]); assert!(r @@ -797,10 +806,7 @@ mod test { 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 - ); + assert_eq!(render_error2.column_no.unwrap(), 31); } use crate::json::value::ScopedJson; @@ -959,4 +965,84 @@ mod test { .unwrap() ); } + + #[test] + fn test_dev_mode_template_reload() { + let mut reg = Registry::new(); + reg.set_dev_mode(true); + assert!(reg.dev_mode()); + + let dir = tempdir().unwrap(); + let file1_path = dir.path().join("t1.hbs"); + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "

Hello {{{{name}}}}!

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

Hello Alex!

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

Privet {{{{name}}}}!

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

Privet Alex!

" + ); + + dir.close().unwrap(); + } + + #[test] + #[cfg(feature = "script_helper")] + fn test_script_helper() { + let mut reg = Registry::new(); + + reg.register_script_helper("acc", "params.reduce(|sum, x| x + sum, || 0 )") + .unwrap(); + + assert_eq!( + reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), + "10" + ); + } + + #[test] + #[cfg(feature = "script_helper")] + fn test_script_helper_dev_mode() { + let mut reg = Registry::new(); + reg.set_dev_mode(true); + + let dir = tempdir().unwrap(); + let file1_path = dir.path().join("acc.rhai"); + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "params.reduce(|sum, x| x + sum, || 0 )").unwrap(); + } + + reg.register_script_helper_file("acc", &file1_path).unwrap(); + + assert_eq!( + reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), + "10" + ); + + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "params.reduce(|sum, x| x * sum, || 1 )").unwrap(); + } + + assert_eq!( + reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), + "24" + ); + + dir.close().unwrap(); + } } diff --git a/src/render.rs b/src/render.rs index 15ad11ad9..d22d6ddea 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,7 +1,6 @@ use std::borrow::{Borrow, Cow}; use std::collections::{BTreeMap, VecDeque}; use std::fmt; -use std::ops::Deref; use std::rc::Rc; use serde_json::value::Value as Json; @@ -40,7 +39,7 @@ pub struct RenderContext<'reg, 'rc> { #[derive(Clone)] pub struct RenderContextInner<'reg: 'rc, 'rc> { partials: BTreeMap, - local_helpers: BTreeMap>, + local_helpers: BTreeMap>, /// current template name current_template: Option<&'reg String>, /// root template name @@ -192,11 +191,11 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { pub fn register_local_helper( &mut self, name: &str, - def: Box, - ) -> Option> { + def: Box, + ) { self.inner_mut() .local_helpers - .insert(name.to_string(), def.into()) + .insert(name.to_string(), def.into()); } /// Remove a helper from render context @@ -205,7 +204,7 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { } /// Attempt to get a helper from current render context. - pub fn get_local_helper(&self, name: &str) -> Option> { + pub fn get_local_helper(&self, name: &str) -> Option> { self.inner().local_helpers.get(name).cloned() } @@ -504,6 +503,7 @@ pub trait Evaluable { ) -> Result<(), RenderError>; } +#[inline] fn call_helper_for_value<'reg: 'rc, 'rc>( hd: &dyn HelperDef, ht: &Helper<'reg, 'rc>, @@ -584,22 +584,25 @@ impl Parameter { let h = Helper::try_from_template(ht, registry, ctx, rc)?; if let Some(ref d) = rc.get_local_helper(&name) { - let helper_def = d.deref(); - call_helper_for_value(helper_def, &h, registry, ctx, rc) + call_helper_for_value(d.as_ref(), &h, registry, ctx, rc) } else { - registry - .get_helper(&name) - .or_else(|| { - registry.get_helper(if ht.block { - BLOCK_HELPER_MISSING - } else { - HELPER_MISSING - }) - }) + let mut helper = registry.get_or_load_helper(&name)?; + + if helper.is_none() { + helper = registry.get_or_load_helper(if ht.block { + BLOCK_HELPER_MISSING + } else { + HELPER_MISSING + })?; + } + + helper .ok_or_else(|| { RenderError::new(format!("Helper not defined: {:?}", ht.name)) }) - .and_then(move |d| call_helper_for_value(d, &h, registry, ctx, rc)) + .and_then(|d| { + call_helper_for_value(d.as_ref(), &h, registry, ctx, rc) + }) } } } @@ -624,11 +627,9 @@ impl Renderable for Template { t.render(registry, ctx, rc, out).map_err(|mut e| { // add line/col number if the template has mapping data if e.line_no.is_none() { - if let Some(ref mapping) = self.mapping { - if let Some(&TemplateMapping(line, col)) = mapping.get(idx) { - e.line_no = Some(line); - e.column_no = Some(col); - } + if let Some(&TemplateMapping(line, col)) = self.mapping.get(idx) { + e.line_no = Some(line); + e.column_no = Some(col); } } @@ -655,11 +656,9 @@ impl Evaluable for Template { for (idx, t) in iter.enumerate() { t.eval(registry, ctx, rc).map_err(|mut e| { if e.line_no.is_none() { - if let Some(ref mapping) = self.mapping { - if let Some(&TemplateMapping(line, col)) = mapping.get(idx) { - e.line_no = Some(line); - e.column_no = Some(col); - } + if let Some(&TemplateMapping(line, col)) = self.mapping.get(idx) { + e.line_no = Some(line); + e.column_no = Some(col); } } @@ -679,6 +678,7 @@ fn helper_exists<'reg: 'rc, 'rc>( rc.has_local_helper(name) || reg.has_helper(name) } +#[inline] fn render_helper<'reg: 'rc, 'rc>( ht: &'reg HelperTemplate, registry: &'reg Registry<'reg>, @@ -696,17 +696,19 @@ fn render_helper<'reg: 'rc, 'rc>( if let Some(ref d) = rc.get_local_helper(h.name()) { d.call(&h, registry, ctx, rc, out) } else { - registry - .get_helper(h.name()) - .or_else(|| { - registry.get_helper(if ht.block { - BLOCK_HELPER_MISSING - } else { - HELPER_MISSING - }) - }) - .ok_or_else(|| RenderError::new(format!("Helper not defined: {:?}", ht.name))) - .and_then(move |d| d.call(&h, registry, ctx, rc, out)) + let mut helper = registry.get_or_load_helper(h.name())?; + + if helper.is_none() { + helper = registry.get_or_load_helper(if ht.block { + BLOCK_HELPER_MISSING + } else { + HELPER_MISSING + })?; + } + + helper + .ok_or_else(|| RenderError::new(format!("Helper not defined: {:?}", h.name()))) + .and_then(|d| d.call(&h, registry, ctx, rc, out)) } } @@ -750,7 +752,7 @@ impl Renderable for TemplateElement { Err(RenderError::strict_error(context_json.relative_path())) } else { // helper missing - if let Some(hook) = registry.get_helper(HELPER_MISSING) { + if let Some(hook) = registry.get_or_load_helper(HELPER_MISSING)? { let h = Helper::try_from_template(ht, registry, ctx, rc)?; hook.call(&h, registry, ctx, rc, out) } else { @@ -891,7 +893,7 @@ fn test_template() { let template = Template { elements, name: None, - mapping: None, + mapping: Vec::new(), }; { diff --git a/src/sources.rs b/src/sources.rs new file mode 100644 index 000000000..8c8b2ba57 --- /dev/null +++ b/src/sources.rs @@ -0,0 +1,34 @@ +use std::fs::File; +use std::io::{BufReader, Error as IOError, Read}; +use std::path::PathBuf; + +pub(crate) trait Source { + type Item; + type Error; + + fn load(&self) -> Result; +} + +pub(crate) struct FileSource { + path: PathBuf, +} + +impl FileSource { + pub(crate) fn new(path: PathBuf) -> FileSource { + FileSource { path } + } +} + +impl Source for FileSource { + type Item = String; + type Error = IOError; + + fn load(&self) -> Result { + let mut reader = BufReader::new(File::open(&self.path)?); + + let mut buf = String::new(); + reader.read_to_string(&mut buf)?; + + Ok(buf) + } +} diff --git a/src/template.rs b/src/template.rs index 5c819902d..b9240eedb 100644 --- a/src/template.rs +++ b/src/template.rs @@ -18,11 +18,11 @@ use self::TemplateElement::*; pub struct TemplateMapping(pub usize, pub usize); /// A handlebars template -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Default)] pub struct Template { pub name: Option, pub elements: Vec, - pub mapping: Option>, + pub mapping: Vec, } #[derive(PartialEq, Clone, Debug)] @@ -167,23 +167,13 @@ impl Parameter { } impl Template { - pub fn new(mapping: bool) -> Template { - Template { - elements: Vec::new(), - name: None, - mapping: if mapping { Some(Vec::new()) } else { None }, - } + pub fn new() -> Template { + Template::default() } fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) { self.elements.push(e); - if let Some(ref mut maps) = self.mapping { - maps.push(TemplateMapping(line, col)); - } - } - - pub fn compile>(source: S) -> Result { - Template::compile2(source, false) + self.mapping.push(TemplateMapping(line, col)); } fn parse_subexpression<'a, I>( @@ -289,11 +279,7 @@ impl Template { Ok((key, value)) } - fn parse_block_param<'a, I>( - _: &'a str, - it: &mut Peekable, - limit: usize, - ) -> Result + fn parse_block_param<'a, I>(_: &'a str, it: &mut Peekable, limit: usize) -> BlockParam where I: Iterator>, { @@ -313,9 +299,9 @@ impl Template { if let Some(p2) = p2 { it.next(); - Ok(BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2)))) + BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2))) } else { - Ok(BlockParam::Single(Parameter::Name(p1))) + BlockParam::Single(Parameter::Name(p1)) } } @@ -366,7 +352,7 @@ impl Template { hashes.insert(key, value); } Rule::block_param => { - block_param = Some(Template::parse_block_param(source, it.by_ref(), end)?); + block_param = Some(Template::parse_block_param(source, it.by_ref(), end)); } Rule::pro_whitespace_omitter => { omit_pro_ws = true; @@ -434,10 +420,7 @@ impl Template { } } - pub fn compile2<'a, S: AsRef + 'a>( - source: S, - mapping: bool, - ) -> Result { + pub fn compile<'a, S: AsRef + 'a>(source: S) -> Result { let source = source.as_ref(); let mut helper_stack: VecDeque = VecDeque::new(); let mut decorator_stack: VecDeque = VecDeque::new(); @@ -477,7 +460,7 @@ impl Template { // trailing string check let (line_no, col_no) = span.start_pos().line_col(); if rule == Rule::raw_block_end { - let mut t = Template::new(mapping); + let mut t = Template::new(); t.push_element( Template::raw_string(&source[prev_end..span.start()], None, false), line_no, @@ -497,7 +480,7 @@ impl Template { let (line_no, col_no) = span.start_pos().line_col(); match rule { Rule::template => { - template_stack.push_front(Template::new(mapping)); + template_stack.push_front(Template::new()); } Rule::raw_text => { // leading space fix @@ -555,9 +538,7 @@ impl Template { omit_pro_ws = exp.omit_pro_ws; let t = template_stack.front_mut().unwrap(); - if let Some(ref mut maps) = t.mapping { - maps.push(TemplateMapping(line_no, col_no)); - } + t.mapping.push(TemplateMapping(line_no, col_no)); } Rule::invert_tag => { // hack: invert_tag structure is similar to ExpressionSpec, so I @@ -574,7 +555,7 @@ impl Template { h.template = Some(t); } Rule::raw_block_text => { - let mut t = Template::new(mapping); + let mut t = Template::new(); t.push_element( Template::raw_string(span.as_str(), Some(pair.clone()), omit_pro_ws), line_no, @@ -717,9 +698,8 @@ impl Template { pub fn compile_with_name>( source: S, name: String, - mapping: bool, ) -> Result { - match Template::compile2(source, mapping) { + match Template::compile(source) { Ok(mut t) => { t.name = Some(name); Ok(t) @@ -843,12 +823,11 @@ fn test_parse_block_partial_path_identifier() { fn test_parse_error() { let source = "{{#ifequals name compare=\"hello\"}}\nhello\n\t{{else}}\ngood"; - let t = Template::compile(source.to_string()); + let terr = Template::compile(source.to_string()).unwrap_err(); - assert_eq!( - t.unwrap_err(), - TemplateError::of(TemplateErrorReason::InvalidSyntax).at(source, 4, 5) - ); + assert!(matches!(terr.reason, TemplateErrorReason::InvalidSyntax)); + assert_eq!(terr.line_no.unwrap(), 4); + assert_eq!(terr.column_no.unwrap(), 5); } #[test] @@ -1029,16 +1008,12 @@ fn test_literal_parameter_parser() { #[test] fn test_template_mapping() { - match Template::compile2("hello\n {{~world}}\n{{#if nice}}\n\thello\n{{/if}}", true) { + match Template::compile("hello\n {{~world}}\n{{#if nice}}\n\thello\n{{/if}}") { Ok(t) => { - if let Some(ref mapping) = t.mapping { - assert_eq!(mapping.len(), t.elements.len()); - assert_eq!(mapping[0], TemplateMapping(1, 1)); - assert_eq!(mapping[1], TemplateMapping(2, 3)); - assert_eq!(mapping[3], TemplateMapping(3, 1)); - } else { - panic!("should contains mapping"); - } + assert_eq!(t.mapping.len(), t.elements.len()); + assert_eq!(t.mapping[0], TemplateMapping(1, 1)); + assert_eq!(t.mapping[1], TemplateMapping(2, 3)); + assert_eq!(t.mapping[3], TemplateMapping(3, 1)); } Err(e) => panic!("{}", e), }