liquid_lib/stdlib/tags/
cycle_tag.rs

1use std::collections::HashMap;
2use std::io::Write;
3
4use liquid_core::error::{ResultLiquidExt, ResultLiquidReplaceExt};
5use liquid_core::parser::TagToken;
6use liquid_core::parser::TryMatchToken;
7use liquid_core::Expression;
8use liquid_core::Language;
9use liquid_core::Renderable;
10use liquid_core::Runtime;
11use liquid_core::ValueView;
12use liquid_core::{Error, Result};
13use liquid_core::{ParseTag, TagReflection, TagTokenIter};
14
15#[derive(Copy, Clone, Debug, Default)]
16pub struct CycleTag;
17
18impl CycleTag {
19    pub fn new() -> Self {
20        Self
21    }
22}
23
24impl TagReflection for CycleTag {
25    fn tag(&self) -> &'static str {
26        "cycle"
27    }
28
29    fn description(&self) -> &'static str {
30        ""
31    }
32}
33
34impl ParseTag for CycleTag {
35    fn parse(
36        &self,
37        arguments: TagTokenIter<'_>,
38        options: &Language,
39    ) -> Result<Box<dyn Renderable>> {
40        parse_cycle(arguments, options).map(|opt| Box::new(opt) as Box<dyn Renderable>)
41    }
42
43    fn reflection(&self) -> &dyn TagReflection {
44        self
45    }
46}
47
48/// Internal implementation of cycle, to allow easier testing.
49fn parse_cycle(mut arguments: TagTokenIter<'_>, _options: &Language) -> Result<Cycle> {
50    let mut name = String::new();
51    let mut values = Vec::new();
52
53    let first = arguments.expect_next("Identifier or value expected")?;
54    let second = arguments.next();
55    match second.as_ref().map(TagToken::as_str) {
56        Some(":") => {
57            name = match first.expect_identifier() {
58                TryMatchToken::Matches(name) => name.to_owned(),
59                TryMatchToken::Fails(name) => match name.expect_literal() {
60                    // This will allow non string literals such as 0 to be parsed as such.
61                    // Is this ok or should more specific functions be created?
62                    TryMatchToken::Matches(name) => name.to_kstr().into_string(),
63                    TryMatchToken::Fails(name) => return name.raise_error().into_err(),
64                },
65            };
66        }
67        Some(",") | None => {
68            // first argument is the first item in the cycle
69            values.push(first.expect_value().into_result()?);
70        }
71        Some(_) => {
72            return second
73                .expect("is some")
74                .raise_custom_error("\":\" or \",\" expected.")
75                .into_err();
76        }
77    }
78
79    while let Some(a) = arguments.next() {
80        values.push(a.expect_value().into_result()?);
81
82        let next = arguments.next();
83        match next.as_ref().map(TagToken::as_str) {
84            Some(",") => {}
85            None => break,
86            Some(_) => {
87                return next
88                    .expect("is some")
89                    .raise_custom_error("\",\" expected.")
90                    .into_err();
91            }
92        }
93    }
94
95    if name.is_empty() {
96        name = itertools::join(values.iter(), "-");
97    }
98
99    // no more arguments should be supplied, trying to supply them is an error
100    arguments.expect_nothing()?;
101
102    Ok(Cycle { name, values })
103}
104
105#[derive(Clone, Debug)]
106struct Cycle {
107    name: String,
108    values: Vec<Expression>,
109}
110
111impl Cycle {
112    fn trace(&self) -> String {
113        format!(
114            "{{% cycle {} %}}",
115            itertools::join(self.values.iter(), ", ")
116        )
117    }
118}
119
120impl Renderable for Cycle {
121    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
122        let expr = runtime
123            .registers()
124            .get_mut::<CycleRegister>()
125            .cycle(&self.name, &self.values)
126            .trace_with(|| self.trace().into())?;
127        let value = expr.evaluate(runtime).trace_with(|| self.trace().into())?;
128        write!(writer, "{}", value.render()).replace("Failed to render")?;
129        Ok(())
130    }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Default)]
134struct CycleRegister {
135    // The indices of all the cycles encountered during rendering.
136    cycles: HashMap<String, usize>,
137}
138
139impl CycleRegister {
140    fn cycle<'e>(&mut self, name: &str, values: &'e [Expression]) -> Result<&'e Expression> {
141        let index = self.cycle_index(name, values.len());
142        if index >= values.len() {
143            return Error::with_msg(
144                "cycle index out of bounds, most likely from mismatched cycles",
145            )
146            .context("index", format!("{index}"))
147            .context("count", format!("{}", values.len()))
148            .into_err();
149        }
150
151        Ok(&values[index])
152    }
153
154    fn cycle_index(&mut self, name: &str, max: usize) -> usize {
155        let i = self.cycles.entry(name.to_owned()).or_insert(0);
156        let j = *i;
157        *i = (*i + 1) % max;
158        j
159    }
160}
161
162#[cfg(test)]
163mod test {
164    use super::*;
165
166    use liquid_core::model::Value;
167    use liquid_core::parser;
168    use liquid_core::runtime;
169    use liquid_core::runtime::RuntimeBuilder;
170
171    fn options() -> Language {
172        let mut options = Language::default();
173        options.tags.register("cycle".to_owned(), CycleTag.into());
174        options
175    }
176
177    #[test]
178    fn unnamed_cycle_gets_a_name() {
179        let tag = parser::Tag::new("{% cycle this, cycle, has, no, name %}").unwrap();
180        let cycle = parse_cycle(tag.into_tokens(), &options()).unwrap();
181        assert!(!cycle.name.is_empty());
182    }
183
184    #[test]
185    fn named_values_are_independent() {
186        let text = concat!(
187            "{% cycle 'a': 'one', 'two', 'three' %}\n",
188            "{% cycle 'a': 'one', 'two', 'three' %}\n",
189            "{% cycle 'b': 'one', 'two', 'three' %}\n",
190            "{% cycle 'b': 'one', 'two', 'three' %}\n"
191        );
192        let template = parser::parse(text, &options())
193            .map(runtime::Template::new)
194            .unwrap();
195
196        let runtime = RuntimeBuilder::new().build();
197        let output = template.render(&runtime);
198
199        assert_eq!(output.unwrap(), "one\ntwo\none\ntwo\n");
200    }
201
202    #[test]
203    fn values_are_cycled() {
204        let text = concat!(
205            "{% cycle 'one', 'two', 'three' %}\n",
206            "{% cycle 'one', 'two', 'three' %}\n",
207            "{% cycle 'one', 'two', 'three' %}\n",
208            "{% cycle 'one', 'two', 'three' %}\n"
209        );
210        let template = parser::parse(text, &options())
211            .map(runtime::Template::new)
212            .unwrap();
213
214        let runtime = RuntimeBuilder::new().build();
215        let output = template.render(&runtime);
216
217        assert_eq!(output.unwrap(), "one\ntwo\nthree\none\n");
218    }
219
220    #[test]
221    fn values_can_be_variables() {
222        let text = concat!(
223            "{% cycle alpha, beta, gamma %}\n",
224            "{% cycle alpha, beta, gamma %}\n",
225            "{% cycle alpha, beta, gamma %}\n",
226            "{% cycle alpha, beta, gamma %}\n"
227        );
228        let template = parser::parse(text, &options())
229            .map(runtime::Template::new)
230            .unwrap();
231
232        let runtime = RuntimeBuilder::new().build();
233        runtime.set_global("alpha".into(), Value::scalar(1f64));
234        runtime.set_global("beta".into(), Value::scalar(2f64));
235        runtime.set_global("gamma".into(), Value::scalar(3f64));
236
237        let output = template.render(&runtime);
238
239        assert_eq!(output.unwrap(), "1\n2\n3\n1\n");
240    }
241
242    #[test]
243    fn bad_cycle_indices_dont_crash() {
244        // note the pair of cycle tags with the same name but a differing
245        // number of elements
246        let text = concat!("{% cycle c: 1, 2 %}\n", "{% cycle c: 1 %}\n");
247
248        let runtime = RuntimeBuilder::new().build();
249        let template = parser::parse(text, &options())
250            .map(runtime::Template::new)
251            .unwrap();
252        let output = template.render(&runtime);
253        assert!(output.is_err());
254    }
255}