handlebars_concat/lib.rs
1use handlebars::{
2 BlockContext, Context, Handlebars, Helper, HelperDef, HelperResult, JsonRender, Output,
3 PathAndJson, RenderContext, Renderable, ScopedJson, StringOutput,
4};
5
6const QUOTES_DOUBLE: &str = "\"";
7const QUOTES_SINGLE: &str = "\'";
8
9#[allow(clippy::assigning_clones)]
10pub(crate) fn create_block<'rc>(param: &PathAndJson<'rc>) -> BlockContext<'rc> {
11 let mut block = BlockContext::new();
12
13 if let Some(new_path) = param.context_path() {
14 *block.base_path_mut() = new_path.clone();
15 } else {
16 // use clone for now
17 block.set_base_value(param.value().clone());
18 }
19
20 block
21}
22
23pub(crate) fn apply_wrapper(subject: String, wrapper: &str, wrap: bool) -> String {
24 if wrap {
25 format!("{}{}{}", wrapper, subject, wrapper)
26 } else {
27 subject
28 }
29}
30
31#[derive(Clone, Copy)]
32/// Concat helper for handlebars-rust
33///
34/// # Registration
35///
36/// ```rust
37/// use handlebars::Handlebars;
38/// use handlebars_concat::HandlebarsConcat;
39/// use serde_json::json;
40///
41/// let mut h = Handlebars::new();
42/// h.register_helper("concat", Box::new(HandlebarsConcat));
43///
44/// assert_eq!(h.render_template(r#"{{concat item1 item2}}"#, &json!({"item1": "Value 1", "item2": "Value 2"})).expect("Render error"), "Value 1,Value 2");
45/// assert_eq!(h.render_template(r#"{{concat this separator=", "}}"#, &json!({"item1": "Value 1", "item2": "Value 2"})).expect("Render error"), "item1, item2");
46/// assert_eq!(h.render_template(r#"{{#concat this separator=", "}}{{this}}{{/concat}}"#, &json!({"item1": "Value 1", "item2": "Value 2"})).expect("Render error"), "Value 1, Value 2");
47/// assert_eq!(h.render_template(r#"{{#concat "Form" this separator="" render_all=true}}<{{#if tag}}{{tag}}{{else}}{{this}}{{/if}}/>{{/concat}}"#, &json!({"key0":{"tag":"Input"},"key1":{"tag":"Select"},"key2":{"tag":"Button"}})).expect("Render error"), "<Form/><Input/><Select/><Button/>");
48/// ```
49///
50/// # Behavior
51///
52/// The helper is looking for multiple arguments of type string, array or object. Arguments are being added to an output buffer and returned altogether as string.
53///
54/// The helper has few parameters modifying the behavior slightly. For example `distinct=true` eliminates duplicate values from the output buffer, while `quotes=true` in combination with `single_quote=true` wraps the values in quotation marks.
55///
56/// ## String
57/// ~~String arguments are added directly to the output buffer.~~
58/// As of `0.1.3` strings could be handled in one of two ways:
59/// 1. By default strings are added to the output buffer without modification (other than the quotation mark modifiers).
60/// 2. If you add a block template and use the `render_all` parameter, strings will be passed as `{{this}}` to the block template.
61///
62/// The block template rendering is disabled by default for backward compatibility.
63///
64/// ## Array
65/// ~~Array arguments are iterated and added as individual strings to the output buffer.~~
66/// As of `0.1.3` arrays could be handled in one of two ways:
67/// 1. By default array values are added as individual strings to the output buffer without modification (other than the quotation mark modifiers).
68/// 2. If you add a block template and use the `render_all` parameter, array values are passed as `{{this}}` to the block template.
69///
70/// The block template rendering is disabled by default for backward compatibility.
71///
72/// ## Object
73/// Object arguments could be handled two different ways:
74/// 1. By default only the object keys are being used and the values are ignored.
75/// 2. If you add a block template the helper will use it to render the object value and
76/// concatenate it as string to the output buffer.
77///
78/// Object rendering results are subject to `distinct`, `quotes` and `single_quote` modifier parameters, just like strings and arrays.
79///
80/// # Hash parameters
81///
82/// * separator: Set specific string to join elements with. Default is ","
83/// * distinct: Eliminate duplicates upon adding to output buffer
84/// * quotes: Wrap each value in double quotation marks
85/// * single_quote: Modifier of `quotes` to switch to single quotation mark instead
86/// * render_all: Render all values using the block template, not just object values
87///
88/// # Example usage:
89///
90///
91/// Example with string literals:
92///
93/// ```handlebars
94/// {{concat "One" "Two" separator=", "}}
95/// ```
96///
97/// Result: `One, Two`
98///
99/// ---
100///
101/// ```handlebars
102/// {{concat "One" "Two" separator=", " quotes=true}}
103/// ```
104///
105/// Result: `"One", "Two"`
106///
107/// ---
108///
109/// Where `s` is `"One"`, `arr` is `["One", "Two"]` and `obj` is `{"Three":3}`
110///
111/// ```handlebars
112/// {{concat s arr obj separator=", " distinct=true}}
113/// ```
114///
115/// Result: `One, Two, Three`
116///
117/// ---
118///
119/// Where `s` is `"One"`, `arr` is `["One", "Two"]` and `obj` is `{"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}`:
120///
121/// ```handlebars
122/// {{#concat s arr obj separator=", " distinct=true}}{{label}}{{/concat}}
123/// ```
124///
125/// Result: `One, Two, Three, Four`
126///
127/// ---
128///
129/// Where `s` is `"One"`, `arr` is `["One", "Two"]` and `obj` is `{"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}`
130///
131/// ```handlebars
132/// {{#concat s arr obj separator=", " distinct=true render_all=true}}<{{#if label}}{{label}}{{else}}{{this}}{{/if}}/>{{/concat}}
133/// ```
134///
135/// Result: `<One/>, <Two/>, <Three/>, <Four/>`
136///
137/// ---
138///
139pub struct HandlebarsConcat;
140
141impl HelperDef for HandlebarsConcat {
142 fn call<'reg: 'rc, 'rc>(
143 &self,
144 h: &Helper<'rc>,
145 r: &'reg Handlebars,
146 ctx: &'rc Context,
147 rc: &mut RenderContext<'reg, 'rc>,
148 out: &mut dyn Output,
149 ) -> HelperResult {
150 let separator = if let Some(s) = h.hash_get("separator") {
151 s.render()
152 } else {
153 ",".to_string()
154 };
155
156 // filter output
157 let distinct = h.hash_get("distinct").is_some();
158
159 // enable quotation marks wrapping
160 let quotes = h.hash_get("quotes").is_some();
161
162 // as a modifier on top of "quotes", switches to single quotation
163 let single_quote = h.hash_get("single_quote").is_some();
164
165 let wrapper = if quotes {
166 if single_quote {
167 QUOTES_SINGLE
168 } else {
169 QUOTES_DOUBLE
170 }
171 } else {
172 ""
173 };
174
175 let render_all = h.hash_get("render_all").is_some(); // force all values through the block template
176
177 let template = h.template();
178
179 let mut output: Vec<String> = Vec::new();
180
181 for param in h.params() {
182 match param.value() {
183 serde_json::Value::Null => {}
184 serde_json::Value::Bool(_)
185 | serde_json::Value::Number(_)
186 | serde_json::Value::String(_) => {
187 let value = if h.is_block() && render_all {
188 // use block template to render strings
189
190 let mut content = StringOutput::default();
191
192 rc.push_block(create_block(¶m));
193 template
194 .map(|t| t.render(r, ctx, rc, &mut content))
195 .unwrap_or(Ok(()))?;
196 rc.pop_block();
197
198 content.into_string().unwrap_or_default()
199 } else {
200 param.value().render()
201 };
202
203 let value = apply_wrapper(value, wrapper, quotes);
204
205 if !value.is_empty() && (!output.contains(&value) || !distinct) {
206 output.push(value);
207 }
208 }
209 serde_json::Value::Array(ar) => {
210 if h.is_block() && render_all {
211 // use block template to render array elements
212
213 for array_item in ar {
214 let mut content = StringOutput::default();
215
216 let block = create_block(&PathAndJson::new(
217 None,
218 ScopedJson::from(array_item.clone()),
219 ));
220 rc.push_block(block);
221
222 template
223 .map(|t| t.render(r, ctx, rc, &mut content))
224 .unwrap_or(Ok(()))?;
225
226 rc.pop_block();
227
228 if let Ok(value) = content.into_string() {
229 let value = apply_wrapper(value, wrapper, quotes);
230
231 if !value.is_empty() && (!output.contains(&value) || !distinct) {
232 output.push(value);
233 }
234 }
235 }
236 } else {
237 output.append(
238 &mut ar
239 .iter()
240 .map(|item| item.render())
241 .map(|item| apply_wrapper(item, wrapper, quotes))
242 .filter(|item| {
243 if distinct {
244 !output.contains(item)
245 } else {
246 true
247 }
248 })
249 .collect::<Vec<String>>(),
250 );
251 }
252 }
253 serde_json::Value::Object(o) => {
254 if h.is_block() {
255 // use block template to render objects
256
257 for obj in o.values() {
258 let mut content = StringOutput::default();
259
260 let block = create_block(&PathAndJson::new(
261 None,
262 ScopedJson::from(obj.clone()),
263 ));
264 rc.push_block(block);
265
266 template
267 .map(|t| t.render(r, ctx, rc, &mut content))
268 .unwrap_or(Ok(()))?;
269
270 rc.pop_block();
271
272 if let Ok(value) = content.into_string() {
273 let value = apply_wrapper(value, wrapper, quotes);
274
275 if !value.is_empty() && (!output.contains(&value) || !distinct) {
276 output.push(value);
277 }
278 }
279 }
280 } else {
281 // render keys only
282
283 output.append(
284 &mut o
285 .keys()
286 .cloned()
287 .map(|item| apply_wrapper(item, wrapper, quotes))
288 .filter(|item| {
289 if distinct {
290 !output.contains(item)
291 } else {
292 true
293 }
294 })
295 .collect::<Vec<String>>(),
296 );
297 }
298 }
299 }
300 }
301
302 out.write(&output.join(&*separator))?;
303
304 Ok(())
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn it_works() {
314 use handlebars::Handlebars;
315 use serde_json::json;
316
317 let mut h = Handlebars::new();
318 h.register_helper("concat", Box::new(HandlebarsConcat));
319
320 assert_eq!(
321 h.render_template(r#"{{concat 1 2}}"#, &String::new())
322 .expect("Render error"),
323 "1,2",
324 "Failed to concat numeric literals"
325 );
326 assert_eq!(
327 h.render_template(r#"{{concat "One" "Two"}}"#, &String::new())
328 .expect("Render error"),
329 "One,Two",
330 "Failed to concat literals"
331 );
332 assert_eq!(
333 h.render_template(r#"{{concat "One" "Two" separator=", "}}"#, &String::new())
334 .expect("Render error"),
335 "One, Two",
336 "Failed to concat literals with separator"
337 );
338 assert_eq!(
339 h.render_template(
340 r#"{{concat "One" "Two" separator=", " quotes=true}}"#,
341 &String::new()
342 )
343 .expect("Render error"),
344 r#""One", "Two""#,
345 "Failed to concat literals with separator and quotes"
346 );
347 assert_eq!(
348 h.render_template(
349 r#"{{concat "One" "Two" separator=", " quotes=true single_quote=true}}"#,
350 &String::new()
351 )
352 .expect("Render error"),
353 r#"'One', 'Two'"#,
354 "Failed to concat literals with separator and single quotation marks"
355 );
356 assert_eq!(
357 h.render_template(
358 r#"{{concat s arr obj separator=", " quotes=true}}"#,
359 &json!({"arr": ["One", "Two", "Three"]})
360 )
361 .expect("Render error"),
362 r#""One", "Two", "Three""#,
363 "Failed to concat array with quotes"
364 );
365 assert_eq!(
366 h.render_template(
367 r#"{{concat s arr obj separator=", " distinct=true}}"#,
368 &json!({"s": "One", "arr": ["One", "Two"], "obj": {"Three":3}})
369 )
370 .expect("Render error"),
371 "One, Two, Three",
372 "Failed to concat literal, array and object"
373 );
374 assert_eq!(
375 h.render_template(
376 r#"{{#concat s arr obj separator=", " distinct=true}}{{label}}{{/concat}}"#,
377 &json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
378 ).expect("Render error"),
379 "One, Two, Three, Four",
380 "Failed to concat literal, array and object using block template"
381 );
382 assert_eq!(
383 h.render_template(
384 r#"{{#concat s arr obj separator=", " distinct=true quotes=true}}{{label}}{{/concat}}"#,
385 &json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
386 ).expect("Render error"),
387 r#""One", "Two", "Three", "Four""#,
388 "Failed to concat literal, array and object using block template"
389 );
390 assert_eq!(
391 h.render_template(
392 r#"{{concat obj separator=", " quotes=true}}"#,
393 &json!({"obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
394 ).expect("Render error"),
395 r#""key0", "key1", "key2""#,
396 "Failed to concat object keys with quotation marks and no distinction"
397 );
398 assert_eq!(
399 h.render_template(
400 r#"{{#concat s arr obj separator=", " distinct=true render_all=true}}<{{#if label}}{{label}}{{else}}{{this}}{{/if}}/>{{/concat}}"#,
401 &json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
402 ).expect("Render error"),
403 r#"<One/>, <Two/>, <Three/>, <Four/>"#,
404 "Failed to concat literal, array and object using block template"
405 );
406 assert_eq!(
407 h.render_template(
408 r#"{{#concat s arr obj separator=", " distinct=true render_all=true quotes=true}}[{{#if label}}{{label}}{{else}}{{this}}{{/if}}]{{/concat}}"#,
409 &json!({"s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
410 ).expect("Render error"),
411 r#""[One]", "[Two]", "[Three]", "[Four]""#,
412 "Failed to concat literal, array and object using block template"
413 );
414 assert_eq!(
415 h.render_template(
416 r#"{{#concat s arr obj separator=", " distinct=true render_all=true quotes=true}}[{{#if label}}{{label}}{{else}}{{@root/zero}}{{/if}}]{{/concat}}"#,
417 &json!({"zero":"Zero", "s": "One", "arr": ["One", "Two"], "obj": {"key0":{"label":"Two"},"key1":{"label":"Three"},"key2":{"label":"Four"}}})
418 ).expect("Render error"),
419 r#""[Zero]", "[Two]", "[Three]", "[Four]""#,
420 "Failed to concat literal, array and object using block template"
421 );
422 }
423}