liquid_lib/stdlib/tags/
include_tag.rs1use 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 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 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}