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