diff --git a/.travis.yml b/.travis.yml index bf31b25d3..aa3a1bd63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,5 @@ before_script: script: - | cargo test && + cargo test --features partial4 && travis-cargo --only nightly test -- --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 76365ee81..5229afce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ default = ["rustc_ser_type"] rustc_ser_type = ["rustc-serialize"] serde_type = ["serde", "serde_json"] unstable = ["serde_type", "serde_macros"] +partial4 = [] [dev-dependencies] env_logger = "^0.3.2" diff --git a/src/directives/inline.rs b/src/directives/inline.rs new file mode 100644 index 000000000..44aba1bac --- /dev/null +++ b/src/directives/inline.rs @@ -0,0 +1,80 @@ +use directives::DirectiveDef; +use registry::Registry; +use context::Context; +use render::{RenderError, RenderContext, Directive}; + +#[derive(Clone, Copy)] +pub struct InlineDirective; + +#[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] +fn get_name<'a>(d: &'a Directive) -> Result<&'a str, RenderError> { + d.param(0) + .ok_or_else(|| RenderError::new("Param required for directive \"inline\"")) + .and_then(|v| { + v.value() + .as_string() + .ok_or_else(|| RenderError::new("inline name must be string")) + }) +} + +#[cfg(feature = "serde_type")] +fn get_name<'a>(d: &'a Directive) -> Result<&'a str, RenderError> { + d.param(0) + .ok_or_else(|| RenderError::new("Param required for directive \"inline\"")) + .and_then(|v| { + v.value() + .as_str() + .ok_or_else(|| RenderError::new("inline name must be string")) + }) +} + +impl DirectiveDef for InlineDirective { + fn call(&self, + _: &Context, + d: &Directive, + _: &Registry, + rc: &mut RenderContext) + -> Result<(), RenderError> { + let name = try!(get_name(d)); + + let template = try!(d.template() + .ok_or_else(|| RenderError::new("inline should have a block"))); + + + rc.set_partial(name.to_owned(), template.clone()); + Ok(()) + } +} + +pub static INLINE_DIRECTIVE: InlineDirective = InlineDirective; + +#[cfg(test)] +mod test { + use template::Template; + use registry::Registry; + use context::Context; + use render::{RenderContext, Evalable}; + use support::str::StringWriter; + + #[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] + use serialize::json::Json; + #[cfg(feature = "serde_type")] + use serde_json::value::Value as Json; + + #[test] + fn test_inline() { + let t0 = + Template::compile("{{#*inline \"hello\"}}the hello world inline partial.{{/inline}}" + .to_string()) + .ok() + .unwrap(); + + let hbs = Registry::new(); + + let mut sw = StringWriter::new(); + let mut rc = RenderContext::new(&mut sw); + t0.elements[0].eval(&Context::wraps(&Json::Null), &hbs, &mut rc).unwrap(); + + assert!(rc.get_partial(&"hello".to_owned()).is_some()); + } +} diff --git a/src/directives/mod.rs b/src/directives/mod.rs new file mode 100644 index 000000000..ef92528ae --- /dev/null +++ b/src/directives/mod.rs @@ -0,0 +1,26 @@ +use render::{RenderContext, RenderError, Directive}; +use registry::Registry; +use context::Context; + +pub use self::inline::INLINE_DIRECTIVE; + +/// Directive Definition +/// +/// Implement this trait to define your own decorators or directives +pub trait DirectiveDef: Send + Sync { + fn call(&self, + ctx: &Context, + d: &Directive, + r: &Registry, + rc: &mut RenderContext) + -> Result<(), RenderError>; +} + +/// implement DirectiveDef for bare function so we can use function as directive +impl Fn(&'a Context, &'b Directive, &'c Registry, &'d mut RenderContext) -> Result<(), RenderError>> DirectiveDef for F { + fn call(&self, ctx: &Context, d: &Directive, r: &Registry, rc: &mut RenderContext) -> Result<(), RenderError>{ + (*self)(ctx, d, r, rc) + } +} + +mod inline; diff --git a/src/error.rs b/src/error.rs index 455faf1bd..2ca08689e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,6 +11,11 @@ quick_error! { open, closed, line_no, col_no) description("wrong name of closing helper") } + MismatchingClosedDirective(line_no: usize, col_no: usize, open: String, closed: String) { + display("directive {:?} was opened, but {:?} is closing at line {:?}, column {:?}", + open, closed, line_no, col_no) + description("wrong name of closing directive") + } InvalidSyntax (line_no: usize, col_no: usize) { display("invalid handlebars syntax at line {:?}, column {:?}", line_no, col_no) description("invalid handlebars syntax") diff --git a/src/grammar.rs b/src/grammar.rs index d61d9060b..2e21874a8 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -1,5 +1,6 @@ use pest::prelude::*; +#[cfg(not(feature="partial4"))] impl_rdp! { grammar! { whitespace = _{ [" "]|["\t"]|["\n"]|["\r"] } @@ -25,7 +26,7 @@ impl_rdp! { object_literal = { ["{"] ~ (string_literal ~ [":"] ~ literal)? ~ ([","] ~ string_literal ~ [":"] ~ literal)* ~ ["}"] } // FIXME: a[0], a["b] - symbol_char = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["."]|["@"]|["$"]|["<"]|[">"]|["-"] } + symbol_char = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["."]|["@"]|["$"]|["-"]|["<"]|[">"] } path_char = _{ ["/"] } identifier = @{ symbol_char ~ ( symbol_char | path_char )* } @@ -51,6 +52,129 @@ impl_rdp! { helper_expression = { !invert_tag ~ ["{{"] ~ pre_whitespace_omitter? ~ exp_line ~ pro_whitespace_omitter? ~ ["}}"] } + directive_expression = { ["{{"] ~ pre_whitespace_omitter? ~ ["*"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + partial_expression = { ["{{"] ~ pre_whitespace_omitter? ~ [">"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + + invert_tag = { ["{{else}}"]|["{{^}}"] } + helper_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + helper_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~ + pro_whitespace_omitter? ~ ["}}"] } + helper_block = _{ helper_block_start ~ template ~ + (invert_tag ~ template)? ~ + helper_block_end } + + directive_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ ["*"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + directive_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~ + pro_whitespace_omitter? ~ ["}}"] } + directive_block = _{ directive_block_start ~ template ~ + directive_block_end } + + partial_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ [">"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + partial_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~ + pro_whitespace_omitter? ~ ["}}"] } + partial_block = _{ partial_block_start ~ template ~ partial_block_end } + + raw_block_start = { ["{{{{"] ~ pre_whitespace_omitter? ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}}}"] } + raw_block_end = { ["{{{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~ + pro_whitespace_omitter? ~ ["}}}}"] } + raw_block = _{ raw_block_start ~ raw_block_text ~ raw_block_end } + + hbs_comment = { ["{{!"] ~ (!["}}"] ~ any)* ~ ["}}"] } + + template = { ( + raw_text | + expression | + html_expression | + helper_expression | + helper_block | + raw_block | + hbs_comment | + directive_expression | + directive_block )* + } + + parameter = _{ param ~ eoi } + handlebars = _{ template ~ eoi } + +// json path visitor + path_ident = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["@"]|["$"]|["<"]|[">"]|["-"]} + path_id = { path_ident+ } + path_num_id = { ['0'..'9']+ } + path_raw_id = { path_ident* } + path_this = { ["this"] } + path_sep = _{ ["/"] | ["."] } + path_up = { [".."] } + path_var = { path_id } + path_key = { ["["] ~ (["\""]|["'"])? ~ path_raw_id ~ (["\""]|["'"])? ~ ["]"] } + path_idx = { ["["] ~ path_num_id ~ ["]"]} + path_item = _{ path_this|path_up|path_var } + path = _{ ["./"]? ~ path_item ~ ((path_sep ~ path_item) | (path_sep? ~ (path_key | path_idx)))* ~ eoi } + } +} + +#[cfg(feature="partial4")] +impl_rdp! { + grammar! { + whitespace = _{ [" "]|["\t"]|["\n"]|["\r"] } + + raw_text = @{ ( !["{{"] ~ any )+ } + raw_block_text = @{ ( !["{{{{"] ~ any )* } + +// Note: this is not full and strict json literal definition, just for tokenize string, +// array and object types which may contains whitespace. We will use a real json parser +// for real json processing + literal = { string_literal | + array_literal | + object_literal | + number_literal | + null_literal | + boolean_literal } + + null_literal = { ["null"] } + boolean_literal = { ["true"]|["false"] } + number_literal = @{ ["-"]? ~ ['0'..'9']+ ~ ["."]? ~ ['0'..'9']* ~ (["E"] ~ ["-"]? ~ ['0'..'9']+)? } + string_literal = @{ ["\""] ~ (!["\""] ~ (["\\\""] | any))* ~ ["\""] } + array_literal = { ["["] ~ literal? ~ ([","] ~ literal)* ~ ["]"] } + object_literal = { ["{"] ~ (string_literal ~ [":"] ~ literal)? ~ ([","] ~ string_literal ~ [":"] ~ literal)* ~ ["}"] } + +// FIXME: a[0], a["b] + symbol_char = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["."]|["@"]|["$"]|["-"] } + path_char = _{ ["/"] } + + identifier = @{ symbol_char ~ ( symbol_char | path_char )* } + reference = @{ identifier ~ (["["] ~ (string_literal|['0'..'9']+) ~ ["]"])* ~ ["-"]* ~ reference* } + name = _{ subexpression | reference } + + param = { !["as"] ~ (literal | reference | subexpression) } + hash = { identifier ~ ["="] ~ param } + block_param = { ["as"] ~ ["|"] ~ identifier ~ identifier? ~ ["|"]} + exp_line = _{ identifier ~ (hash|param)* ~ block_param?} + + subexpression = { ["("] ~ name ~ (hash|param)* ~ [")"] } + + pre_whitespace_omitter = { ["~"] } + pro_whitespace_omitter = { ["~"] } + + expression = { !invert_tag ~ ["{{"] ~ pre_whitespace_omitter? ~ name ~ + pro_whitespace_omitter? ~ ["}}"] } + + html_expression = { ["{{{"] ~ pre_whitespace_omitter? ~ name ~ + pro_whitespace_omitter? ~ ["}}}"] } + + helper_expression = { !invert_tag ~ ["{{"] ~ pre_whitespace_omitter? ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + + directive_expression = { ["{{"] ~ pre_whitespace_omitter? ~ ["*"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + partial_expression = { ["{{"] ~ pre_whitespace_omitter? ~ [">"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + invert_tag = { ["{{else}}"]|["{{^}}"] } helper_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ exp_line ~ pro_whitespace_omitter? ~ ["}}"] } @@ -58,7 +182,20 @@ impl_rdp! { pro_whitespace_omitter? ~ ["}}"] } helper_block = _{ helper_block_start ~ template ~ (invert_tag ~ template)? ~ - helper_block_end } + helper_block_end } + + directive_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ ["*"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + directive_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~ + pro_whitespace_omitter? ~ ["}}"] } + directive_block = _{ directive_block_start ~ template ~ + directive_block_end } + + partial_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ [">"] ~ exp_line ~ + pro_whitespace_omitter? ~ ["}}"] } + partial_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~ + pro_whitespace_omitter? ~ ["}}"] } + partial_block = _{ partial_block_start ~ template ~ partial_block_end } raw_block_start = { ["{{{{"] ~ pre_whitespace_omitter? ~ exp_line ~ pro_whitespace_omitter? ~ ["}}}}"] } @@ -75,7 +212,11 @@ impl_rdp! { helper_expression | helper_block | raw_block | - hbs_comment )* + hbs_comment | + directive_expression | + directive_block | + partial_expression | + partial_block )* } parameter = _{ param ~ eoi } @@ -331,3 +472,45 @@ fn test_path() { assert!(rdp.end()); } } + +#[test] +fn test_directive_expression() { + let s = vec!["{{* ssh}}", "{{~* ssh}}"]; + for i in s.iter() { + let mut rdp = Rdp::new(StringInput::new(i)); + assert!(rdp.directive_expression()); + assert!(rdp.end()); + } +} + +#[test] +fn test_directive_block() { + let s = vec!["{{#* inline}}something{{/inline}}", + "{{~#* inline}}hello{{/inline}}", + "{{#* inline \"partialname\"}}something{{/inline}}"]; + for i in s.iter() { + let mut rdp = Rdp::new(StringInput::new(i)); + assert!(rdp.directive_block()); + assert!(rdp.end()); + } +} + +#[test] +fn test_partial_expression() { + let s = vec!["{{> hello}}"]; + for i in s.iter() { + let mut rdp = Rdp::new(StringInput::new(i)); + assert!(rdp.partial_expression()); + assert!(rdp.end()); + } +} + +#[test] +fn test_partial_block() { + let s = vec!["{{#> hello}}nice{{/hello}}"]; + for i in s.iter() { + let mut rdp = Rdp::new(StringInput::new(i)); + assert!(rdp.partial_block()); + assert!(rdp.end()); + } +} diff --git a/src/helpers/helper_lookup.rs b/src/helpers/helper_lookup.rs index ac65b6aa4..a1b2dfd6c 100644 --- a/src/helpers/helper_lookup.rs +++ b/src/helpers/helper_lookup.rs @@ -12,7 +12,6 @@ use render::{RenderContext, RenderError, Helper}; pub struct LookupHelper; impl HelperDef for LookupHelper { - #[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] fn call(&self, _: &Context, h: &Helper, @@ -35,42 +34,14 @@ impl HelperDef for LookupHelper { .and_then(|u| v.get(u)) .unwrap_or(&null) } + #[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] &Json::Object(ref m) => { index.value() .as_string() .and_then(|k| m.get(k)) .unwrap_or(&null) } - _ => &null, - }; - let r = value.render(); - try!(rc.writer.write(r.into_bytes().as_ref())); - Ok(()) - } - - #[cfg(feature = "serde_type")] - fn call(&self, - _: &Context, - h: &Helper, - _: &Registry, - rc: &mut RenderContext) - -> Result<(), RenderError> { - let collection_value = try!(h.param(0).ok_or_else(|| { - RenderError::new("Param not found for helper \"lookup\"") - })); - let index = try!(h.param(1).ok_or_else(|| { - RenderError::new("Insufficient params for helper \"lookup\"") - })); - - let null = Json::Null; - let value = match collection_value.value() { - &Json::Array(ref v) => { - index.value() - .as_u64() - .and_then(|u| Some(u as usize)) - .and_then(|u| v.get(u)) - .unwrap_or(&null) - } + #[cfg(feature = "serde_type")] &Json::Object(ref m) => { index.value() .as_str() diff --git a/src/helpers/helper_partial.rs b/src/helpers/helper_partial.rs index 8955c5495..ecb075567 100644 --- a/src/helpers/helper_partial.rs +++ b/src/helpers/helper_partial.rs @@ -5,6 +5,8 @@ use helpers::HelperDef; use registry::Registry; use context::Context; use render::{Renderable, RenderContext, RenderError, Helper}; +#[cfg(feature = "partial4")] +use render::Evalable; #[derive(Clone, Copy)] pub struct IncludeHelper; @@ -15,6 +17,7 @@ pub struct BlockHelper; #[derive(Clone, Copy)] pub struct PartialHelper; +#[cfg(not(feature = "partial4"))] impl HelperDef for IncludeHelper { fn call(&self, c: &Context, @@ -22,20 +25,17 @@ impl HelperDef for IncludeHelper { r: &Registry, rc: &mut RenderContext) -> Result<(), RenderError> { - let template = match h.params().get(0) { - Some(ref t) => { - if let Some(include_path) = t.path() { - if rc.current_template == Some(include_path.to_owned()) { - return Err(RenderError::new("Cannot include self in >")); - } else { - r.get_template(&include_path) - } - } else { - return Err(RenderError::new("Do not use literal here, use partial name directly.")); - } - } - None => return Err(RenderError::new("Param not found for helper")), - }; + let template = try!(h.params().get(0) + .ok_or(RenderError::new("Param not found for helper")) + .and_then(|ref t|{ + t.path() + .ok_or(RenderError::new("Do not use literal here, use partial name directly.")) + .and_then(|p| { + if rc.is_current_template(p){ + Err(RenderError::new("Cannot include self in >")) + } else { + Ok(r.get_template(&p)) + }})})); let context_param = h.params().get(1).and_then(|p| p.path()); let old_path = match context_param { @@ -75,6 +75,74 @@ impl HelperDef for IncludeHelper { } } +#[cfg(feature = "partial4")] +impl HelperDef for IncludeHelper { + fn call(&self, + c: &Context, + h: &Helper, + r: &Registry, + rc: &mut RenderContext) + -> Result<(), RenderError> { + + // try eval inline partials first + if let Some(t) = h.template() { + try!(t.eval(c, r, rc)); + } + + let tname = try!(h.params().get(0) + .ok_or(RenderError::new("Param not found for helper")) + .and_then(|ref t|{ + t.path() + .ok_or(RenderError::new("Do not use literal here, use partial name directly.")) + .and_then(|p| { + if rc.is_current_template(p) { + Err(RenderError::new("Cannot include self in >")) + } else { + Ok(p) + } + }) + })); + + let partial = rc.get_partial(tname); + let render_template = partial.as_ref().or(r.get_template(tname)).or(h.template()); + match render_template { + Some(t) => { + let context_param = h.params().get(1).and_then(|p| p.path()); + let old_path = match context_param { + Some(p) => { + let old_path = rc.get_path().clone(); + rc.promote_local_vars(); + let new_path = format!("{}/{}", old_path, p); + rc.set_path(new_path); + Some(old_path) + } + None => None, + }; + + let r = if h.hash().is_empty() { + t.render(c, r, rc) + } else { + let hash_ctx = BTreeMap::from_iter(h.hash() + .iter() + .map(|(k, v)| { + (k.clone(), v.value().clone()) + })); + let new_ctx = c.extend(&hash_ctx); + t.render(&new_ctx, r, rc) + }; + + if let Some(path) = old_path { + rc.set_path(path); + rc.demote_local_vars(); + } + + r + } + None => Ok(()), + } + } +} + impl HelperDef for BlockHelper { fn call(&self, c: &Context, @@ -115,10 +183,13 @@ impl HelperDef for PartialHelper { } pub static INCLUDE_HELPER: IncludeHelper = IncludeHelper; +#[cfg(not(feature = "partial4"))] pub static BLOCK_HELPER: BlockHelper = BlockHelper; +#[cfg(not(feature = "partial4"))] pub static PARTIAL_HELPER: PartialHelper = PartialHelper; #[cfg(test)] +#[cfg(not(feature = "partial4"))] mod test { use template::Template; use registry::Registry; @@ -207,6 +278,33 @@ mod test { assert_eq!(r1.ok().unwrap(), "Partial not found".to_string()); } + #[test] + #[cfg(feature = "partial4")] + fn test_inline_partial4() { + let t0 = Template::compile("{{*inline \"title\"}}hello {{name}}{{/inline}}

