handlebars_repeat/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! This crate provides a [handlebars] helper function which repeats a block
4//! a given number of times (the `count`). For example:
5//!
6//! ```notrust
7//! {{#repeat 3}}
8//! hi
9//! {{/repeat}}
10//! ```
11//!
12//! Produces:
13//!
14//! ```notrust
15//! hi
16//! hi
17//! hi
18//! ```
19//!
20//! ## Local Variables
21//!
22//! Within the repeated block, there are three local variables in addition to
23//! the standard context:
24//!
25//! 1. `@index` is an integer indicating the index of the current repetition.
26//! 2. `@first` is a boolean indicating whether this is the first repetation.
27//! 3. `@last` is a boolean indicating whether this is the last repetation.
28//!
29//! For example:
30//!
31//! ```notrust
32//! {{#repeat 3}}
33//! Index: {{@index}} (first: {{@first}}; last: {{@last}})
34//! {{/repeat}}
35//! ```
36//!
37//! Produces:
38//!
39//! ```notrust
40//! Index: 0 (first: true; last: false)
41//! Index: 1 (first: false; last: false)
42//! Index: 2 (first: false; last: true)
43//! ```
44//!
45//! ## Inverse Block
46//!
47//! Like the standard `each` helper function, `repeat` can specify an inverse
48//! block which will be rendered when `count == 0`. For example:
49//!
50//! ```notrust
51//! {{#repeat 0}}
52//! foo
53//! {{else}}
54//! bar
55//! {{/repeat}}
56//! ```
57//!
58//! Produces:
59//!
60//! ```notrust
61//! bar
62//! ```
63//!
64//! [handlebars]: https://github.com/sunng87/handlebars-rust
65
66#![deny(clippy::all)]
67#![deny(missing_docs)]
68
69use handlebars::*;
70
71/// The `repeat` handler object
72///
73/// To use, register it in your handlebars registry:
74///
75/// ```rust
76/// let mut reg = handlebars::Handlebars::new();
77/// reg.register_helper("repeat", Box::new(handlebars_repeat::RepeatHelper));
78/// ```
79#[derive(Clone, Copy)]
80pub struct RepeatHelper;
81
82impl HelperDef for RepeatHelper {
83    fn call<'reg: 'rc, 'rc>(
84        &self,
85        h: &Helper<'reg, 'rc>,
86        r: &'reg Handlebars<'reg>,
87        ctx: &'rc Context,
88        rc: &mut RenderContext<'reg, 'rc>,
89        out: &mut dyn Output,
90    ) -> HelperResult {
91        let value = h
92            .param(0)
93            .ok_or_else(|| RenderError::new("`repeat` helper: cannot read parameter `count`"))?
94            .value();
95
96        let count = value.as_u64().ok_or_else(|| {
97            RenderError::new(format!(
98                "`repeat` helper: received {:?} instead of u64",
99                value
100            ))
101        })?;
102
103        let template = h
104            .template()
105            .ok_or_else(|| RenderError::new("`repeat` helper: missing block"))?;
106
107        for i in 0..count {
108            let mut block = rc.block().cloned().unwrap_or_default();
109            block.set_local_var("index", i.into());
110            block.set_local_var("first", (i == 0).into());
111            block.set_local_var("last", (i == count - 1).into());
112            rc.push_block(block);
113
114            template.render(r, ctx, rc, out)?;
115
116            rc.pop_block();
117        }
118
119        if count == 0 {
120            if let Some(template) = h.inverse() {
121                template.render(r, ctx, rc, out)?;
122            }
123        }
124
125        Ok(())
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use rstest::*;
133    use serde_json::json;
134
135    const T: &str =
136        "{{#repeat count}}{{name}}:{{@index}}:{{@first}}:{{@last}} {{else}}bar{{/repeat}}";
137
138    #[inline]
139    fn render(template: &str, count: u64) -> Result<String, RenderError> {
140        let data = json!({"name": "foo", "count": count});
141
142        let mut reg = Handlebars::new();
143        reg.register_helper("repeat", Box::new(RepeatHelper));
144        reg.render_template(template, &data)
145    }
146
147    #[rstest]
148    #[case(0, "bar")]
149    #[case(1, "foo:0:true:true ")]
150    #[case(2, "foo:0:true:false foo:1:false:true ")]
151    #[case(3, "foo:0:true:false foo:1:false:false foo:2:false:true ")]
152    fn success(#[case] count: u64, #[case] output: &str) {
153        assert_eq!(render(T, count).unwrap(), output);
154    }
155
156    #[rstest]
157    #[case(0)]
158    #[case(1)]
159    #[case(2)]
160    #[case(3)]
161    fn missing_arg(#[case] count: u64) {
162        let template = "{{#repeat}}{{name}}{{/repeat}}";
163        let err = render(template, count).unwrap_err();
164        assert!(err.desc.contains("cannot read parameter"))
165    }
166
167    #[rstest]
168    #[case(0)]
169    #[case(1)]
170    #[case(2)]
171    #[case(3)]
172    fn wrong_arg_type(#[case] count: u64) {
173        let template = "{{#repeat \"foo\"}}{{name}}{{/repeat}}";
174        let err = render(template, count).unwrap_err();
175        assert!(err.desc.contains("instead of u64"))
176    }
177
178    #[rstest]
179    #[case(0)]
180    #[case(1)]
181    #[case(2)]
182    #[case(3)]
183    fn missing_block(#[case] count: u64) {
184        let template = "{{repeat count}}";
185        let err = render(template, count).unwrap_err();
186        assert!(err.desc.contains("missing block"))
187    }
188}