1 use std::io::Write;
2 
3 use kstring::KString;
4 use liquid_core::error::ResultLiquidExt;
5 use liquid_core::Expression;
6 use liquid_core::Language;
7 use liquid_core::Renderable;
8 use liquid_core::ValueView;
9 use liquid_core::{runtime::StackFrame, Runtime};
10 use liquid_core::{Error, Result};
11 use liquid_core::{ParseTag, TagReflection, TagTokenIter};
12 
13 #[derive(Copy, Clone, Debug, Default)]
14 pub struct IncludeTag;
15 
16 impl IncludeTag {
new() -> Self17     pub fn new() -> Self {
18         Self::default()
19     }
20 }
21 
22 impl TagReflection for IncludeTag {
tag(&self) -> &'static str23     fn tag(&self) -> &'static str {
24         "include"
25     }
26 
description(&self) -> &'static str27     fn description(&self) -> &'static str {
28         ""
29     }
30 }
31 
32 impl ParseTag for IncludeTag {
parse( &self, mut arguments: TagTokenIter<'_>, _options: &Language, ) -> Result<Box<dyn Renderable>>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_string();
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 varaibles 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 
reflection(&self) -> &dyn TagReflection73     fn reflection(&self) -> &dyn TagReflection {
74         self
75     }
76 }
77 
78 #[derive(Debug)]
79 struct Include {
80     partial: Expression,
81     vars: Vec<(KString, Expression)>,
82 }
83 
84 impl Renderable for Include {
render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()>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 varaibles
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)]
127 mod 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 {
contains(&self, _name: &str) -> bool146         fn contains(&self, _name: &str) -> bool {
147             true
148         }
149 
names(&self) -> Vec<&str>150         fn names(&self) -> Vec<&str> {
151             vec![]
152         }
153 
try_get<'a>(&'a self, name: &str) -> Option<borrow::Cow<'a, str>>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 
options() -> Language164     fn options() -> Language {
165         let mut options = Language::default();
166         options
167             .tags
168             .register("include".to_string(), IncludeTag.into());
169         options
170             .blocks
171             .register("comment".to_string(), stdlib::CommentBlock.into());
172         options
173             .blocks
174             .register("if".to_string(), stdlib::IfBlock.into());
175         options
176     }
177 
178     #[derive(Clone, ParseFilter, FilterReflection)]
179     #[filter(name = "size", description = "tests helper", parsed(SizeFilter))]
180     pub struct SizeFilterParser;
181 
182     #[derive(Debug, Default, Display_filter)]
183     #[name = "size"]
184     pub struct SizeFilter;
185 
186     impl Filter for SizeFilter {
evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value>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]
include_tag_quotes()201     fn include_tag_quotes() {
202         let text = "{% include 'example.txt' %}";
203         let mut options = options();
204         options
205             .filters
206             .register("size".to_string(), 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]
include_varaible()224     fn include_varaible() {
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]
include_multiple_varaibles()242     fn include_multiple_varaibles() {
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]
include_multiple_varaibles_trailing_commma()260     fn include_multiple_varaibles_trailing_commma() {
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]
no_file()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_string(), 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 }
300