use handlebars::{
BlockContext, Context, Handlebars, Helper, HelperDef, HelperResult, JsonRender, Output,
PathAndJson, RenderContext, Renderable, ScopedJson, StringOutput,
};
const QUOTES_DOUBLE: &str = "\"";
const QUOTES_SINGLE: &str = "\'";
#[allow(clippy::assigning_clones)]
pub(crate) fn create_block<'rc>(param: &PathAndJson<'rc>) -> BlockContext<'rc> {
let mut block = BlockContext::new();
if let Some(new_path) = param.context_path() {
*block.base_path_mut() = new_path.clone();
} else {
block.set_base_value(param.value().clone());
}
block
}
pub(crate) fn apply_wrapper(subject: String, wrapper: &str, wrap: bool) -> String {
if wrap {
format!("{}{}{}", wrapper, subject, wrapper)
} else {
subject
}
}
#[derive(Clone, Copy)]
pub struct HandlebarsConcat;
impl HelperDef for HandlebarsConcat {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'rc>,
r: &'reg Handlebars,
ctx: &'rc Context,
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let separator = if let Some(s) = h.hash_get("separator") {
s.render()
} else {
",".to_string()
};
let distinct = h.hash_get("distinct").is_some();
let quotes = h.hash_get("quotes").is_some();
let single_quote = h.hash_get("single_quote").is_some();
let wrapper = if quotes {
if single_quote {
QUOTES_SINGLE
} else {
QUOTES_DOUBLE
}
} else {
""
};
let render_all = h.hash_get("render_all").is_some();
let template = h.template();
let mut output: Vec<String> = Vec::new();
for param in h.params() {
match param.value() {
serde_json::Value::Null => {}
serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::String(_) => {
let value = if h.is_block() && render_all {
let mut content = StringOutput::default();
rc.push_block(create_block(¶m));
template
.map(|t| t.render(r, ctx, rc, &mut content))
.unwrap_or(Ok(()))?;
rc.pop_block();
content.into_string().unwrap_or_default()
} else {
param.value().render()
};
let value = apply_wrapper(value, wrapper, quotes);
if !value.is_empty() && (!output.contains(&value) || !distinct) {
output.push(value);
}
}
serde_json::Value::Array(ar) => {
if h.is_block() && render_all {
for array_item in ar {
let mut content = StringOutput::default();
let block = create_block(&PathAndJson::new(
None,
ScopedJson::from(array_item.clone()),
));
rc.push_block(block);
template
.map(|t| t.render(r, ctx, rc, &mut content))
.unwrap_or(Ok(()))?;
rc.pop_block();
if let Ok(value) = content.into_string() {
let value = apply_wrapper(value, wrapper, quotes);
if !value.is_empty() && (!output.contains(&value) || !distinct) {
output.push(value);
}
}
}
} else {
output.append(
&mut ar
.iter()
.map(|item| item.render())
.map(|item| apply_wrapper(item, wrapper, quotes))
.filter(|item| {
if distinct {
!output.contains(item)
} else {
true
}
})
.collect::<Vec<String>>(),
);
}
}
serde_json::Value::Object(o) => {
if h.is_block() {
for obj in o.values() {
let mut content = StringOutput::default();
let block = create_block(&PathAndJson::new(
None,
ScopedJson::from(obj.clone()),
));
rc.push_block(block);
template
.map(|t| t.render(r, ctx, rc, &mut content))
.unwrap_or(Ok(()))?;
rc.pop_block();
if let Ok(value) = content.into_string() {
let value = apply_wrapper(value, wrapper, quotes);
if !value.is_empty() && (!output.contains(&value) || !distinct) {
output.push(value);
}
}
}
} else {
output.append(
&mut o
.keys()
.cloned()
.map(|item| apply_wrapper(item, wrapper, quotes))
.filter(|item| {
if distinct {
!output.contains(item)
} else {
true
}
})
.collect::<Vec<String>>(),
);
}
}
}
}
out.write(&output.join(&*separator))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
use handlebars::Handlebars;
use serde_json::json;
let mut h = Handlebars::new();
h.register_helper("concat", Box::new(HandlebarsConcat));
assert_eq!(
h.render_template(r#"{{concat 1 2}}"#, &String::new())
.expect("Render error"),
"1,2",
"Failed to concat numeric literals"
);
assert_eq!(
h.render_template(r#"{{concat "One" "Two"}}"#, &String::new())
.expect("Render error"),
"One,Two",
"Failed to concat literals"
);
assert_eq!(
h.render_template(r#"{{concat "One" "Two" separator=", "}}"#, &String::new())
.expect("Render error"),
"One, Two",
"Failed to concat literals with separator"
);
assert_eq!(
h.render_template(
r#"{{concat "One" "Two" separator=", " quotes=true}}"#,
&String::new()
)
.expect("Render error"),
r#""One", "Two""#,
"Failed to concat literals with separator and quotes"
);
assert_eq!(
h.render_template(
r#"{{concat "One" "Two" separator=", " quotes=true single_quote=true}}"#,
&String::new()
)
.expect("Render error"),
r#"'One', 'Two'"#,
"Failed to concat literals with separator and single quotation marks"
);
assert_eq!(
h.render_template(
r#"{{concat s arr obj separator=", " quotes=true}}"#,
&json!({"arr": ["One", "Two", "Three"]})
)
.expect("Render error"),
r#""One", "Two", "Three""#,
"Failed to concat array with quotes"
);
assert_eq!(
h.render_template(
r#"{{concat s arr obj separator=", " distinct=true}}"#,
&json!({"s": "One", "arr": ["One", "Two"], "obj": {"Three":3}})
)
.expect("Render error"),
"One, Two, Three",
"Failed to concat literal, array and object"
);
assert_eq!(
h.render_template(
r#"{{#concat s arr obj separator=", " distinct=true}}{{label}}{{/concat}}"#,
&json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
).expect("Render error"),
"One, Two, Three, Four",
"Failed to concat literal, array and object using block template"
);
assert_eq!(
h.render_template(
r#"{{#concat s arr obj separator=", " distinct=true quotes=true}}{{label}}{{/concat}}"#,
&json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
).expect("Render error"),
r#""One", "Two", "Three", "Four""#,
"Failed to concat literal, array and object using block template"
);
assert_eq!(
h.render_template(
r#"{{concat obj separator=", " quotes=true}}"#,
&json!({"obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
).expect("Render error"),
r#""key0", "key1", "key2""#,
"Failed to concat object keys with quotation marks and no distinction"
);
assert_eq!(
h.render_template(
r#"{{#concat s arr obj separator=", " distinct=true render_all=true}}<{{#if label}}{{label}}{{else}}{{this}}{{/if}}/>{{/concat}}"#,
&json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
).expect("Render error"),
r#"<One/>, <Two/>, <Three/>, <Four/>"#,
"Failed to concat literal, array and object using block template"
);
assert_eq!(
h.render_template(
r#"{{#concat s arr obj separator=", " distinct=true render_all=true quotes=true}}[{{#if label}}{{label}}{{else}}{{this}}{{/if}}]{{/concat}}"#,
&json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
).expect("Render error"),
r#""[One]", "[Two]", "[Three]", "[Four]""#,
"Failed to concat literal, array and object using block template"
);
assert_eq!(
h.render_template(
r#"{{#concat s arr obj separator=", " distinct=true render_all=true quotes=true}}[{{#if label}}{{label}}{{else}}{{@root/zero}}{{/if}}]{{/concat}}"#,
&json!({"zero":"Zero", "s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
).expect("Render error"),
r#""[Zero]", "[Two]", "[Three]", "[Four]""#,
"Failed to concat literal, array and object using block template"
);
}
}