include \ + partial: {{> title}}

" + .to_string()) + .ok() + .unwrap(); + // let t1 = Template::compile("{{#> none_partial}}Partial not found{{/block}}".to_string()) + // .ok() + // .unwrap(); + + let mut handlebars = Registry::new(); + handlebars.register_template("t0", t0); + // handlebars.register_template("t1", t1); + + let mut map: BTreeMap = BTreeMap::new(); + map.insert("name".into(), "world".into()); + + let r0 = handlebars.render("t0", &map); + assert_eq!(r0.ok().unwrap(), + "

include partial: hello world

".to_string()); + + // let r1 = handlebars.render("t1", &map); + // assert_eq!(r1.ok().unwrap(), "Partial not found".to_string()); + } + #[test] fn test_include_self() { let t0 = Template::compile("

{{> t0}}

".to_string()).ok().unwrap(); diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 26e3635b5..3a1fd27df 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -7,7 +7,10 @@ pub use self::helper_each::EACH_HELPER; pub use self::helper_with::WITH_HELPER; pub use self::helper_lookup::LOOKUP_HELPER; pub use self::helper_raw::RAW_HELPER; +#[cfg(not(feature = "partial4"))] pub use self::helper_partial::{INCLUDE_HELPER, BLOCK_HELPER, PARTIAL_HELPER}; +#[cfg(feature = "partial4")] +pub use self::helper_partial::INCLUDE_HELPER; pub use self::helper_log::LOG_HELPER; /// Helper Definition diff --git a/src/lib.rs b/src/lib.rs index bf9bf2c9d..1befa94ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,7 +244,7 @@ //! * `{{log ...}}` log value with rust logger, default level: INFO. Currently you cannot change the level. //! - +#![allow(dead_code)] #![recursion_limit = "200"] #[macro_use] @@ -282,3 +282,6 @@ mod render; mod helpers; mod context; mod support; +mod directives; +#[cfg(feature="partial4")] +mod partial; diff --git a/src/partial.rs b/src/partial.rs new file mode 100644 index 000000000..868673f69 --- /dev/null +++ b/src/partial.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; +use std::iter::FromIterator; + +use registry::Registry; +use context::Context; +use render::{RenderError, RenderContext, Directive, Evalable, Renderable}; + +pub fn expand_partial(c: &Context, + d: &Directive, + r: &Registry, + rc: &mut RenderContext) + -> Result<(), RenderError> { + + // try eval inline partials first + if let Some(t) = d.template() { + try!(t.eval(c, r, rc)); + } + + if rc.is_current_template(d.name()) { + return Err(RenderError::new("Cannot include self in >")); + } + + + let tname = d.name(); + let partial = rc.get_partial(tname); + let render_template = partial.as_ref().or(r.get_template(tname)).or(d.template()); + match render_template { + Some(t) => { + let context_param = d.params().get(0).and_then(|p| p.path()); + let old_path = match context_param { + Some(p) => { + let old_path = rc.get_path().clone(); + rc.promote_local_vars(); + let new_path = format!("{}/{}", old_path, p); + rc.set_path(new_path); + Some(old_path) + } + None => None, + }; + + let hash = d.hash(); + let r = if hash.is_empty() { + t.render(c, r, rc) + } else { + let hash_ctx = BTreeMap::from_iter(hash.iter() + .map(|(k, v)| { + (k.clone(), v.value().clone()) + })); + let new_ctx = c.extend(&hash_ctx); + t.render(&new_ctx, r, rc) + }; + + if let Some(path) = old_path { + rc.set_path(path); + rc.demote_local_vars(); + } + + r + } + None => Ok(()), + } + +} + +#[cfg(test)] +mod test { + use template::Template; + use registry::Registry; + + #[test] + fn test() { + let t0 = Template::compile("{{> t1}}".to_string()).ok().unwrap(); + let t1 = Template::compile("{{this}}".to_string()).ok().unwrap(); + let t2 = Template::compile("{{#> t99}}not there{{/t99}}".to_string()).ok().unwrap(); + let t3 = Template::compile("{{#*inline \"t31\"}}{{this}}{{/inline}}{{> t31}}".to_string()) + .ok() + .unwrap(); + let t4 = Template::compile("{{#> t5}}{{#*inline \"nav\"}}navbar{{/inline}}{{/t5}}" + .to_string()) + .ok() + .unwrap(); + let t5 = Template::compile("include {{> nav}}".to_string()).ok().unwrap(); + let t6 = Template::compile("{{> t1 a}}".to_string()).ok().unwrap(); + let t7 = Template::compile("{{#*inline \"t71\"}}{{a}}{{/inline}}{{> t71 a=\"world\"}}") + .ok() + .unwrap(); + + let mut handlebars = Registry::new(); + handlebars.register_template("t0", t0); + handlebars.register_template("t1", t1); + handlebars.register_template("t2", t2); + handlebars.register_template("t3", t3); + handlebars.register_template("t4", t4); + handlebars.register_template("t5", t5); + handlebars.register_template("t6", t6); + handlebars.register_template("t7", t7); + + assert_eq!(handlebars.render("t0", &1).ok().unwrap(), "1".to_string()); + assert_eq!(handlebars.render("t2", &1).ok().unwrap(), + "not there".to_string()); + assert_eq!(handlebars.render("t3", &1).ok().unwrap(), "1".to_string()); + assert_eq!(handlebars.render("t4", &1).ok().unwrap(), + "include navbar".to_string()); + assert_eq!(handlebars.render("t6", &btreemap!{"a".to_string() => "2".to_string()}) + .ok() + .unwrap(), + "2".to_string()); + assert_eq!(handlebars.render("t7", &1).ok().unwrap(), + "world".to_string()); + } +} diff --git a/src/registry.rs b/src/registry.rs index 9a9207400..f48dabcac 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -10,9 +10,9 @@ use serde::ser::Serialize as ToJson; use template::Template; use render::{Renderable, RenderError, RenderContext}; -use helpers::HelperDef; use context::Context; -use helpers; +use helpers::{self, HelperDef}; +use directives::{self, DirectiveDef}; use support::str::StringWriter; use error::{TemplateError, TemplateFileError, TemplateRenderError}; @@ -44,31 +44,54 @@ pub fn no_escape(data: &str) -> String { pub struct Registry { templates: HashMap, helpers: HashMap>, + directives: HashMap>, escape_fn: EscapeFn, source_map: bool, } impl Registry { pub fn new() -> Registry { - let mut r = Registry { + let r = Registry { templates: HashMap::new(), helpers: HashMap::new(), + directives: HashMap::new(), escape_fn: Box::new(html_escape), source_map: true, }; - r.register_helper("if", Box::new(helpers::IF_HELPER)); - r.register_helper("unless", Box::new(helpers::UNLESS_HELPER)); - r.register_helper("each", Box::new(helpers::EACH_HELPER)); - r.register_helper("with", Box::new(helpers::WITH_HELPER)); - r.register_helper("lookup", Box::new(helpers::LOOKUP_HELPER)); - r.register_helper("raw", Box::new(helpers::RAW_HELPER)); - r.register_helper(">", Box::new(helpers::INCLUDE_HELPER)); - r.register_helper("block", Box::new(helpers::BLOCK_HELPER)); - r.register_helper("partial", Box::new(helpers::PARTIAL_HELPER)); - r.register_helper("log", Box::new(helpers::LOG_HELPER)); - - r + r.setup_builtins() + } + + #[cfg(not(feature = "partial4"))] + fn setup_builtins(mut self) -> Registry { + 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(">", Box::new(helpers::INCLUDE_HELPER)); + self.register_helper("block", Box::new(helpers::BLOCK_HELPER)); + self.register_helper("partial", Box::new(helpers::PARTIAL_HELPER)); + self.register_helper("log", Box::new(helpers::LOG_HELPER)); + + self.register_decorator("inline", Box::new(directives::INLINE_DIRECTIVE)); + self + } + + #[cfg(feature = "partial4")] + fn setup_builtins(mut self) -> Registry { + 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(">", Box::new(helpers::INCLUDE_HELPER)); + self.register_helper("log", Box::new(helpers::LOG_HELPER)); + + self.register_decorator("inline", Box::new(directives::INLINE_DIRECTIVE)); + self } /// Enable handlebars template source map @@ -125,6 +148,14 @@ impl Registry { self.helpers.insert(name.to_string(), def) } + /// register a directive + pub fn register_decorator(&mut self, + name: &str, + def: Box) + -> Option> { + self.directives.insert(name.to_string(), def) + } + /// Register a new *escape fn* to be used from now on by this registry. pub fn register_escape_fn String + Send + Sync>(&mut self, escape_fn: F) { @@ -151,6 +182,11 @@ impl Registry { self.helpers.get(name) } + /// Return a registered directive, aka decorator + pub fn get_decorator(&self, name: &str) -> Option<&Box> { + self.directives.get(name) + } + /// Return all templates registered pub fn get_templates(&self) -> &HashMap { &self.templates @@ -238,6 +274,7 @@ mod test { use helpers::HelperDef; use context::Context; use support::str::StringWriter; + #[cfg(not(feature = "partial4"))] use error::TemplateRenderError; #[derive(Clone, Copy)] @@ -255,9 +292,11 @@ mod test { } } + #[cfg(not(feature = "partial4"))] static DUMMY_HELPER: DummyHelper = DummyHelper; #[test] + #[cfg(not(feature = "partial4"))] fn test_registry_operations() { let mut r = Registry::new(); @@ -320,6 +359,7 @@ mod test { } #[test] + #[cfg(not(feature = "partial4"))] fn test_template_render() { let mut r = Registry::new(); diff --git a/src/render.rs b/src/render.rs index b3ac2189d..94e5de3ab 100644 --- a/src/render.rs +++ b/src/render.rs @@ -11,12 +11,14 @@ use serde_json::value::Value as Json; #[cfg(feature = "serde_type")] use serde::ser::Serialize as ToJson; -use template::{Template, TemplateElement, Parameter, HelperTemplate, TemplateMapping, BlockParam}; -use template::TemplateElement::{RawString, Expression, Comment, HelperBlock, HTMLExpression, - HelperExpression}; +use template::{Template, TemplateElement, Parameter, HelperTemplate, TemplateMapping, BlockParam, + Directive as DirectiveTemplate}; +use template::TemplateElement::*; use registry::Registry; use context::{Context, JsonRender}; use support::str::StringWriter; +#[cfg(feature="partial4")] +use partial; #[derive(Debug, Clone)] pub struct RenderError { @@ -135,11 +137,8 @@ impl<'a> RenderContext<'a> { } } - pub fn get_partial(&self, name: &String) -> Option