diff --git a/examples/template.hbs b/examples/template.hbs index 2476531c1..b6817adb1 100644 --- a/examples/template.hbs +++ b/examples/template.hbs @@ -5,10 +5,10 @@

CSL {{year}}

diff --git a/src/context.rs b/src/context.rs index 0528886ae..f9527fc68 100644 --- a/src/context.rs +++ b/src/context.rs @@ -19,7 +19,7 @@ pub type Object = BTreeMap; /// The context wrap data you render on your templates. /// -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Context { data: Json, } @@ -178,6 +178,20 @@ impl JsonRender for Json { } } +#[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] +pub fn to_json(src: &T) -> Json + where T: ToJson +{ + src.to_json() +} + +#[cfg(feature = "serde_type")] +pub fn to_json(src: &T) -> Json + where T: Serialize +{ + value::to_value(src) +} + impl JsonTruthy for Json { fn is_truthy(&self) -> bool { match *self { diff --git a/src/grammar.rs b/src/grammar.rs index 6d3803433..a5b2ff3d9 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -32,9 +32,10 @@ impl_rdp! { reference = @{ identifier ~ (["["] ~ (string_literal|['0'..'9']+) ~ ["]"])* ~ ["-"]* ~ reference* } name = _{ subexpression | reference } - param = { literal | reference | subexpression } + param = { !["as"] ~ (literal | reference | subexpression) } hash = { identifier ~ ["="] ~ param } - exp_line = _{ identifier ~ (hash|param)* } + block_param = { ["as"] ~ ["|"] ~ identifier ~ identifier? ~ ["|"]} + exp_line = _{ identifier ~ (hash|param)* ~ block_param?} subexpression = { ["("] ~ name ~ (hash|param)* ~ [")"] } @@ -243,7 +244,9 @@ fn test_helper_start() { "{{#if []}}", "{{#if {}}}", "{{#if}}", - "{{~#if hello~}}"]; + "{{~#if hello~}}", + "{{#each people as |person|}}", + "{{#each-obj obj as |key val|}}"]; for i in s.iter() { let mut rdp = Rdp::new(StringInput::new(i)); assert!(rdp.helper_block_start()); @@ -287,3 +290,13 @@ fn test_raw_block() { assert!(rdp.end()); } } + +#[test] +fn test_block_param() { + let s = vec!["as |person|", "as |key val|"]; + for i in s.iter() { + let mut rdp = Rdp::new(StringInput::new(i)); + assert!(rdp.block_param()); + assert!(rdp.end()); + } +} diff --git a/src/helpers/helper_each.rs b/src/helpers/helper_each.rs index e5f2f9327..adb531efc 100644 --- a/src/helpers/helper_each.rs +++ b/src/helpers/helper_each.rs @@ -1,19 +1,19 @@ +use std::collections::BTreeMap; + #[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] -use serialize::json::{Json, ToJson}; +use serialize::json::Json; #[cfg(feature = "serde_type")] -use serde_json::value::{self, Value as Json}; - +use serde_json::value::Value as Json; use helpers::HelperDef; use registry::Registry; -use context::{Context, JsonTruthy}; +use context::{Context, JsonTruthy, to_json}; use render::{Renderable, RenderContext, RenderError, Helper}; #[derive(Clone, Copy)] pub struct EachHelper; impl HelperDef for EachHelper { - #[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] fn call(&self, c: &Context, h: &Helper, @@ -39,9 +39,9 @@ impl HelperDef for EachHelper { let len = list.len(); for i in 0..len { let mut local_rc = rc.derive(); - local_rc.set_local_var("@first".to_string(), (i == 0usize).to_json()); - local_rc.set_local_var("@last".to_string(), (i == len - 1).to_json()); - local_rc.set_local_var("@index".to_string(), i.to_json()); + local_rc.set_local_var("@first".to_string(), to_json(&(i == 0usize))); + local_rc.set_local_var("@last".to_string(), to_json(&(i == len - 1))); + local_rc.set_local_var("@index".to_string(), to_json(&i)); if let Some(inner_path) = value.path() { let new_path = format!("{}/{}.[{}]", @@ -49,95 +49,20 @@ impl HelperDef for EachHelper { inner_path, i); debug!("each path {:?}", new_path); - local_rc.set_path(new_path); + local_rc.set_path(new_path.clone()); } - try!(t.render(c, r, &mut local_rc)); - } - Ok(()) - } - (true, &Json::Object(ref obj)) => { - let mut first: bool = true; - for k in obj.keys() { - let mut local_rc = rc.derive(); - local_rc.set_local_var("@first".to_string(), first.to_json()); - if first { - first = false; - } - - local_rc.set_local_var("@key".to_string(), k.to_json()); - if let Some(inner_path) = value.path() { - let new_path = format!("{}/{}.[{}]", - local_rc.get_path(), - inner_path, - k); - local_rc.set_path(new_path); + if let Some(block_param) = h.block_param() { + let mut map = BTreeMap::new(); + map.insert(block_param.to_string(), to_json(&list[i])); + local_rc.push_block_context(&map); } try!(t.render(c, r, &mut local_rc)); - } - - Ok(()) - } - (false, _) => { - if let Some(else_template) = h.inverse() { - try!(else_template.render(c, r, rc)); - } - Ok(()) - } - _ => { - Err(RenderError::new(format!("Param type is not iterable: {:?}", template))) - } - }; - rc.demote_local_vars(); - rendered - } - None => Ok(()), - } - } - - #[cfg(feature = "serde_type")] - fn call(&self, - c: &Context, - h: &Helper, - r: &Registry, - rc: &mut RenderContext) - -> Result<(), RenderError> { - let value = try!(h.param(0) - .ok_or_else(|| RenderError::new("Param not found for helper \"each\""))); - - let template = h.template(); - - match template { - Some(t) => { - rc.promote_local_vars(); - if let Some(path_root) = value.path_root() { - let local_path_root = format!("{}/{}", rc.get_path(), path_root); - rc.set_local_path_root(local_path_root); - } - - debug!("each value {:?}", value.value()); - let rendered = match (value.value().is_truthy(), value.value()) { - (true, &Json::Array(ref list)) => { - let len = list.len(); - for i in 0..len { - let mut local_rc = rc.derive(); - local_rc.set_local_var("@first".to_string(), - value::to_value(&(i == 0usize))); - local_rc.set_local_var("@last".to_string(), - value::to_value(&(i == len - 1))); - local_rc.set_local_var("@index".to_string(), value::to_value(&i)); - - if let Some(inner_path) = value.path() { - let new_path = format!("{}/{}.[{}]", - local_rc.get_path(), - inner_path, - i); - debug!("each value {:?}", new_path); - local_rc.set_path(new_path); + if h.block_param().is_some() { + local_rc.pop_block_context(); } - try!(t.render(c, r, &mut local_rc)); } Ok(()) } @@ -145,22 +70,33 @@ impl HelperDef for EachHelper { let mut first: bool = true; for k in obj.keys() { let mut local_rc = rc.derive(); - local_rc.set_local_var("@first".to_string(), value::to_value(&first)); + local_rc.set_local_var("@first".to_string(), to_json(&first)); if first { first = false; } - local_rc.set_local_var("@key".to_string(), value::to_value(&k)); + local_rc.set_local_var("@key".to_string(), to_json(k)); + if let Some(inner_path) = value.path() { let new_path = format!("{}/{}.[{}]", local_rc.get_path(), inner_path, k); - debug!("each value {:?}", new_path); local_rc.set_path(new_path); } + if let Some((bp_key, bp_val)) = h.block_param_pair() { + let mut map = BTreeMap::new(); + map.insert(bp_key.to_string(), to_json(k)); + map.insert(bp_val.to_string(), to_json(obj.get(k).unwrap())); + local_rc.push_block_context(&map); + } + try!(t.render(c, r, &mut local_rc)); + + if h.block_param().is_some() { + local_rc.pop_block_context(); + } } Ok(()) @@ -175,7 +111,7 @@ impl HelperDef for EachHelper { Err(RenderError::new(format!("Param type is not iterable: {:?}", template))) } }; - // rc.set_path(path); + rc.demote_local_vars(); rendered } @@ -345,4 +281,33 @@ mod test { assert_eq!(r1, "empty"); } + #[test] + fn test_block_param() { + let t0 = Template::compile("{{#each a as |i|}}{{i}}{{/each}}".to_owned()).unwrap(); + let mut handlebars = Registry::new(); + handlebars.register_template("t0", t0); + let m1 = btreemap! { + "a".to_string() => vec![1,2,3,4,5] + }; + let r0 = handlebars.render("t0", &m1).unwrap(); + println!("render: {}", r0); + assert_eq!(r0, "12345"); + } + + #[test] + fn test_each_object_block_param() { + let t0 = Template::compile("{{#each this as |k v|}}{{#with k as |inner_k|}}{{inner_k}}{{/with}}:{{v}}|{{/each}}".to_string()) + .ok() + .unwrap(); + + let mut handlebars = Registry::new(); + handlebars.register_template("t0", t0); + + let m = btreemap!{ + "ftp".to_string() => 21, + "http".to_string() => 80 + }; + let r0 = handlebars.render("t0", &m); + assert_eq!(r0.ok().unwrap(), "ftp:21|http:80|".to_string()); + } } diff --git a/src/helpers/helper_with.rs b/src/helpers/helper_with.rs index c2c6af885..36683cd5e 100644 --- a/src/helpers/helper_with.rs +++ b/src/helpers/helper_with.rs @@ -1,6 +1,8 @@ +use std::collections::BTreeMap; + use helpers::HelperDef; use registry::Registry; -use context::{Context, JsonTruthy}; +use context::{Context, JsonTruthy, to_json}; use render::{Renderable, RenderContext, RenderError, Helper}; #[derive(Clone, Copy)] @@ -35,6 +37,12 @@ impl HelperDef for WithHelper { let new_path = format!("{}/{}", path, inner_path); rc.set_path(new_path); } + + if let Some(block_param) = h.block_param() { + let mut map = BTreeMap::new(); + map.insert(block_param.to_string(), to_json(param.value())); + rc.push_block_context(&map); + } } let rendered = match template { @@ -44,6 +52,9 @@ impl HelperDef for WithHelper { rc.set_path(path); rc.demote_local_vars(); + if not_empty { + rc.pop_block_context(); + } rendered } } @@ -129,7 +140,47 @@ mod test { } #[test] - fn test_with_in_each(){ + fn test_with_block_param() { + let addr = Address { + city: "Beijing".to_string(), + country: "China".to_string(), + }; + + let person = Person { + name: "Ning Sun".to_string(), + age: 27, + addr: addr, + titles: vec!["programmer".to_string(), "cartographier".to_string()], + }; + + let t0 = Template::compile("{{#with addr as |a|}}{{a.city}}{{/with}}".to_string()) + .ok() + .unwrap(); + let t1 = Template::compile("{{#with notfound as |c|}}hello{{else}}world{{/with}}" + .to_string()) + .ok() + .unwrap(); + let t2 = Template::compile("{{#with addr/country as |t|}}{{t}}{{/with}}".to_string()) + .ok() + .unwrap(); + + let mut handlebars = Registry::new(); + handlebars.register_template("t0", t0); + handlebars.register_template("t1", t1); + handlebars.register_template("t2", t2); + + let r0 = handlebars.render("t0", &person); + assert_eq!(r0.ok().unwrap(), "Beijing".to_string()); + + let r1 = handlebars.render("t1", &person); + assert_eq!(r1.ok().unwrap(), "world".to_string()); + + let r2 = handlebars.render("t2", &person); + assert_eq!(r2.ok().unwrap(), "China".to_string()); + } + + #[test] + fn test_with_in_each() { let addr = Address { city: "Beijing".to_string(), country: "China".to_string(), @@ -156,9 +207,18 @@ mod test { let people = vec![person, person2]; - let t0 = Template::compile("{{#each this}}{{#with addr}}{{city}}{{/with}}{{/each}}".to_string()).ok().unwrap(); - let t1 = Template::compile("{{#each this}}{{#with addr}}{{../age}}{{/with}}{{/each}}".to_string()).ok().unwrap(); - let t2 = Template::compile("{{#each this}}{{#with addr}}{{@../index}}{{/with}}{{/each}}".to_string()).ok().unwrap(); + let t0 = Template::compile("{{#each this}}{{#with addr}}{{city}}{{/with}}{{/each}}" + .to_string()) + .ok() + .unwrap(); + let t1 = Template::compile("{{#each this}}{{#with addr}}{{../age}}{{/with}}{{/each}}" + .to_string()) + .ok() + .unwrap(); + let t2 = Template::compile("{{#each this}}{{#with addr}}{{@../index}}{{/with}}{{/each}}" + .to_string()) + .ok() + .unwrap(); let mut handlebars = Registry::new(); handlebars.register_template("t0", t0); diff --git a/src/render.rs b/src/render.rs index 63d111a0d..5431c824d 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,15 +1,17 @@ -use std::collections::{HashMap, BTreeMap}; +use std::collections::{HashMap, BTreeMap, VecDeque}; use std::error; use std::fmt; use std::io::Write; use std::io::Error as IOError; #[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))] -use serialize::json::Json; +use serialize::json::{ToJson, Json}; #[cfg(feature = "serde_type")] use serde_json::value::Value as Json; +#[cfg(feature = "serde_type")] +use serde::ser::Serialize as ToJson; -use template::{Template, TemplateElement, Parameter, HelperTemplate, TemplateMapping}; +use template::{Template, TemplateElement, Parameter, HelperTemplate, TemplateMapping, BlockParam}; use template::TemplateElement::{RawString, Expression, Comment, HelperBlock, HTMLExpression, HelperExpression}; use registry::Registry; @@ -75,6 +77,7 @@ pub struct RenderContext<'a> { local_path_root: Option, local_variables: HashMap, default_var: Json, + block_context: VecDeque, /// the `Write` where page is generated pub writer: &'a mut Write, /// current template name @@ -93,6 +96,7 @@ impl<'a> RenderContext<'a> { local_path_root: None, local_variables: HashMap::new(), default_var: Json::Null, + block_context: VecDeque::new(), writer: w, current_template: None, root_template: None, @@ -108,6 +112,7 @@ impl<'a> RenderContext<'a> { local_path_root: self.local_path_root.clone(), local_variables: self.local_variables.clone(), default_var: self.default_var.clone(), + block_context: self.block_context.clone(), writer: w, current_template: self.current_template.clone(), root_template: self.root_template.clone(), @@ -122,6 +127,7 @@ impl<'a> RenderContext<'a> { local_path_root: self.local_path_root.clone(), local_variables: self.local_variables.clone(), default_var: self.default_var.clone(), + block_context: self.block_context.clone(), writer: self.writer, current_template: self.current_template.clone(), root_template: self.root_template.clone(), @@ -192,16 +198,34 @@ impl<'a> RenderContext<'a> { self.local_variables = new_map; } - pub fn get_local_var(&self, name: &String) -> &Json { - match self.local_variables.get(name) { - Some(j) => j, - None => &self.default_var, - } + pub fn get_local_var(&self, name: &String) -> Option<&Json> { + self.local_variables.get(name) } pub fn writer(&mut self) -> &mut Write { self.writer } + + pub fn push_block_context(&mut self, ctx: &T) + where T: ToJson + { + self.block_context.push_front(Context::wraps(ctx)); + } + + pub fn pop_block_context(&mut self) { + self.block_context.pop_front(); + } + + pub fn evaluate_in_block_context(&self, local_path: &str) -> Option<&Json> { + for bc in self.block_context.iter() { + let v = bc.navigate(".", local_path); + if !v.is_null() { + return Some(v); + } + } + + None + } } impl<'a> fmt::Debug for RenderContext<'a> { @@ -249,6 +273,7 @@ pub struct Helper<'a> { name: &'a str, params: Vec, hash: BTreeMap, + block_param: &'a Option, template: &'a Option