liquid_lib/stdlib/tags/
include_tag.rs

1use std::io::Write;
2
3use liquid_core::error::ResultLiquidExt;
4use liquid_core::model::KString;
5use liquid_core::Expression;
6use liquid_core::Language;
7use liquid_core::Renderable;
8use liquid_core::ValueView;
9use liquid_core::{runtime::StackFrame, Runtime};
10use liquid_core::{Error, Result};
11use liquid_core::{ParseTag, TagReflection, TagTokenIter};
12
13#[derive(Copy, Clone, Debug, Default)]
14pub struct IncludeTag;
15
16impl IncludeTag {
17    pub fn new() -> Self {
18        Self
19    }
20}
21
22impl TagReflection for IncludeTag {
23    fn tag(&self) -> &'static str {
24        "include"
25    }
26
27    fn description(&self) -> &'static str {
28        ""
29    }
30}
31
32impl ParseTag for IncludeTag {
33    fn parse(
34        &self,
35        mut arguments: TagTokenIter<'_>,
36        _options: &Language,
37    ) -> Result<Box<dyn Renderable>> {
38        let partial = arguments.expect_next("Identifier or literal expected.")?;
39
40        let partial = partial.expect_value().into_result()?;
41
42        let mut vars: Vec<(KString, Expression)> = Vec::new();
43        while let Ok(next) = arguments.expect_next("") {
44            let id = next.expect_identifier().into_result()?.to_owned();
45
46            arguments
47                .expect_next("\":\" expected.")?
48                .expect_str(":")
49                .into_result_custom_msg("expected \":\" to be used for the assignment")?;
50
51            vars.push((
52                id.into(),
53                arguments
54                    .expect_next("expected value")?
55                    .expect_value()
56                    .into_result()?,
57            ));
58
59            if let Ok(comma) = arguments.expect_next("") {
60                // stop looking for variables if there is no comma
61                // currently allows for one trailing comma
62                if comma.expect_str(",").into_result().is_err() {
63                    break;
64                }
65            }
66        }
67
68        arguments.expect_nothing()?;
69
70        Ok(Box::new(Include { partial, vars }))
71    }
72
73    fn reflection(&self) -> &dyn TagReflection {
74        self
75    }
76}
77
78#[derive(Debug)]
79struct Include {
80    partial: Expression,
81    vars: Vec<(KString, Expression)>,
82}
83
84impl Renderable for Include {
85    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
86        let value = self.partial.evaluate(runtime)?;
87        if !value.is_scalar() {
88            return Error::with_msg("Can only `include` strings")
89                .context("partial", format!("{}", value.source()))
90                .into_err();
91        }
92        let name = value.to_kstr().into_owned();
93
94        {
95            // if there our additional variables creates a include object to access all the variables
96            // from e.g. { include 'image.html' path="foo.png" }
97            // then in image.html you could have <img src="{{include.path}}" />
98            let mut pass_through = std::collections::HashMap::new();
99            if !self.vars.is_empty() {
100                for (id, val) in &self.vars {
101                    let value = val
102                        .try_evaluate(runtime)
103                        .ok_or_else(|| Error::with_msg("failed to evaluate value"))?;
104
105                    pass_through.insert(id.as_ref(), value);
106                }
107            }
108
109            let scope = StackFrame::new(runtime, &pass_through);
110            let partial = scope
111                .partials()
112                .get(&name)
113                .trace_with(|| format!("{{% include {} %}}", self.partial).into())?;
114
115            partial
116                .render_to(writer, &scope)
117                .trace_with(|| format!("{{% include {} %}}", self.partial).into())
118                .context_key_with(|| self.partial.to_string().into())
119                .value_with(|| name.to_string().into())?;
120        }
121
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod test {
128    use std::borrow;
129
130    use liquid_core::parser;
131    use liquid_core::partials;
132    use liquid_core::partials::PartialCompiler;
133    use liquid_core::runtime;
134    use liquid_core::runtime::RuntimeBuilder;
135    use liquid_core::Value;
136    use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
137
138    use crate::stdlib;
139
140    use super::*;
141
142    #[derive(Default, Debug, Clone, Copy)]
143    struct TestSource;
144
145    impl partials::PartialSource for TestSource {
146        fn contains(&self, _name: &str) -> bool {
147            true
148        }
149
150        fn names(&self) -> Vec<&str> {
151            vec![]
152        }
153
154        fn try_get<'a>(&'a self, name: &str) -> Option<borrow::Cow<'a, str>> {
155            match name {
156                "example.txt" => Some(r#"{{'whooo' | size}}{%comment%}What happens{%endcomment%} {%if num < numTwo%}wat{%else%}wot{%endif%} {%if num > numTwo%}wat{%else%}wot{%endif%}"#.into()),
157                "example_var.txt" => Some(r#"{{example_var}}"#.into()),
158                "example_multi_var.txt" => Some(r#"{{example_var}} {{example}}"#.into()),
159                _ => None
160            }
161        }
162    }
163
164    fn options() -> Language {
165        let mut options = Language::default();
166        options
167            .tags
168            .register("include".to_owned(), IncludeTag.into());
169        options
170            .blocks
171            .register("comment".to_owned(), stdlib::CommentBlock.into());
172        options
173            .blocks
174            .register("if".to_owned(), stdlib::IfBlock.into());
175        options
176    }
177
178    #[derive(Clone, ParseFilter, FilterReflection)]
179    #[filter(name = "size", description = "tests helper", parsed(SizeFilter))]
180    pub(super) struct SizeFilterParser;
181
182    #[derive(Debug, Default, Display_filter)]
183    #[name = "size"]
184    pub(super) struct SizeFilter;
185
186    impl Filter for SizeFilter {
187        fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
188            if let Some(x) = input.as_scalar() {
189                Ok(Value::scalar(x.to_kstr().len() as i64))
190            } else if let Some(x) = input.as_array() {
191                Ok(Value::scalar(x.size()))
192            } else if let Some(x) = input.as_object() {
193                Ok(Value::scalar(x.size()))
194            } else {
195                Ok(Value::scalar(0i64))
196            }
197        }
198    }
199
200    #[test]
201    fn include_tag_quotes() {
202        let text = "{% include 'example.txt' %}";
203        let mut options = options();
204        options
205            .filters
206            .register("size".to_owned(), Box::new(SizeFilterParser));
207        let template = parser::parse(text, &options)
208            .map(runtime::Template::new)
209            .unwrap();
210
211        let partials = partials::OnDemandCompiler::<TestSource>::empty()
212            .compile(::std::sync::Arc::new(options))
213            .unwrap();
214        let runtime = RuntimeBuilder::new()
215            .set_partials(partials.as_ref())
216            .build();
217        runtime.set_global("num".into(), Value::scalar(5f64));
218        runtime.set_global("numTwo".into(), Value::scalar(10f64));
219        let output = template.render(&runtime).unwrap();
220        assert_eq!(output, "5 wat wot");
221    }
222
223    #[test]
224    fn include_variable() {
225        let text = "{% include 'example_var.txt' example_var:\"hello\" %}";
226        let options = options();
227        let template = parser::parse(text, &options)
228            .map(runtime::Template::new)
229            .unwrap();
230
231        let partials = partials::OnDemandCompiler::<TestSource>::empty()
232            .compile(::std::sync::Arc::new(options))
233            .unwrap();
234        let runtime = RuntimeBuilder::new()
235            .set_partials(partials.as_ref())
236            .build();
237        let output = template.render(&runtime).unwrap();
238        assert_eq!(output, "hello");
239    }
240
241    #[test]
242    fn include_multiple_variables() {
243        let text = "{% include 'example_multi_var.txt' example_var:\"hello\", example:\"world\" %}";
244        let options = options();
245        let template = parser::parse(text, &options)
246            .map(runtime::Template::new)
247            .unwrap();
248
249        let partials = partials::OnDemandCompiler::<TestSource>::empty()
250            .compile(::std::sync::Arc::new(options))
251            .unwrap();
252        let runtime = RuntimeBuilder::new()
253            .set_partials(partials.as_ref())
254            .build();
255        let output = template.render(&runtime).unwrap();
256        assert_eq!(output, "hello world");
257    }
258
259    #[test]
260    fn include_multiple_variables_trailing_comma() {
261        let text = "{% include 'example_multi_var.txt' example_var:\"hello\", example:\"dogs\", %}";
262        let options = options();
263        let template = parser::parse(text, &options)
264            .map(runtime::Template::new)
265            .unwrap();
266
267        let partials = partials::OnDemandCompiler::<TestSource>::empty()
268            .compile(::std::sync::Arc::new(options))
269            .unwrap();
270        let runtime = RuntimeBuilder::new()
271            .set_partials(partials.as_ref())
272            .build();
273        let output = template.render(&runtime).unwrap();
274        assert_eq!(output, "hello dogs");
275    }
276
277    #[test]
278    fn no_file() {
279        let text = "{% include 'file_does_not_exist.liquid' %}";
280        let mut options = options();
281        options
282            .filters
283            .register("size".to_owned(), Box::new(SizeFilterParser));
284        let template = parser::parse(text, &options)
285            .map(runtime::Template::new)
286            .unwrap();
287
288        let partials = partials::OnDemandCompiler::<TestSource>::empty()
289            .compile(::std::sync::Arc::new(options))
290            .unwrap();
291        let runtime = RuntimeBuilder::new()
292            .set_partials(partials.as_ref())
293            .build();
294        runtime.set_global("num".into(), Value::scalar(5f64));
295        runtime.set_global("numTwo".into(), Value::scalar(10f64));
296        let output = template.render(&runtime);
297        assert!(output.is_err());
298    }
299}