| use serde_json::value::Value as Json; |
| |
| use super::block_util::create_block; |
| use crate::block::{BlockContext, BlockParams}; |
| use crate::context::Context; |
| use crate::error::RenderError; |
| use crate::helpers::{HelperDef, HelperResult}; |
| use crate::json::value::to_json; |
| use crate::output::Output; |
| use crate::registry::Registry; |
| use crate::render::{Helper, RenderContext, Renderable}; |
| use crate::util::copy_on_push_vec; |
| |
| fn update_block_context<'reg>( |
| block: &mut BlockContext<'reg>, |
| base_path: Option<&Vec<String>>, |
| relative_path: String, |
| is_first: bool, |
| value: &Json, |
| ) { |
| if let Some(ref p) = base_path { |
| if is_first { |
| *block.base_path_mut() = copy_on_push_vec(p, relative_path); |
| } else if let Some(ptr) = block.base_path_mut().last_mut() { |
| *ptr = relative_path; |
| } |
| } else { |
| block.set_base_value(value.clone()); |
| } |
| } |
| |
| fn set_block_param<'reg: 'rc, 'rc>( |
| block: &mut BlockContext<'reg>, |
| h: &Helper<'reg, 'rc>, |
| base_path: Option<&Vec<String>>, |
| k: &Json, |
| v: &Json, |
| ) -> Result<(), RenderError> { |
| if let Some(bp_val) = h.block_param() { |
| let mut params = BlockParams::new(); |
| if base_path.is_some() { |
| params.add_path(bp_val, Vec::with_capacity(0))?; |
| } else { |
| params.add_value(bp_val, v.clone())?; |
| } |
| |
| block.set_block_params(params); |
| } else if let Some((bp_val, bp_key)) = h.block_param_pair() { |
| let mut params = BlockParams::new(); |
| if base_path.is_some() { |
| params.add_path(bp_val, Vec::with_capacity(0))?; |
| } else { |
| params.add_value(bp_val, v.clone())?; |
| } |
| params.add_value(bp_key, k.clone())?; |
| |
| block.set_block_params(params); |
| } |
| |
| Ok(()) |
| } |
| |
| #[derive(Clone, Copy)] |
| pub struct EachHelper; |
| |
| impl HelperDef for EachHelper { |
| fn call<'reg: 'rc, 'rc>( |
| &self, |
| h: &Helper<'reg, 'rc>, |
| r: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| out: &mut dyn Output, |
| ) -> HelperResult { |
| let value = h |
| .param(0) |
| .ok_or_else(|| RenderError::new("Param not found for helper \"each\""))?; |
| |
| let template = h.template(); |
| |
| match template { |
| Some(t) => match *value.value() { |
| Json::Array(ref list) if !list.is_empty() => { |
| let block_context = create_block(&value)?; |
| rc.push_block(block_context); |
| |
| let len = list.len(); |
| |
| let array_path = value.context_path(); |
| |
| for (i, v) in list.iter().enumerate().take(len) { |
| if let Some(ref mut block) = rc.block_mut() { |
| let is_first = i == 0usize; |
| let is_last = i == len - 1; |
| |
| let index = to_json(i); |
| block.set_local_var("@first".to_string(), to_json(is_first)); |
| block.set_local_var("@last".to_string(), to_json(is_last)); |
| block.set_local_var("@index".to_string(), index.clone()); |
| |
| update_block_context(block, array_path, i.to_string(), is_first, &v); |
| set_block_param(block, h, array_path, &index, &v)?; |
| } |
| |
| t.render(r, ctx, rc, out)?; |
| } |
| |
| rc.pop_block(); |
| Ok(()) |
| } |
| Json::Object(ref obj) if !obj.is_empty() => { |
| let block_context = create_block(&value)?; |
| rc.push_block(block_context); |
| |
| let mut is_first = true; |
| let obj_path = value.context_path(); |
| |
| for (k, v) in obj.iter() { |
| if let Some(ref mut block) = rc.block_mut() { |
| let key = to_json(k); |
| |
| block.set_local_var("@first".to_string(), to_json(is_first)); |
| block.set_local_var("@key".to_string(), key.clone()); |
| |
| update_block_context(block, obj_path, k.to_string(), is_first, &v); |
| set_block_param(block, h, obj_path, &key, &v)?; |
| } |
| |
| t.render(r, ctx, rc, out)?; |
| |
| if is_first { |
| is_first = false; |
| } |
| } |
| |
| rc.pop_block(); |
| Ok(()) |
| } |
| _ => { |
| if let Some(else_template) = h.inverse() { |
| else_template.render(r, ctx, rc, out)?; |
| } |
| Ok(()) |
| } |
| }, |
| None => Ok(()), |
| } |
| } |
| } |
| |
| pub static EACH_HELPER: EachHelper = EachHelper; |
| |
| #[cfg(test)] |
| mod test { |
| use crate::json::value::to_json; |
| use crate::registry::Registry; |
| use serde_json::value::Value as Json; |
| use std::collections::BTreeMap; |
| use std::str::FromStr; |
| |
| #[test] |
| fn test_each() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string( |
| "t0", |
| "{{#each this}}{{@first}}|{{@last}}|{{@index}}:{{this}}|{{/each}}" |
| ) |
| .is_ok()); |
| assert!(handlebars |
| .register_template_string("t1", "{{#each this}}{{@first}}|{{@key}}:{{this}}|{{/each}}") |
| .is_ok()); |
| |
| let r0 = handlebars.render("t0", &vec![1u16, 2u16, 3u16]); |
| assert_eq!( |
| r0.ok().unwrap(), |
| "true|false|0:1|false|false|1:2|false|true|2:3|".to_string() |
| ); |
| |
| let mut m: BTreeMap<String, u16> = BTreeMap::new(); |
| m.insert("ftp".to_string(), 21); |
| m.insert("http".to_string(), 80); |
| let r1 = handlebars.render("t1", &m); |
| assert_eq!(r1.ok().unwrap(), "true|ftp:21|false|http:80|".to_string()); |
| } |
| |
| #[test] |
| fn test_each_with_parent() { |
| let json_str = r#"{"a":{"a":99,"c":[{"d":100},{"d":200}]}}"#; |
| |
| let data = Json::from_str(json_str).unwrap(); |
| // println!("data: {}", data); |
| let mut handlebars = Registry::new(); |
| |
| // previously, to access the parent in an each block, |
| // a user would need to specify ../../b, as the path |
| // that is computed includes the array index: ./a.c.[0] |
| assert!(handlebars |
| .register_template_string("t0", "{{#each a.c}} d={{d}} b={{../a.a}} {{/each}}") |
| .is_ok()); |
| |
| let r1 = handlebars.render("t0", &data); |
| assert_eq!(r1.ok().unwrap(), " d=100 b=99 d=200 b=99 ".to_string()); |
| } |
| |
| #[test] |
| fn test_nested_each_with_parent() { |
| let json_str = r#"{"a": [{"b": [{"d": 100}], "c": 200}]}"#; |
| |
| let data = Json::from_str(json_str).unwrap(); |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string( |
| "t0", |
| "{{#each a}}{{#each b}}{{d}}:{{../c}}{{/each}}{{/each}}" |
| ) |
| .is_ok()); |
| |
| let r1 = handlebars.render("t0", &data); |
| assert_eq!(r1.ok().unwrap(), "100:200".to_string()); |
| } |
| |
| #[test] |
| fn test_nested_each() { |
| let json_str = r#"{"a": [{"b": true}], "b": [[1, 2, 3],[4, 5]]}"#; |
| |
| let data = Json::from_str(json_str).unwrap(); |
| |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string( |
| "t0", |
| "{{#each b}}{{#if ../a}}{{#each this}}{{this}}{{/each}}{{/if}}{{/each}}" |
| ) |
| .is_ok()); |
| |
| let r1 = handlebars.render("t0", &data); |
| assert_eq!(r1.ok().unwrap(), "12345".to_string()); |
| } |
| |
| #[test] |
| fn test_nested_array() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string("t0", "{{#each this.[0]}}{{this}}{{/each}}") |
| .is_ok()); |
| |
| let r0 = handlebars.render("t0", &(vec![vec![1, 2, 3]])); |
| |
| assert_eq!(r0.ok().unwrap(), "123".to_string()); |
| } |
| |
| #[test] |
| fn test_empty_key() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string("t0", "{{#each this}}{{@key}}-{{value}}\n{{/each}}") |
| .is_ok()); |
| |
| let r0 = handlebars |
| .render( |
| "t0", |
| &json!({ |
| "foo": { |
| "value": "bar" |
| }, |
| "": { |
| "value": "baz" |
| } |
| }), |
| ) |
| .unwrap(); |
| |
| let mut r0_sp: Vec<_> = r0.split('\n').collect(); |
| r0_sp.sort(); |
| |
| assert_eq!(r0_sp, vec!["", "-baz", "foo-bar"]); |
| } |
| |
| #[test] |
| fn test_each_else() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string("t0", "{{#each a}}1{{else}}empty{{/each}}") |
| .is_ok()); |
| let m1 = btreemap! { |
| "a".to_string() => Vec::<String>::new(), |
| }; |
| let r0 = handlebars.render("t0", &m1).unwrap(); |
| assert_eq!(r0, "empty"); |
| |
| let m2 = btreemap! { |
| "b".to_string() => Vec::<String>::new() |
| }; |
| let r1 = handlebars.render("t0", &m2).unwrap(); |
| assert_eq!(r1, "empty"); |
| } |
| |
| #[test] |
| fn test_block_param() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string("t0", "{{#each a as |i|}}{{i}}{{/each}}") |
| .is_ok()); |
| let m1 = btreemap! { |
| "a".to_string() => vec![1,2,3,4,5] |
| }; |
| let r0 = handlebars.render("t0", &m1).unwrap(); |
| assert_eq!(r0, "12345"); |
| } |
| |
| #[test] |
| fn test_each_object_block_param() { |
| let mut handlebars = Registry::new(); |
| let template = "{{#each this as |v k|}}\ |
| {{#with k as |inner_k|}}{{inner_k}}{{/with}}:{{v}}|\ |
| {{/each}}"; |
| assert!(handlebars.register_template_string("t0", template).is_ok()); |
| |
| 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()); |
| } |
| |
| #[test] |
| fn test_each_object_block_param2() { |
| let mut handlebars = Registry::new(); |
| let template = "{{#each this as |v k|}}\ |
| {{#with v as |inner_v|}}{{k}}:{{inner_v}}{{/with}}|\ |
| {{/each}}"; |
| |
| assert!(handlebars.register_template_string("t0", template).is_ok()); |
| |
| 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()); |
| } |
| |
| fn test_nested_each_with_path_ups() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string( |
| "t0", |
| "{{#each a.b}}{{#each c}}{{../../d}}{{/each}}{{/each}}" |
| ) |
| .is_ok()); |
| |
| let data = btreemap! { |
| "a".to_string() => to_json(&btreemap! { |
| "b".to_string() => vec![btreemap!{"c".to_string() => vec![1]}] |
| }), |
| "d".to_string() => to_json(&1) |
| }; |
| |
| let r0 = handlebars.render("t0", &data); |
| assert_eq!(r0.ok().unwrap(), "1".to_string()); |
| } |
| |
| #[test] |
| fn test_nested_each_with_path_up_this() { |
| let mut handlebars = Registry::new(); |
| let template = "{{#each variant}}{{#each ../typearg}}\ |
| {{#if @first}}template<{{/if}}{{this}}{{#if @last}}>{{else}},{{/if}}\ |
| {{/each}}{{/each}}"; |
| assert!(handlebars.register_template_string("t0", template).is_ok()); |
| let data = btreemap! { |
| "typearg".to_string() => vec!["T".to_string()], |
| "variant".to_string() => vec!["1".to_string(), "2".to_string()] |
| }; |
| let r0 = handlebars.render("t0", &data); |
| assert_eq!(r0.ok().unwrap(), "template<T>template<T>".to_string()); |
| } |
| |
| #[test] |
| fn test_key_iteration_with_unicode() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string("t0", "{{#each this}}{{@key}}: {{this}}\n{{/each}}") |
| .is_ok()); |
| let data = json!({ |
| "normal": 1, |
| "你好": 2, |
| "#special key": 3, |
| "😂": 4, |
| "me.dot.key": 5 |
| }); |
| let r0 = handlebars.render("t0", &data).ok().unwrap(); |
| assert!(r0.contains("normal: 1")); |
| assert!(r0.contains("你好: 2")); |
| assert!(r0.contains("#special key: 3")); |
| assert!(r0.contains("😂: 4")); |
| assert!(r0.contains("me.dot.key: 5")); |
| } |
| |
| #[test] |
| fn test_base_path_after_each() { |
| let mut handlebars = Registry::new(); |
| assert!(handlebars |
| .register_template_string("t0", "{{#each a}}{{this}}{{/each}} {{b}}") |
| .is_ok()); |
| let data = json!({ |
| "a": [1, 2, 3, 4], |
| "b": "good", |
| }); |
| |
| let r0 = handlebars.render("t0", &data).ok().unwrap(); |
| |
| assert_eq!("1234 good", r0); |
| } |
| |
| #[test] |
| fn test_else_context() { |
| let reg = Registry::new(); |
| let template = "{{#each list}}A{{else}}{{foo}}{{/each}}"; |
| let input = json!({"list": [], "foo": "bar"}); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("bar", rendered); |
| } |
| |
| #[test] |
| fn test_block_context_leak() { |
| let reg = Registry::new(); |
| let template = "{{#each list}}{{#each inner}}{{this}}{{/each}}{{foo}}{{/each}}"; |
| let input = json!({"list": [{"inner": [], "foo": 1}, {"inner": [], "foo": 2}]}); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("12", rendered); |
| } |
| |
| #[test] |
| fn test_derived_array_as_block_params() { |
| handlebars_helper!(range: |x: u64| (0..x).collect::<Vec<u64>>()); |
| let mut reg = Registry::new(); |
| reg.register_helper("range", Box::new(range)); |
| let template = "{{#each (range 3) as |i|}}{{i}}{{/each}}"; |
| let input = json!(0); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("012", rendered); |
| } |
| |
| #[test] |
| fn test_derived_object_as_block_params() { |
| handlebars_helper!(point: |x: u64, y: u64| json!({"x":x, "y":y})); |
| let mut reg = Registry::new(); |
| reg.register_helper("point", Box::new(point)); |
| let template = "{{#each (point 0 1) as |i|}}{{i}}{{/each}}"; |
| let input = json!(0); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("01", rendered); |
| } |
| |
| #[test] |
| fn test_derived_array_without_block_param() { |
| handlebars_helper!(range: |x: u64| (0..x).collect::<Vec<u64>>()); |
| let mut reg = Registry::new(); |
| reg.register_helper("range", Box::new(range)); |
| let template = "{{#each (range 3)}}{{this}}{{/each}}"; |
| let input = json!(0); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("012", rendered); |
| } |
| |
| #[test] |
| fn test_derived_object_without_block_params() { |
| handlebars_helper!(point: |x: u64, y: u64| json!({"x":x, "y":y})); |
| let mut reg = Registry::new(); |
| reg.register_helper("point", Box::new(point)); |
| let template = "{{#each (point 0 1)}}{{this}}{{/each}}"; |
| let input = json!(0); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("01", rendered); |
| } |
| |
| #[test] |
| fn test_non_iterable() { |
| let reg = Registry::new(); |
| let template = "{{#each this}}each block{{else}}else block{{/each}}"; |
| let input = json!("strings aren't iterable"); |
| let rendered = reg.render_template(template, &input).unwrap(); |
| assert_eq!("else block", rendered); |
| } |
| |
| #[test] |
| fn test_recursion() { |
| let mut reg = Registry::new(); |
| assert!(reg |
| .register_template_string( |
| "walk", |
| "(\ |
| {{#each this}}\ |
| {{#if @key}}{{@key}}{{else}}{{@index}}{{/if}}: \ |
| {{this}} \ |
| {{> walk this}}, \ |
| {{/each}}\ |
| )", |
| ) |
| .is_ok()); |
| |
| let input = json!({ |
| "array": [42, {"wow": "cool"}, [[]]], |
| "object": { "a": { "b": "c", "d": ["e"] } }, |
| "string": "hi" |
| }); |
| let expected_output = "(\ |
| array: [42, [object], [[], ], ] (\ |
| 0: 42 (), \ |
| 1: [object] (wow: cool (), ), \ |
| 2: [[], ] (0: [] (), ), \ |
| ), \ |
| object: [object] (\ |
| a: [object] (\ |
| b: c (), \ |
| d: [e, ] (0: e (), ), \ |
| ), \ |
| ), \ |
| string: hi (), \ |
| )"; |
| |
| let rendered = reg.render("walk", &input).unwrap(); |
| assert_eq!(expected_output, rendered); |
| } |
| } |