liquid_lib/jekyll/
include_tag.rs1use 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 let name = match name.expect_identifier() {
44 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}