liquid_lib/stdlib/tags/
assign_tag.rs

1use std::io::Write;
2
3use liquid_core::error::ResultLiquidExt;
4use liquid_core::parser::FilterChain;
5use liquid_core::Language;
6use liquid_core::Renderable;
7use liquid_core::Result;
8use liquid_core::Runtime;
9use liquid_core::{ParseTag, TagReflection, TagTokenIter};
10
11#[derive(Copy, Clone, Debug, Default)]
12pub struct AssignTag;
13
14impl AssignTag {
15    pub fn new() -> Self {
16        Self
17    }
18}
19
20impl TagReflection for AssignTag {
21    fn tag(&self) -> &'static str {
22        "assign"
23    }
24
25    fn description(&self) -> &'static str {
26        ""
27    }
28}
29
30impl ParseTag for AssignTag {
31    fn parse(
32        &self,
33        mut arguments: TagTokenIter<'_>,
34        options: &Language,
35    ) -> Result<Box<dyn Renderable>> {
36        let dst = arguments
37            .expect_next("Identifier expected.")?
38            .expect_identifier()
39            .into_result()?
40            .to_owned()
41            .into();
42
43        arguments
44            .expect_next("Assignment operator \"=\" expected.")?
45            .expect_str("=")
46            .into_result_custom_msg("Assignment operator \"=\" expected.")?;
47
48        let src = arguments
49            .expect_next("FilterChain expected.")?
50            .expect_filter_chain(options)
51            .into_result()?;
52
53        // no more arguments should be supplied, trying to supply them is an error
54        arguments.expect_nothing()?;
55
56        Ok(Box::new(Assign { dst, src }))
57    }
58
59    fn reflection(&self) -> &dyn TagReflection {
60        self
61    }
62}
63
64#[derive(Debug)]
65struct Assign {
66    dst: liquid_core::model::KString,
67    src: FilterChain,
68}
69
70impl Assign {
71    fn trace(&self) -> String {
72        format!("{{% assign {} = {}%}}", self.dst, self.src)
73    }
74}
75
76impl Renderable for Assign {
77    fn render_to(&self, _writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
78        let value = self
79            .src
80            .evaluate(runtime)
81            .trace_with(|| self.trace().into())?
82            .into_owned();
83        runtime.set_global(self.dst.clone(), value);
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod test {
90    use super::*;
91
92    use liquid_core::model::Scalar;
93    use liquid_core::model::Value;
94    use liquid_core::parser;
95    use liquid_core::runtime;
96    use liquid_core::runtime::RuntimeBuilder;
97
98    use crate::stdlib;
99
100    fn options() -> Language {
101        let mut options = Language::default();
102        options.tags.register("assign".to_owned(), AssignTag.into());
103        options
104            .blocks
105            .register("if".to_owned(), stdlib::IfBlock.into());
106        options
107            .blocks
108            .register("for".to_owned(), stdlib::ForBlock.into());
109        options
110    }
111
112    #[test]
113    fn assign() {
114        let options = options();
115        let template = parser::parse("{% assign freestyle = false %}{{ freestyle }}", &options)
116            .map(runtime::Template::new)
117            .unwrap();
118
119        let runtime = RuntimeBuilder::new().build();
120
121        let output = template.render(&runtime).unwrap();
122        assert_eq!(output, "false");
123    }
124
125    #[test]
126    fn assign_array_indexing() {
127        let text = concat!("{% assign freestyle = tags[1] %}", "{{ freestyle }}");
128        let options = options();
129        let template = parser::parse(text, &options)
130            .map(runtime::Template::new)
131            .unwrap();
132
133        let runtime = RuntimeBuilder::new().build();
134        runtime.set_global(
135            "tags".into(),
136            Value::Array(vec![
137                Value::scalar("alpha"),
138                Value::scalar("beta"),
139                Value::scalar("gamma"),
140            ]),
141        );
142
143        let output = template.render(&runtime).unwrap();
144        assert_eq!(output, "beta");
145    }
146
147    #[test]
148    fn assign_object_indexing() {
149        let text = concat!(
150            r#"{% assign freestyle = tags["greek"] %}"#,
151            "{{ freestyle }}"
152        );
153        let options = options();
154        let template = parser::parse(text, &options)
155            .map(runtime::Template::new)
156            .unwrap();
157
158        let runtime = RuntimeBuilder::new().build();
159        runtime.set_global(
160            "tags".into(),
161            Value::Object(
162                vec![("greek".into(), Value::scalar("alpha"))]
163                    .into_iter()
164                    .collect(),
165            ),
166        );
167
168        let output = template.render(&runtime).unwrap();
169        assert_eq!(output, "alpha");
170    }
171
172    #[test]
173    fn assign_in_loop_persists_on_loop_exit() {
174        let text = concat!(
175            "{% assign freestyle = false %}",
176            "{% for t in tags %}{% if t == 'freestyle' %}",
177            "{% assign freestyle = true %}",
178            "{% endif %}{% endfor %}",
179            "{% if freestyle %}",
180            "<p>Freestyle!</p>",
181            "{% endif %}"
182        );
183
184        let options = options();
185        let template = parser::parse(text, &options)
186            .map(runtime::Template::new)
187            .unwrap();
188
189        // test one: no matching value in `tags`
190        {
191            let runtime = RuntimeBuilder::new().build();
192            runtime.set_global(
193                "tags".into(),
194                Value::Array(vec![
195                    Value::scalar("alpha"),
196                    Value::scalar("beta"),
197                    Value::scalar("gamma"),
198                ]),
199            );
200
201            let output = template.render(&runtime).unwrap();
202            assert_eq!(runtime.get(&[Scalar::new("freestyle")]).unwrap(), false);
203            assert_eq!(output, "");
204        }
205
206        // test two: matching value in `tags`
207        {
208            let runtime = RuntimeBuilder::new().build();
209            runtime.set_global(
210                "tags".into(),
211                Value::Array(vec![
212                    Value::scalar("alpha"),
213                    Value::scalar("beta"),
214                    Value::scalar("freestyle"),
215                    Value::scalar("gamma"),
216                ]),
217            );
218
219            let output = template.render(&runtime).unwrap();
220            assert_eq!(runtime.get(&[Scalar::new("freestyle")]).unwrap(), true);
221            assert_eq!(output, "<p>Freestyle!</p>");
222        }
223    }
224}