liquid_lib/jekyll/
include_tag.rs

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