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(&param));
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}