/rust/registry/src/index.crates.io-6f17d22bba15001f/handlebars-6.2.0/src/template.rs
Line | Count | Source (jump to first uncovered line) |
1 | | use std::collections::{HashMap, VecDeque}; |
2 | | use std::iter::Peekable; |
3 | | use std::str::FromStr; |
4 | | |
5 | | use pest::error::LineColLocation; |
6 | | use pest::iterators::Pair; |
7 | | use pest::{Parser, Position, Span}; |
8 | | use serde_json::value::Value as Json; |
9 | | |
10 | | use crate::error::{TemplateError, TemplateErrorReason}; |
11 | | use crate::grammar::{HandlebarsParser, Rule}; |
12 | | use crate::json::path::{parse_json_path_from_iter, Path}; |
13 | | use crate::support; |
14 | | |
15 | | use self::TemplateElement::{ |
16 | | Comment, DecoratorBlock, DecoratorExpression, Expression, HelperBlock, HtmlExpression, |
17 | | PartialBlock, PartialExpression, RawString, |
18 | | }; |
19 | | |
20 | | #[non_exhaustive] |
21 | | #[derive(PartialEq, Eq, Clone, Debug)] |
22 | | pub struct TemplateMapping(pub usize, pub usize); |
23 | | |
24 | | /// A handlebars template |
25 | | #[non_exhaustive] |
26 | | #[derive(PartialEq, Eq, Clone, Debug, Default)] |
27 | | pub struct Template { |
28 | | pub name: Option<String>, |
29 | | pub elements: Vec<TemplateElement>, |
30 | | pub mapping: Vec<TemplateMapping>, |
31 | | } |
32 | | |
33 | | #[derive(Default)] |
34 | | pub(crate) struct TemplateOptions { |
35 | | pub(crate) prevent_indent: bool, |
36 | | pub(crate) is_partial: bool, |
37 | | pub(crate) name: Option<String>, |
38 | | } |
39 | | |
40 | | impl TemplateOptions { |
41 | 0 | fn name(&self) -> String { |
42 | 0 | self.name.clone().unwrap_or_else(|| "Unnamed".to_owned()) |
43 | 0 | } |
44 | | } |
45 | | |
46 | | #[non_exhaustive] |
47 | | #[derive(PartialEq, Eq, Clone, Debug)] |
48 | | pub struct Subexpression { |
49 | | // we use box here avoid resursive struct definition |
50 | | pub element: Box<TemplateElement>, |
51 | | } |
52 | | |
53 | | impl Subexpression { |
54 | 0 | pub fn new( |
55 | 0 | name: Parameter, |
56 | 0 | params: Vec<Parameter>, |
57 | 0 | hash: HashMap<String, Parameter>, |
58 | 0 | ) -> Subexpression { |
59 | 0 | Subexpression { |
60 | 0 | element: Box::new(Expression(Box::new(HelperTemplate { |
61 | 0 | name, |
62 | 0 | params, |
63 | 0 | hash, |
64 | 0 | template: None, |
65 | 0 | inverse: None, |
66 | 0 | block_param: None, |
67 | 0 | block: false, |
68 | 0 | chain: false, |
69 | 0 | indent_before_write: false, |
70 | 0 | }))), |
71 | 0 | } |
72 | 0 | } |
73 | | |
74 | 0 | pub fn is_helper(&self) -> bool { |
75 | 0 | match *self.as_element() { |
76 | 0 | TemplateElement::Expression(ref ht) => !ht.is_name_only(), |
77 | 0 | _ => false, |
78 | | } |
79 | 0 | } |
80 | | |
81 | 0 | pub fn as_element(&self) -> &TemplateElement { |
82 | 0 | self.element.as_ref() |
83 | 0 | } |
84 | | |
85 | 0 | pub fn name(&self) -> &str { |
86 | 0 | match *self.as_element() { |
87 | | // FIXME: avoid unwrap here |
88 | 0 | Expression(ref ht) => ht.name.as_name().unwrap(), |
89 | 0 | _ => unreachable!(), |
90 | | } |
91 | 0 | } |
92 | | |
93 | 0 | pub fn params(&self) -> Option<&Vec<Parameter>> { |
94 | 0 | match *self.as_element() { |
95 | 0 | Expression(ref ht) => Some(&ht.params), |
96 | 0 | _ => None, |
97 | | } |
98 | 0 | } |
99 | | |
100 | 0 | pub fn hash(&self) -> Option<&HashMap<String, Parameter>> { |
101 | 0 | match *self.as_element() { |
102 | 0 | Expression(ref ht) => Some(&ht.hash), |
103 | 0 | _ => None, |
104 | | } |
105 | 0 | } |
106 | | } |
107 | | |
108 | | #[non_exhaustive] |
109 | | #[derive(PartialEq, Eq, Clone, Debug)] |
110 | | pub enum BlockParam { |
111 | | Single(Parameter), |
112 | | Pair((Parameter, Parameter)), |
113 | | } |
114 | | |
115 | | #[non_exhaustive] |
116 | | #[derive(PartialEq, Eq, Clone, Debug)] |
117 | | pub struct ExpressionSpec { |
118 | | pub name: Parameter, |
119 | | pub params: Vec<Parameter>, |
120 | | pub hash: HashMap<String, Parameter>, |
121 | | pub block_param: Option<BlockParam>, |
122 | | pub omit_pre_ws: bool, |
123 | | pub omit_pro_ws: bool, |
124 | | } |
125 | | |
126 | | #[non_exhaustive] |
127 | | #[derive(PartialEq, Eq, Clone, Debug)] |
128 | | pub enum Parameter { |
129 | | // for helper name only |
130 | | Name(String), |
131 | | // for expression, helper param and hash |
132 | | Path(Path), |
133 | | Literal(Json), |
134 | | Subexpression(Subexpression), |
135 | | } |
136 | | |
137 | | #[non_exhaustive] |
138 | | #[derive(PartialEq, Eq, Clone, Debug)] |
139 | | pub struct HelperTemplate { |
140 | | pub name: Parameter, |
141 | | pub params: Vec<Parameter>, |
142 | | pub hash: HashMap<String, Parameter>, |
143 | | pub block_param: Option<BlockParam>, |
144 | | pub template: Option<Template>, |
145 | | pub inverse: Option<Template>, |
146 | | pub block: bool, |
147 | | pub chain: bool, |
148 | | pub(crate) indent_before_write: bool, |
149 | | } |
150 | | |
151 | | impl HelperTemplate { |
152 | 0 | pub fn new(exp: ExpressionSpec, block: bool, indent_before_write: bool) -> HelperTemplate { |
153 | 0 | HelperTemplate { |
154 | 0 | name: exp.name, |
155 | 0 | params: exp.params, |
156 | 0 | hash: exp.hash, |
157 | 0 | block_param: exp.block_param, |
158 | 0 | block, |
159 | 0 | template: None, |
160 | 0 | inverse: None, |
161 | 0 | chain: false, |
162 | 0 | indent_before_write, |
163 | 0 | } |
164 | 0 | } |
165 | | |
166 | 0 | pub fn new_chain( |
167 | 0 | exp: ExpressionSpec, |
168 | 0 | block: bool, |
169 | 0 | indent_before_write: bool, |
170 | 0 | ) -> HelperTemplate { |
171 | 0 | HelperTemplate { |
172 | 0 | name: exp.name, |
173 | 0 | params: exp.params, |
174 | 0 | hash: exp.hash, |
175 | 0 | block_param: exp.block_param, |
176 | 0 | block, |
177 | 0 | template: None, |
178 | 0 | inverse: None, |
179 | 0 | chain: true, |
180 | 0 | indent_before_write, |
181 | 0 | } |
182 | 0 | } |
183 | | |
184 | | // test only |
185 | 0 | pub(crate) fn with_path(path: Path) -> HelperTemplate { |
186 | 0 | HelperTemplate { |
187 | 0 | name: Parameter::Path(path), |
188 | 0 | params: Vec::with_capacity(5), |
189 | 0 | hash: HashMap::new(), |
190 | 0 | block_param: None, |
191 | 0 | template: None, |
192 | 0 | inverse: None, |
193 | 0 | block: false, |
194 | 0 | chain: false, |
195 | 0 | indent_before_write: false, |
196 | 0 | } |
197 | 0 | } |
198 | | |
199 | 0 | pub(crate) fn is_name_only(&self) -> bool { |
200 | 0 | !self.block && self.params.is_empty() && self.hash.is_empty() |
201 | 0 | } |
202 | | |
203 | 0 | fn insert_inverse_node(&mut self, mut node: Box<HelperTemplate>) { |
204 | 0 | // Create a list in "inverse" member to hold the else-chain. |
205 | 0 | // Here we create the new template to save the else-chain node. |
206 | 0 | // The template render could render it successfully without any code add. |
207 | 0 | let mut new_chain_template = Template::new(); |
208 | 0 | node.inverse = self.inverse.take(); |
209 | 0 | new_chain_template.elements.push(HelperBlock(node)); |
210 | 0 | self.inverse = Some(new_chain_template); |
211 | 0 | } |
212 | | |
213 | 0 | fn ref_chain_head_mut(&mut self) -> Option<&mut Box<HelperTemplate>> { |
214 | 0 | if self.chain { |
215 | 0 | if let Some(inverse_tmpl) = &mut self.inverse { |
216 | 0 | assert_eq!(inverse_tmpl.elements.len(), 1); |
217 | 0 | if let HelperBlock(helper) = &mut inverse_tmpl.elements[0] { |
218 | 0 | return Some(helper); |
219 | 0 | } |
220 | 0 | } |
221 | 0 | } |
222 | 0 | None |
223 | 0 | } |
224 | | |
225 | 0 | fn set_chain_template(&mut self, tmpl: Option<Template>) { |
226 | 0 | if let Some(hepler) = self.ref_chain_head_mut() { |
227 | 0 | hepler.template = tmpl; |
228 | 0 | } else { |
229 | 0 | self.template = tmpl; |
230 | 0 | } |
231 | 0 | } |
232 | | |
233 | 0 | fn revert_chain_and_set(&mut self, inverse: Option<Template>) { |
234 | 0 | if self.chain { |
235 | 0 | let mut prev = None; |
236 | | |
237 | 0 | if let Some(head) = self.ref_chain_head_mut() { |
238 | 0 | if head.template.is_some() { |
239 | 0 | // Here the prev will hold the head inverse template. |
240 | 0 | // It will be set when reverse the chain. |
241 | 0 | prev = inverse; |
242 | 0 | } else { |
243 | 0 | // If the head already has template. set the inverse template. |
244 | 0 | head.template = inverse; |
245 | 0 | } |
246 | 0 | } |
247 | | |
248 | | // Reverse the else chain, to the normal list order. |
249 | 0 | while let Some(mut node) = self.inverse.take() { |
250 | 0 | assert_eq!(node.elements.len(), 1); |
251 | 0 | if let HelperBlock(c) = &mut node.elements[0] { |
252 | 0 | self.inverse = c.inverse.take(); |
253 | 0 | c.inverse = prev; |
254 | 0 | prev = Some(node); |
255 | 0 | } |
256 | | } |
257 | | |
258 | 0 | self.inverse = prev; |
259 | | } else { |
260 | | // If the helper has no else chain. |
261 | | // set the template to self. |
262 | 0 | if self.template.is_some() { |
263 | 0 | self.inverse = inverse; |
264 | 0 | } else { |
265 | 0 | self.template = inverse; |
266 | 0 | } |
267 | | } |
268 | 0 | } |
269 | | |
270 | 0 | fn set_chained(&mut self) { |
271 | 0 | self.chain = true; |
272 | 0 | } |
273 | | |
274 | 0 | pub fn is_chained(&self) -> bool { |
275 | 0 | self.chain |
276 | 0 | } |
277 | | } |
278 | | |
279 | | #[non_exhaustive] |
280 | | #[derive(PartialEq, Eq, Clone, Debug)] |
281 | | pub struct DecoratorTemplate { |
282 | | pub name: Parameter, |
283 | | pub params: Vec<Parameter>, |
284 | | pub hash: HashMap<String, Parameter>, |
285 | | pub template: Option<Template>, |
286 | | // for partial indent |
287 | | pub indent: Option<String>, |
288 | | pub(crate) indent_before_write: bool, |
289 | | } |
290 | | |
291 | | impl DecoratorTemplate { |
292 | 0 | pub fn new(exp: ExpressionSpec, indent_before_write: bool) -> DecoratorTemplate { |
293 | 0 | DecoratorTemplate { |
294 | 0 | name: exp.name, |
295 | 0 | params: exp.params, |
296 | 0 | hash: exp.hash, |
297 | 0 | template: None, |
298 | 0 | indent: None, |
299 | 0 | indent_before_write, |
300 | 0 | } |
301 | 0 | } |
302 | | } |
303 | | |
304 | | impl Parameter { |
305 | 0 | pub fn as_name(&self) -> Option<&str> { |
306 | 0 | match self { |
307 | 0 | Parameter::Name(ref n) => Some(n), |
308 | 0 | Parameter::Path(ref p) => Some(p.raw()), |
309 | 0 | _ => None, |
310 | | } |
311 | 0 | } |
312 | | |
313 | 0 | pub fn parse(s: &str) -> Result<Parameter, TemplateError> { |
314 | 0 | let parser = HandlebarsParser::parse(Rule::parameter, s) |
315 | 0 | .map_err(|_| TemplateError::of(TemplateErrorReason::InvalidParam(s.to_owned())))?; |
316 | | |
317 | 0 | let mut it = parser.flatten().peekable(); |
318 | 0 | Template::parse_param(s, &mut it, s.len() - 1) |
319 | 0 | } |
320 | | |
321 | 0 | fn debug_name(&self) -> String { |
322 | 0 | if let Some(name) = self.as_name() { |
323 | 0 | name.to_owned() |
324 | | } else { |
325 | 0 | format!("{self:?}") |
326 | | } |
327 | 0 | } |
328 | | } |
329 | | |
330 | | impl Template { |
331 | 0 | pub fn new() -> Template { |
332 | 0 | Template::default() |
333 | 0 | } |
334 | | |
335 | 0 | fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) { |
336 | 0 | self.elements.push(e); |
337 | 0 | self.mapping.push(TemplateMapping(line, col)); |
338 | 0 | } |
339 | | |
340 | 0 | fn parse_subexpression<'a, I>( |
341 | 0 | source: &'a str, |
342 | 0 | it: &mut Peekable<I>, |
343 | 0 | limit: usize, |
344 | 0 | ) -> Result<Parameter, TemplateError> |
345 | 0 | where |
346 | 0 | I: Iterator<Item = Pair<'a, Rule>>, |
347 | 0 | { |
348 | 0 | let espec = Template::parse_expression(source, it.by_ref(), limit)?; |
349 | 0 | Ok(Parameter::Subexpression(Subexpression::new( |
350 | 0 | espec.name, |
351 | 0 | espec.params, |
352 | 0 | espec.hash, |
353 | 0 | ))) |
354 | 0 | } Unexecuted instantiation: <handlebars::template::Template>::parse_subexpression::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>> Unexecuted instantiation: <handlebars::template::Template>::parse_subexpression::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>> |
355 | | |
356 | 0 | fn parse_name<'a, I>( |
357 | 0 | source: &'a str, |
358 | 0 | it: &mut Peekable<I>, |
359 | 0 | _: usize, |
360 | 0 | ) -> Result<Parameter, TemplateError> |
361 | 0 | where |
362 | 0 | I: Iterator<Item = Pair<'a, Rule>>, |
363 | 0 | { |
364 | 0 | let name_node = it.next().unwrap(); |
365 | 0 | let rule = name_node.as_rule(); |
366 | 0 | let name_span = name_node.as_span(); |
367 | 0 | match rule { |
368 | | Rule::identifier | Rule::partial_identifier | Rule::invert_tag_item => { |
369 | 0 | Ok(Parameter::Name(name_span.as_str().to_owned())) |
370 | | } |
371 | | Rule::reference => { |
372 | 0 | let paths = parse_json_path_from_iter(it, name_span.end()); |
373 | 0 | Ok(Parameter::Path(Path::new(name_span.as_str(), paths))) |
374 | | } |
375 | | Rule::subexpression => { |
376 | 0 | Template::parse_subexpression(source, it.by_ref(), name_span.end()) |
377 | | } |
378 | 0 | _ => unreachable!(), |
379 | | } |
380 | 0 | } Unexecuted instantiation: <handlebars::template::Template>::parse_name::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>> Unexecuted instantiation: <handlebars::template::Template>::parse_name::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>> |
381 | | |
382 | 0 | fn parse_param<'a, I>( |
383 | 0 | source: &'a str, |
384 | 0 | it: &mut Peekable<I>, |
385 | 0 | _: usize, |
386 | 0 | ) -> Result<Parameter, TemplateError> |
387 | 0 | where |
388 | 0 | I: Iterator<Item = Pair<'a, Rule>>, |
389 | 0 | { |
390 | 0 | let mut param = it.next().unwrap(); |
391 | 0 | if param.as_rule() == Rule::helper_parameter { |
392 | 0 | param = it.next().unwrap(); |
393 | 0 | } |
394 | 0 | let param_rule = param.as_rule(); |
395 | 0 | let param_span = param.as_span(); |
396 | 0 | let result = match param_rule { |
397 | | Rule::reference => { |
398 | 0 | let path_segs = parse_json_path_from_iter(it, param_span.end()); |
399 | 0 | Parameter::Path(Path::new(param_span.as_str(), path_segs)) |
400 | | } |
401 | | Rule::literal => { |
402 | | // Parse the parameter as a JSON literal |
403 | 0 | let param_literal = it.next().unwrap(); |
404 | 0 | let json_result = match param_literal.as_rule() { |
405 | | Rule::string_literal |
406 | 0 | if it.peek().unwrap().as_rule() == Rule::string_inner_single_quote => |
407 | 0 | { |
408 | 0 | // ...unless the parameter is a single-quoted string. |
409 | 0 | // In that case, transform it to a double-quoted string |
410 | 0 | // and then parse it as a JSON literal. |
411 | 0 | let string_inner_single_quote = it.next().unwrap(); |
412 | 0 | let double_quoted = format!( |
413 | 0 | "\"{}\"", |
414 | 0 | string_inner_single_quote |
415 | 0 | .as_str() |
416 | 0 | .replace("\\'", "'") |
417 | 0 | .replace('"', "\\\"") |
418 | 0 | ); |
419 | 0 | Json::from_str(&double_quoted) |
420 | | } |
421 | 0 | _ => Json::from_str(param_span.as_str()), |
422 | | }; |
423 | 0 | if let Ok(json) = json_result { |
424 | 0 | Parameter::Literal(json) |
425 | | } else { |
426 | 0 | return Err(TemplateError::of(TemplateErrorReason::InvalidParam( |
427 | 0 | param_span.as_str().to_owned(), |
428 | 0 | ))); |
429 | | } |
430 | | } |
431 | | Rule::subexpression => { |
432 | 0 | Template::parse_subexpression(source, it.by_ref(), param_span.end())? |
433 | | } |
434 | 0 | _ => unreachable!(), |
435 | | }; |
436 | | |
437 | 0 | while let Some(n) = it.peek() { |
438 | 0 | let n_span = n.as_span(); |
439 | 0 | if n_span.end() > param_span.end() { |
440 | 0 | break; |
441 | 0 | } |
442 | 0 | it.next(); |
443 | | } |
444 | | |
445 | 0 | Ok(result) |
446 | 0 | } Unexecuted instantiation: <handlebars::template::Template>::parse_param::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>> Unexecuted instantiation: <handlebars::template::Template>::parse_param::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>> |
447 | | |
448 | 0 | fn parse_hash<'a, I>( |
449 | 0 | source: &'a str, |
450 | 0 | it: &mut Peekable<I>, |
451 | 0 | limit: usize, |
452 | 0 | ) -> Result<(String, Parameter), TemplateError> |
453 | 0 | where |
454 | 0 | I: Iterator<Item = Pair<'a, Rule>>, |
455 | 0 | { |
456 | 0 | let name = it.next().unwrap(); |
457 | 0 | let name_node = name.as_span(); |
458 | 0 | // identifier |
459 | 0 | let key = name_node.as_str().to_owned(); |
460 | | |
461 | 0 | let value = Template::parse_param(source, it.by_ref(), limit)?; |
462 | 0 | Ok((key, value)) |
463 | 0 | } Unexecuted instantiation: <handlebars::template::Template>::parse_hash::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>> Unexecuted instantiation: <handlebars::template::Template>::parse_hash::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>> |
464 | | |
465 | 0 | fn parse_block_param<'a, I>(_: &'a str, it: &mut Peekable<I>, limit: usize) -> BlockParam |
466 | 0 | where |
467 | 0 | I: Iterator<Item = Pair<'a, Rule>>, |
468 | 0 | { |
469 | 0 | let p1_name = it.next().unwrap(); |
470 | 0 | let p1_name_span = p1_name.as_span(); |
471 | 0 | // identifier |
472 | 0 | let p1 = p1_name_span.as_str().to_owned(); |
473 | 0 |
|
474 | 0 | let p2 = it.peek().and_then(|p2_name| { |
475 | 0 | let p2_name_span = p2_name.as_span(); |
476 | 0 | if p2_name_span.end() <= limit { |
477 | 0 | Some(p2_name_span.as_str().to_owned()) |
478 | | } else { |
479 | 0 | None |
480 | | } |
481 | 0 | }); Unexecuted instantiation: <handlebars::template::Template>::parse_block_param::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>>::{closure#0} Unexecuted instantiation: <handlebars::template::Template>::parse_block_param::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>>::{closure#0} |
482 | | |
483 | 0 | if let Some(p2) = p2 { |
484 | 0 | it.next(); |
485 | 0 | BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2))) |
486 | | } else { |
487 | 0 | BlockParam::Single(Parameter::Name(p1)) |
488 | | } |
489 | 0 | } Unexecuted instantiation: <handlebars::template::Template>::parse_block_param::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>> Unexecuted instantiation: <handlebars::template::Template>::parse_block_param::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>> |
490 | | |
491 | 0 | fn parse_expression<'a, I>( |
492 | 0 | source: &'a str, |
493 | 0 | it: &mut Peekable<I>, |
494 | 0 | limit: usize, |
495 | 0 | ) -> Result<ExpressionSpec, TemplateError> |
496 | 0 | where |
497 | 0 | I: Iterator<Item = Pair<'a, Rule>>, |
498 | 0 | { |
499 | 0 | let mut params: Vec<Parameter> = Vec::new(); |
500 | 0 | let mut hashes: HashMap<String, Parameter> = HashMap::new(); |
501 | 0 | let mut omit_pre_ws = false; |
502 | 0 | let mut omit_pro_ws = false; |
503 | 0 | let mut block_param = None; |
504 | 0 |
|
505 | 0 | if it.peek().unwrap().as_rule() == Rule::leading_tilde_to_omit_whitespace { |
506 | 0 | omit_pre_ws = true; |
507 | 0 | it.next(); |
508 | 0 | } |
509 | | |
510 | 0 | let name = Template::parse_name(source, it.by_ref(), limit)?; |
511 | | |
512 | | loop { |
513 | | let rule; |
514 | | let end; |
515 | 0 | if let Some(pair) = it.peek() { |
516 | 0 | let pair_span = pair.as_span(); |
517 | 0 | if pair_span.end() < limit { |
518 | 0 | rule = pair.as_rule(); |
519 | 0 | end = pair_span.end(); |
520 | 0 | } else { |
521 | 0 | break; |
522 | | } |
523 | | } else { |
524 | 0 | break; |
525 | | } |
526 | | |
527 | 0 | it.next(); |
528 | 0 |
|
529 | 0 | match rule { |
530 | | Rule::helper_parameter => { |
531 | 0 | params.push(Template::parse_param(source, it.by_ref(), end)?); |
532 | | } |
533 | | Rule::hash => { |
534 | 0 | let (key, value) = Template::parse_hash(source, it.by_ref(), end)?; |
535 | 0 | hashes.insert(key, value); |
536 | | } |
537 | 0 | Rule::block_param => { |
538 | 0 | block_param = Some(Template::parse_block_param(source, it.by_ref(), end)); |
539 | 0 | } |
540 | 0 | Rule::trailing_tilde_to_omit_whitespace => { |
541 | 0 | omit_pro_ws = true; |
542 | 0 | } |
543 | 0 | _ => {} |
544 | | } |
545 | | } |
546 | 0 | Ok(ExpressionSpec { |
547 | 0 | name, |
548 | 0 | params, |
549 | 0 | hash: hashes, |
550 | 0 | block_param, |
551 | 0 | omit_pre_ws, |
552 | 0 | omit_pro_ws, |
553 | 0 | }) |
554 | 0 | } Unexecuted instantiation: <handlebars::template::Template>::parse_expression::<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>> Unexecuted instantiation: <handlebars::template::Template>::parse_expression::<core::iter::adapters::filter::Filter<pest::iterators::flat_pairs::FlatPairs<handlebars::grammar::Rule>, <handlebars::template::Template>::compile2::{closure#1}>> |
555 | | |
556 | 0 | fn remove_previous_whitespace(template_stack: &mut VecDeque<Template>) { |
557 | 0 | let t = template_stack.front_mut().unwrap(); |
558 | 0 | if let Some(RawString(ref mut text)) = t.elements.last_mut() { |
559 | 0 | text.trim_end().to_owned().clone_into(text); |
560 | 0 | } |
561 | 0 | } |
562 | | |
563 | | // in handlebars, the whitespaces around statement are |
564 | | // automatically trimed. |
565 | | // this function checks if current span has both leading and |
566 | | // trailing whitespaces, which we treat as a standalone statement. |
567 | | // |
568 | | // |
569 | 0 | fn process_standalone_statement( |
570 | 0 | template_stack: &mut VecDeque<Template>, |
571 | 0 | source: &str, |
572 | 0 | current_span: &Span<'_>, |
573 | 0 | prevent_indent: bool, |
574 | 0 | is_partial: bool, |
575 | 0 | ) -> bool { |
576 | 0 | let continuation = &source[current_span.end()..]; |
577 | 0 |
|
578 | 0 | let mut with_trailing_newline = support::str::starts_with_empty_line(continuation); |
579 | 0 |
|
580 | 0 | // For full templates, we behave as if there was a trailing newline if we encounter |
581 | 0 | // the end of input. See #611. |
582 | 0 | with_trailing_newline |= !is_partial && continuation.is_empty(); |
583 | | |
584 | 0 | if with_trailing_newline { |
585 | 0 | let with_leading_newline = |
586 | 0 | support::str::ends_with_empty_line(&source[..current_span.start()]); |
587 | 0 |
|
588 | 0 | // prevent_indent: a special toggle for partial expression |
589 | 0 | // (>) that leading whitespaces are kept |
590 | 0 | if prevent_indent && with_leading_newline { |
591 | 0 | let t = template_stack.front_mut().unwrap(); |
592 | | // check the last element before current |
593 | 0 | if let Some(RawString(ref mut text)) = t.elements.last_mut() { |
594 | 0 | // trim leading space for standalone statement |
595 | 0 | text.trim_end_matches(support::str::whitespace_matcher) |
596 | 0 | .to_owned() |
597 | 0 | .clone_into(text); |
598 | 0 | } |
599 | 0 | } |
600 | | |
601 | | // return true when the item is the first element in root template |
602 | 0 | current_span.start() == 0 || with_leading_newline |
603 | | } else { |
604 | 0 | false |
605 | | } |
606 | 0 | } |
607 | | |
608 | 0 | fn raw_string<'a>( |
609 | 0 | source: &'a str, |
610 | 0 | pair: Option<Pair<'a, Rule>>, |
611 | 0 | trim_start: bool, |
612 | 0 | trim_start_line: bool, |
613 | 0 | ) -> TemplateElement { |
614 | 0 | let mut s = String::from(source); |
615 | | |
616 | 0 | if let Some(pair) = pair { |
617 | | // the source may contains leading space because of pest's limitation |
618 | | // we calculate none space start here in order to correct the offset |
619 | 0 | let pair_span = pair.as_span(); |
620 | 0 |
|
621 | 0 | let current_start = pair_span.start(); |
622 | 0 | let span_length = pair_span.end() - current_start; |
623 | 0 | let leading_space_offset = s.len() - span_length; |
624 | | |
625 | | // we would like to iterate pair reversely in order to remove certain |
626 | | // index from our string buffer so here we convert the inner pairs to |
627 | | // a vector. |
628 | 0 | for sub_pair in pair.into_inner().rev() { |
629 | | // remove escaped backslash |
630 | 0 | if sub_pair.as_rule() == Rule::escape { |
631 | 0 | let escape_span = sub_pair.as_span(); |
632 | 0 |
|
633 | 0 | let backslash_pos = escape_span.start(); |
634 | 0 | let backslash_rel_pos = leading_space_offset + backslash_pos - current_start; |
635 | 0 | s.remove(backslash_rel_pos); |
636 | 0 | } |
637 | | } |
638 | 0 | } |
639 | | |
640 | 0 | if trim_start { |
641 | 0 | RawString(s.trim_start().to_owned()) |
642 | 0 | } else if trim_start_line { |
643 | 0 | let s = s.trim_start_matches(support::str::whitespace_matcher); |
644 | 0 | RawString(support::str::strip_first_newline(s).to_owned()) |
645 | | } else { |
646 | 0 | RawString(s) |
647 | | } |
648 | 0 | } |
649 | | |
650 | 0 | pub(crate) fn compile2( |
651 | 0 | source: &str, |
652 | 0 | options: TemplateOptions, |
653 | 0 | ) -> Result<Template, TemplateError> { |
654 | 0 | let mut helper_stack: VecDeque<HelperTemplate> = VecDeque::new(); |
655 | 0 | let mut decorator_stack: VecDeque<DecoratorTemplate> = VecDeque::new(); |
656 | 0 | let mut template_stack: VecDeque<Template> = VecDeque::new(); |
657 | 0 |
|
658 | 0 | let mut omit_pro_ws = false; |
659 | 0 | // flag for newline removal of standalone statements |
660 | 0 | // this option is marked as true when standalone statement is detected |
661 | 0 | // then the leading whitespaces and newline of next rawstring will be trimed |
662 | 0 | let mut trim_line_required = false; |
663 | | |
664 | 0 | let parser_queue = HandlebarsParser::parse(Rule::handlebars, source).map_err(|e| { |
665 | 0 | let (line_no, col_no) = match e.line_col { |
666 | 0 | LineColLocation::Pos(line_col) => line_col, |
667 | 0 | LineColLocation::Span(line_col, _) => line_col, |
668 | | }; |
669 | 0 | TemplateError::of(TemplateErrorReason::InvalidSyntax( |
670 | 0 | e.variant.message().to_string(), |
671 | 0 | )) |
672 | 0 | .at(source, line_no, col_no) |
673 | 0 | .in_template(options.name()) |
674 | 0 | })?; |
675 | | |
676 | | // dbg!(parser_queue.clone().flatten()); |
677 | | |
678 | | // remove escape from our pair queue |
679 | 0 | let mut it = parser_queue |
680 | 0 | .flatten() |
681 | 0 | .filter(|p| { |
682 | | // remove rules that should be silent but not for now due to pest limitation |
683 | 0 | !matches!(p.as_rule(), Rule::escape) |
684 | 0 | }) |
685 | 0 | .peekable(); |
686 | 0 | let mut end_pos: Option<Position<'_>> = None; |
687 | | loop { |
688 | 0 | if let Some(pair) = it.next() { |
689 | 0 | let prev_end = end_pos.as_ref().map_or(0, pest::Position::pos); |
690 | 0 | let rule = pair.as_rule(); |
691 | 0 | let span = pair.as_span(); |
692 | | |
693 | 0 | let is_trailing_string = rule != Rule::template |
694 | 0 | && span.start() != prev_end |
695 | 0 | && !omit_pro_ws |
696 | 0 | && rule != Rule::raw_text |
697 | 0 | && rule != Rule::raw_block_text; |
698 | | |
699 | 0 | if is_trailing_string { |
700 | | // trailing string check |
701 | 0 | let (line_no, col_no) = span.start_pos().line_col(); |
702 | 0 | if rule == Rule::raw_block_end { |
703 | 0 | let mut t = Template::new(); |
704 | 0 | t.push_element( |
705 | 0 | Template::raw_string( |
706 | 0 | &source[prev_end..span.start()], |
707 | 0 | None, |
708 | 0 | false, |
709 | 0 | trim_line_required, |
710 | 0 | ), |
711 | 0 | line_no, |
712 | 0 | col_no, |
713 | 0 | ); |
714 | 0 | template_stack.push_front(t); |
715 | 0 | } else { |
716 | 0 | let t = template_stack.front_mut().unwrap(); |
717 | 0 | t.push_element( |
718 | 0 | Template::raw_string( |
719 | 0 | &source[prev_end..span.start()], |
720 | 0 | None, |
721 | 0 | false, |
722 | 0 | trim_line_required, |
723 | 0 | ), |
724 | 0 | line_no, |
725 | 0 | col_no, |
726 | 0 | ); |
727 | 0 | } |
728 | | |
729 | | // reset standalone statement marker |
730 | 0 | trim_line_required = false; |
731 | 0 | } |
732 | | |
733 | 0 | let (line_no, col_no) = span.start_pos().line_col(); |
734 | 0 | match rule { |
735 | 0 | Rule::template => { |
736 | 0 | template_stack.push_front(Template::new()); |
737 | 0 | } |
738 | | Rule::raw_text => { |
739 | | // leading space fix |
740 | 0 | let start = if span.start() != prev_end { |
741 | 0 | prev_end |
742 | | } else { |
743 | 0 | span.start() |
744 | | }; |
745 | | |
746 | 0 | let t = template_stack.front_mut().unwrap(); |
747 | 0 |
|
748 | 0 | t.push_element( |
749 | 0 | Template::raw_string( |
750 | 0 | &source[start..span.end()], |
751 | 0 | Some(pair.clone()), |
752 | 0 | omit_pro_ws, |
753 | 0 | trim_line_required, |
754 | 0 | ), |
755 | 0 | line_no, |
756 | 0 | col_no, |
757 | 0 | ); |
758 | 0 |
|
759 | 0 | // reset standalone statement marker |
760 | 0 | trim_line_required = false; |
761 | | } |
762 | | Rule::helper_block_start |
763 | | | Rule::raw_block_start |
764 | | | Rule::decorator_block_start |
765 | | | Rule::partial_block_start => { |
766 | 0 | let exp = Template::parse_expression(source, it.by_ref(), span.end())?; |
767 | | |
768 | 0 | if exp.omit_pre_ws { |
769 | 0 | Template::remove_previous_whitespace(&mut template_stack); |
770 | 0 | } |
771 | 0 | omit_pro_ws = exp.omit_pro_ws; |
772 | 0 |
|
773 | 0 | // standalone statement check, it also removes leading whitespaces of |
774 | 0 | // previous rawstring when standalone statement detected |
775 | 0 | trim_line_required = Template::process_standalone_statement( |
776 | 0 | &mut template_stack, |
777 | 0 | source, |
778 | 0 | &span, |
779 | 0 | true, |
780 | 0 | options.is_partial, |
781 | 0 | ); |
782 | | |
783 | 0 | let indent_before_write = trim_line_required && !exp.omit_pre_ws; |
784 | | |
785 | 0 | match rule { |
786 | 0 | Rule::helper_block_start | Rule::raw_block_start => { |
787 | 0 | let helper_template = |
788 | 0 | HelperTemplate::new(exp.clone(), true, indent_before_write); |
789 | 0 | helper_stack.push_front(helper_template); |
790 | 0 | } |
791 | 0 | Rule::decorator_block_start | Rule::partial_block_start => { |
792 | 0 | let decorator = |
793 | 0 | DecoratorTemplate::new(exp.clone(), indent_before_write); |
794 | 0 | decorator_stack.push_front(decorator); |
795 | 0 | } |
796 | 0 | _ => unreachable!(), |
797 | | } |
798 | | |
799 | 0 | let t = template_stack.front_mut().unwrap(); |
800 | 0 | t.mapping.push(TemplateMapping(line_no, col_no)); |
801 | | } |
802 | | Rule::invert_tag | Rule::invert_chain_tag => { |
803 | | // hack: invert_tag structure is similar to ExpressionSpec, so I |
804 | | // use it here to represent the data |
805 | | |
806 | 0 | if rule == Rule::invert_chain_tag { |
807 | 0 | let _ = Template::parse_name(source, &mut it, span.end())?; |
808 | 0 | } |
809 | 0 | let exp = Template::parse_expression(source, it.by_ref(), span.end())?; |
810 | | |
811 | 0 | if exp.omit_pre_ws { |
812 | 0 | Template::remove_previous_whitespace(&mut template_stack); |
813 | 0 | } |
814 | 0 | omit_pro_ws = exp.omit_pro_ws; |
815 | 0 |
|
816 | 0 | // standalone statement check, it also removes leading whitespaces of |
817 | 0 | // previous rawstring when standalone statement detected |
818 | 0 | trim_line_required = Template::process_standalone_statement( |
819 | 0 | &mut template_stack, |
820 | 0 | source, |
821 | 0 | &span, |
822 | 0 | true, |
823 | 0 | options.is_partial, |
824 | 0 | ); |
825 | | |
826 | 0 | let indent_before_write = trim_line_required && !exp.omit_pre_ws; |
827 | | |
828 | 0 | let t = template_stack.pop_front().unwrap(); |
829 | 0 | let h = helper_stack.front_mut().unwrap(); |
830 | 0 |
|
831 | 0 | if rule == Rule::invert_chain_tag { |
832 | 0 | h.set_chained(); |
833 | 0 | } |
834 | | |
835 | 0 | h.set_chain_template(Some(t)); |
836 | 0 | if rule == Rule::invert_chain_tag { |
837 | 0 | h.insert_inverse_node(Box::new(HelperTemplate::new_chain( |
838 | 0 | exp, |
839 | 0 | true, |
840 | 0 | indent_before_write, |
841 | 0 | ))); |
842 | 0 | } |
843 | | } |
844 | | |
845 | 0 | Rule::raw_block_text => { |
846 | 0 | let mut t = Template::new(); |
847 | 0 | t.push_element( |
848 | 0 | Template::raw_string( |
849 | 0 | span.as_str(), |
850 | 0 | Some(pair.clone()), |
851 | 0 | omit_pro_ws, |
852 | 0 | trim_line_required, |
853 | 0 | ), |
854 | 0 | line_no, |
855 | 0 | col_no, |
856 | 0 | ); |
857 | 0 | template_stack.push_front(t); |
858 | 0 | } |
859 | | Rule::expression |
860 | | | Rule::html_expression |
861 | | | Rule::decorator_expression |
862 | | | Rule::partial_expression |
863 | | | Rule::helper_block_end |
864 | | | Rule::raw_block_end |
865 | | | Rule::decorator_block_end |
866 | | | Rule::partial_block_end => { |
867 | 0 | let exp = Template::parse_expression(source, it.by_ref(), span.end())?; |
868 | | |
869 | 0 | if exp.omit_pre_ws { |
870 | 0 | Template::remove_previous_whitespace(&mut template_stack); |
871 | 0 | } |
872 | 0 | omit_pro_ws = exp.omit_pro_ws; |
873 | 0 |
|
874 | 0 | match rule { |
875 | | Rule::expression | Rule::html_expression => { |
876 | 0 | let helper_template = |
877 | 0 | HelperTemplate::new(exp.clone(), false, false); |
878 | 0 | let el = if rule == Rule::expression { |
879 | 0 | Expression(Box::new(helper_template)) |
880 | | } else { |
881 | 0 | HtmlExpression(Box::new(helper_template)) |
882 | | }; |
883 | 0 | let t = template_stack.front_mut().unwrap(); |
884 | 0 | t.push_element(el, line_no, col_no); |
885 | | } |
886 | | Rule::decorator_expression | Rule::partial_expression => { |
887 | | // do not auto trim ident spaces for |
888 | | // partial_expression(>) |
889 | 0 | let prevent_indent = |
890 | 0 | !(rule == Rule::partial_expression && options.prevent_indent); |
891 | 0 | trim_line_required = Template::process_standalone_statement( |
892 | 0 | &mut template_stack, |
893 | 0 | source, |
894 | 0 | &span, |
895 | 0 | prevent_indent, |
896 | 0 | options.is_partial, |
897 | 0 | ); |
898 | 0 |
|
899 | 0 | // indent for partial expression > |
900 | 0 | let mut indent = None; |
901 | 0 | if rule == Rule::partial_expression |
902 | 0 | && !options.prevent_indent |
903 | 0 | && !exp.omit_pre_ws |
904 | 0 | { |
905 | 0 | indent = support::str::find_trailing_whitespace_chars( |
906 | 0 | &source[..span.start()], |
907 | 0 | ); |
908 | 0 | } |
909 | | |
910 | 0 | let mut decorator = DecoratorTemplate::new( |
911 | 0 | exp.clone(), |
912 | 0 | trim_line_required && !exp.omit_pre_ws, |
913 | | ); |
914 | 0 | decorator.indent = indent.map(std::borrow::ToOwned::to_owned); |
915 | | |
916 | 0 | let el = if rule == Rule::decorator_expression { |
917 | 0 | DecoratorExpression(Box::new(decorator)) |
918 | | } else { |
919 | 0 | PartialExpression(Box::new(decorator)) |
920 | | }; |
921 | 0 | let t = template_stack.front_mut().unwrap(); |
922 | 0 | t.push_element(el, line_no, col_no); |
923 | | } |
924 | | Rule::helper_block_end | Rule::raw_block_end => { |
925 | | // standalone statement check, it also removes leading whitespaces of |
926 | | // previous rawstring when standalone statement detected |
927 | 0 | trim_line_required = Template::process_standalone_statement( |
928 | 0 | &mut template_stack, |
929 | 0 | source, |
930 | 0 | &span, |
931 | 0 | true, |
932 | 0 | options.is_partial, |
933 | 0 | ); |
934 | 0 |
|
935 | 0 | let mut h = helper_stack.pop_front().unwrap(); |
936 | 0 | let close_tag_name = exp.name.as_name(); |
937 | 0 | if h.name.as_name() == close_tag_name { |
938 | 0 | let prev_t = template_stack.pop_front().unwrap(); |
939 | 0 | h.revert_chain_and_set(Some(prev_t)); |
940 | 0 |
|
941 | 0 | let t = template_stack.front_mut().unwrap(); |
942 | 0 | t.elements.push(HelperBlock(Box::new(h))); |
943 | 0 | } else { |
944 | 0 | return Err(TemplateError::of( |
945 | 0 | TemplateErrorReason::MismatchingClosedHelper( |
946 | 0 | h.name.debug_name(), |
947 | 0 | exp.name.debug_name(), |
948 | 0 | ), |
949 | 0 | ) |
950 | 0 | .at(source, line_no, col_no) |
951 | 0 | .in_template(options.name())); |
952 | | } |
953 | | } |
954 | | Rule::decorator_block_end | Rule::partial_block_end => { |
955 | | // standalone statement check, it also removes leading whitespaces of |
956 | | // previous rawstring when standalone statement detected |
957 | 0 | trim_line_required = Template::process_standalone_statement( |
958 | 0 | &mut template_stack, |
959 | 0 | source, |
960 | 0 | &span, |
961 | 0 | true, |
962 | 0 | options.is_partial, |
963 | 0 | ); |
964 | 0 |
|
965 | 0 | let mut d = decorator_stack.pop_front().unwrap(); |
966 | 0 | let close_tag_name = exp.name.as_name(); |
967 | 0 | if d.name.as_name() == close_tag_name { |
968 | 0 | let prev_t = template_stack.pop_front().unwrap(); |
969 | 0 | d.template = Some(prev_t); |
970 | 0 | let t = template_stack.front_mut().unwrap(); |
971 | 0 | if rule == Rule::decorator_block_end { |
972 | 0 | t.elements.push(DecoratorBlock(Box::new(d))); |
973 | 0 | } else { |
974 | 0 | t.elements.push(PartialBlock(Box::new(d))); |
975 | 0 | } |
976 | | } else { |
977 | 0 | return Err(TemplateError::of( |
978 | 0 | TemplateErrorReason::MismatchingClosedDecorator( |
979 | 0 | d.name.debug_name(), |
980 | 0 | exp.name.debug_name(), |
981 | 0 | ), |
982 | 0 | ) |
983 | 0 | .at(source, line_no, col_no) |
984 | 0 | .in_template(options.name())); |
985 | | } |
986 | | } |
987 | 0 | _ => unreachable!(), |
988 | | } |
989 | | } |
990 | 0 | Rule::hbs_comment_compact => { |
991 | 0 | trim_line_required = Template::process_standalone_statement( |
992 | 0 | &mut template_stack, |
993 | 0 | source, |
994 | 0 | &span, |
995 | 0 | true, |
996 | 0 | options.is_partial, |
997 | 0 | ); |
998 | 0 |
|
999 | 0 | let text = span |
1000 | 0 | .as_str() |
1001 | 0 | .trim_start_matches("{{!") |
1002 | 0 | .trim_end_matches("}}"); |
1003 | 0 | let t = template_stack.front_mut().unwrap(); |
1004 | 0 | t.push_element(Comment(text.to_owned()), line_no, col_no); |
1005 | 0 | } |
1006 | 0 | Rule::hbs_comment => { |
1007 | 0 | trim_line_required = Template::process_standalone_statement( |
1008 | 0 | &mut template_stack, |
1009 | 0 | source, |
1010 | 0 | &span, |
1011 | 0 | true, |
1012 | 0 | options.is_partial, |
1013 | 0 | ); |
1014 | 0 |
|
1015 | 0 | let text = span |
1016 | 0 | .as_str() |
1017 | 0 | .trim_start_matches("{{!--") |
1018 | 0 | .trim_end_matches("--}}"); |
1019 | 0 | let t = template_stack.front_mut().unwrap(); |
1020 | 0 | t.push_element(Comment(text.to_owned()), line_no, col_no); |
1021 | 0 | } |
1022 | 0 | _ => {} |
1023 | | } |
1024 | | |
1025 | 0 | if rule != Rule::template { |
1026 | 0 | end_pos = Some(span.end_pos()); |
1027 | 0 | } |
1028 | | } else { |
1029 | 0 | let prev_end = end_pos.as_ref().map_or(0, pest::Position::pos); |
1030 | 0 | if prev_end < source.len() { |
1031 | 0 | let text = &source[prev_end..source.len()]; |
1032 | 0 | // is some called in if check |
1033 | 0 | let (line_no, col_no) = end_pos.unwrap().line_col(); |
1034 | 0 | let t = template_stack.front_mut().unwrap(); |
1035 | 0 | t.push_element(RawString(text.to_owned()), line_no, col_no); |
1036 | 0 | } |
1037 | 0 | let mut root_template = template_stack.pop_front().unwrap(); |
1038 | 0 | root_template.name = options.name; |
1039 | 0 | return Ok(root_template); |
1040 | | } |
1041 | | } |
1042 | 0 | } |
1043 | | |
1044 | | // These two compile functions are kept for compatibility with 4.x |
1045 | | // Template APIs in case that some developers are using them |
1046 | | // without registry. |
1047 | | |
1048 | 0 | pub fn compile(source: &str) -> Result<Template, TemplateError> { |
1049 | 0 | Self::compile2(source, TemplateOptions::default()) |
1050 | 0 | } |
1051 | | |
1052 | 0 | pub fn compile_with_name<S: AsRef<str>>( |
1053 | 0 | source: S, |
1054 | 0 | name: String, |
1055 | 0 | ) -> Result<Template, TemplateError> { |
1056 | 0 | Self::compile2( |
1057 | 0 | source.as_ref(), |
1058 | 0 | TemplateOptions { |
1059 | 0 | name: Some(name), |
1060 | 0 | ..Default::default() |
1061 | 0 | }, |
1062 | 0 | ) |
1063 | 0 | } |
1064 | | } |
1065 | | |
1066 | | #[non_exhaustive] |
1067 | | #[derive(PartialEq, Eq, Clone, Debug)] |
1068 | | pub enum TemplateElement { |
1069 | | RawString(String), |
1070 | | HtmlExpression(Box<HelperTemplate>), |
1071 | | Expression(Box<HelperTemplate>), |
1072 | | HelperBlock(Box<HelperTemplate>), |
1073 | | DecoratorExpression(Box<DecoratorTemplate>), |
1074 | | DecoratorBlock(Box<DecoratorTemplate>), |
1075 | | PartialExpression(Box<DecoratorTemplate>), |
1076 | | PartialBlock(Box<DecoratorTemplate>), |
1077 | | Comment(String), |
1078 | | } |
1079 | | |
1080 | | #[cfg(test)] |
1081 | | mod test { |
1082 | | use super::*; |
1083 | | use crate::error::TemplateErrorReason; |
1084 | | |
1085 | | #[test] |
1086 | | fn test_parse_escaped_tag_raw_string() { |
1087 | | let source = r"foo \{{bar}}"; |
1088 | | let t = Template::compile(source).ok().unwrap(); |
1089 | | assert_eq!(t.elements.len(), 1); |
1090 | | assert_eq!( |
1091 | | *t.elements.first().unwrap(), |
1092 | | RawString("foo {{bar}}".to_string()) |
1093 | | ); |
1094 | | } |
1095 | | |
1096 | | #[test] |
1097 | | fn test_pure_backslash_raw_string() { |
1098 | | let source = r"\\\\"; |
1099 | | let t = Template::compile(source).ok().unwrap(); |
1100 | | assert_eq!(t.elements.len(), 1); |
1101 | | assert_eq!(*t.elements.first().unwrap(), RawString(source.to_string())); |
1102 | | } |
1103 | | |
1104 | | #[test] |
1105 | | fn test_parse_escaped_block_raw_string() { |
1106 | | let source = r"\{{{{foo}}}} bar"; |
1107 | | let t = Template::compile(source).ok().unwrap(); |
1108 | | assert_eq!(t.elements.len(), 1); |
1109 | | assert_eq!( |
1110 | | *t.elements.first().unwrap(), |
1111 | | RawString("{{{{foo}}}} bar".to_string()) |
1112 | | ); |
1113 | | } |
1114 | | |
1115 | | #[test] |
1116 | | fn test_parse_template() { |
1117 | | let source = "<h1>{{title}} ä½ å¥½</h1> {{{content}}} |
1118 | | {{#if date}}<p>good</p>{{else}}<p>bad</p>{{/if}}<img>{{foo bar}}䏿–‡ä½ 好 |
1119 | | {{#unless true}}kitkat{{^}}lollipop{{/unless}}"; |
1120 | | let t = Template::compile(source).ok().unwrap(); |
1121 | | |
1122 | | assert_eq!(t.elements.len(), 10); |
1123 | | |
1124 | | assert_eq!(*t.elements.first().unwrap(), RawString("<h1>".to_string())); |
1125 | | assert_eq!( |
1126 | | *t.elements.get(1).unwrap(), |
1127 | | Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths( |
1128 | | &["title"] |
1129 | | )))) |
1130 | | ); |
1131 | | |
1132 | | assert_eq!( |
1133 | | *t.elements.get(3).unwrap(), |
1134 | | HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths( |
1135 | | &["content"], |
1136 | | )))) |
1137 | | ); |
1138 | | |
1139 | | match *t.elements.get(5).unwrap() { |
1140 | | HelperBlock(ref h) => { |
1141 | | assert_eq!(h.name.as_name().unwrap(), "if".to_string()); |
1142 | | assert_eq!(h.params.len(), 1); |
1143 | | assert_eq!(h.template.as_ref().unwrap().elements.len(), 1); |
1144 | | } |
1145 | | _ => { |
1146 | | panic!("Helper expected here."); |
1147 | | } |
1148 | | }; |
1149 | | |
1150 | | match *t.elements.get(7).unwrap() { |
1151 | | Expression(ref h) => { |
1152 | | assert_eq!(h.name.as_name().unwrap(), "foo".to_string()); |
1153 | | assert_eq!(h.params.len(), 1); |
1154 | | assert_eq!( |
1155 | | *(h.params.first().unwrap()), |
1156 | | Parameter::Path(Path::with_named_paths(&["bar"])) |
1157 | | ); |
1158 | | } |
1159 | | _ => { |
1160 | | panic!("Helper expression here"); |
1161 | | } |
1162 | | }; |
1163 | | |
1164 | | match *t.elements.get(9).unwrap() { |
1165 | | HelperBlock(ref h) => { |
1166 | | assert_eq!(h.name.as_name().unwrap(), "unless".to_string()); |
1167 | | assert_eq!(h.params.len(), 1); |
1168 | | assert_eq!(h.inverse.as_ref().unwrap().elements.len(), 1); |
1169 | | } |
1170 | | _ => { |
1171 | | panic!("Helper expression here"); |
1172 | | } |
1173 | | }; |
1174 | | } |
1175 | | |
1176 | | #[test] |
1177 | | fn test_parse_block_partial_path_identifier() { |
1178 | | let source = "{{#> foo/bar}}{{/foo/bar}}"; |
1179 | | assert!(Template::compile(source).is_ok()); |
1180 | | } |
1181 | | |
1182 | | #[test] |
1183 | | fn test_parse_error() { |
1184 | | let source = "{{#ifequals name compare=\"hello\"}}\nhello\n\t{{else}}\ngood"; |
1185 | | |
1186 | | let terr = Template::compile(source).unwrap_err(); |
1187 | | |
1188 | | assert!(matches!( |
1189 | | terr.reason(), |
1190 | | TemplateErrorReason::InvalidSyntax(_) |
1191 | | )); |
1192 | | assert_eq!(terr.pos(), Some((4, 5))); |
1193 | | } |
1194 | | |
1195 | | #[test] |
1196 | | fn test_subexpression() { |
1197 | | let source = |
1198 | | "{{foo (bar)}}{{foo (bar baz)}} hello {{#if (baz bar) then=(bar)}}world{{/if}}"; |
1199 | | let t = Template::compile(source).ok().unwrap(); |
1200 | | |
1201 | | assert_eq!(t.elements.len(), 4); |
1202 | | match *t.elements.first().unwrap() { |
1203 | | Expression(ref h) => { |
1204 | | assert_eq!(h.name.as_name().unwrap(), "foo".to_owned()); |
1205 | | assert_eq!(h.params.len(), 1); |
1206 | | if let Parameter::Subexpression(t) = h.params.first().unwrap() { |
1207 | | assert_eq!(t.name(), "bar".to_owned()); |
1208 | | } else { |
1209 | | panic!("Subexpression expected"); |
1210 | | } |
1211 | | } |
1212 | | _ => { |
1213 | | panic!("Helper expression expected"); |
1214 | | } |
1215 | | }; |
1216 | | |
1217 | | match *t.elements.get(1).unwrap() { |
1218 | | Expression(ref h) => { |
1219 | | assert_eq!(h.name.as_name().unwrap(), "foo".to_string()); |
1220 | | assert_eq!(h.params.len(), 1); |
1221 | | if let Parameter::Subexpression(t) = h.params.first().unwrap() { |
1222 | | assert_eq!(t.name(), "bar".to_owned()); |
1223 | | if let Some(Parameter::Path(p)) = t.params().unwrap().first() { |
1224 | | assert_eq!(p, &Path::with_named_paths(&["baz"])); |
1225 | | } else { |
1226 | | panic!("non-empty param expected "); |
1227 | | } |
1228 | | } else { |
1229 | | panic!("Subexpression expected"); |
1230 | | } |
1231 | | } |
1232 | | _ => { |
1233 | | panic!("Helper expression expected"); |
1234 | | } |
1235 | | }; |
1236 | | |
1237 | | match *t.elements.get(3).unwrap() { |
1238 | | HelperBlock(ref h) => { |
1239 | | assert_eq!(h.name.as_name().unwrap(), "if".to_string()); |
1240 | | assert_eq!(h.params.len(), 1); |
1241 | | assert_eq!(h.hash.len(), 1); |
1242 | | |
1243 | | if let Parameter::Subexpression(t) = h.params.first().unwrap() { |
1244 | | assert_eq!(t.name(), "baz".to_owned()); |
1245 | | if let Some(Parameter::Path(p)) = t.params().unwrap().first() { |
1246 | | assert_eq!(p, &Path::with_named_paths(&["bar"])); |
1247 | | } else { |
1248 | | panic!("non-empty param expected "); |
1249 | | } |
1250 | | } else { |
1251 | | panic!("Subexpression expected (baz bar)"); |
1252 | | } |
1253 | | |
1254 | | if let Parameter::Subexpression(t) = h.hash.get("then").unwrap() { |
1255 | | assert_eq!(t.name(), "bar".to_owned()); |
1256 | | } else { |
1257 | | panic!("Subexpression expected (bar)"); |
1258 | | } |
1259 | | } |
1260 | | _ => { |
1261 | | panic!("HelperBlock expected"); |
1262 | | } |
1263 | | } |
1264 | | } |
1265 | | |
1266 | | #[test] |
1267 | | fn test_white_space_omitter() { |
1268 | | let source = "hello~ {{~world~}} \n !{{~#if true}}else{{/if~}}"; |
1269 | | let t = Template::compile(source).ok().unwrap(); |
1270 | | |
1271 | | assert_eq!(t.elements.len(), 4); |
1272 | | |
1273 | | assert_eq!(t.elements[0], RawString("hello~".to_string())); |
1274 | | assert_eq!( |
1275 | | t.elements[1], |
1276 | | Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths( |
1277 | | &["world"] |
1278 | | )))) |
1279 | | ); |
1280 | | assert_eq!(t.elements[2], RawString("!".to_string())); |
1281 | | |
1282 | | let t2 = Template::compile("{{#if true}}1 {{~ else ~}} 2 {{~/if}}") |
1283 | | .ok() |
1284 | | .unwrap(); |
1285 | | assert_eq!(t2.elements.len(), 1); |
1286 | | match t2.elements[0] { |
1287 | | HelperBlock(ref h) => { |
1288 | | assert_eq!( |
1289 | | h.template.as_ref().unwrap().elements[0], |
1290 | | RawString("1".to_string()) |
1291 | | ); |
1292 | | assert_eq!( |
1293 | | h.inverse.as_ref().unwrap().elements[0], |
1294 | | RawString("2".to_string()) |
1295 | | ); |
1296 | | } |
1297 | | _ => unreachable!(), |
1298 | | } |
1299 | | } |
1300 | | |
1301 | | #[test] |
1302 | | fn test_unclosed_expression() { |
1303 | | let sources = ["{{invalid", "{{{invalid", "{{invalid}", "{{!hello"]; |
1304 | | for s in &sources { |
1305 | | let result = Template::compile(s.to_owned()); |
1306 | | let err = result.expect_err("expected a syntax error"); |
1307 | | let syntax_error_msg = match err.reason() { |
1308 | | TemplateErrorReason::InvalidSyntax(s) => s, |
1309 | | _ => panic!("InvalidSyntax expected"), |
1310 | | }; |
1311 | | assert!( |
1312 | | syntax_error_msg.contains("expected identifier"), |
1313 | | "{}", |
1314 | | syntax_error_msg |
1315 | | ); |
1316 | | } |
1317 | | } |
1318 | | |
1319 | | #[test] |
1320 | | fn test_raw_helper() { |
1321 | | let source = "hello{{{{raw}}}}good{{night}}{{{{/raw}}}}world"; |
1322 | | match Template::compile(source) { |
1323 | | Ok(t) => { |
1324 | | assert_eq!(t.elements.len(), 3); |
1325 | | assert_eq!(t.elements[0], RawString("hello".to_owned())); |
1326 | | assert_eq!(t.elements[2], RawString("world".to_owned())); |
1327 | | match t.elements[1] { |
1328 | | HelperBlock(ref h) => { |
1329 | | assert_eq!(h.name.as_name().unwrap(), "raw".to_owned()); |
1330 | | if let Some(ref ht) = h.template { |
1331 | | assert_eq!(ht.elements.len(), 1); |
1332 | | assert_eq!( |
1333 | | *ht.elements.first().unwrap(), |
1334 | | RawString("good{{night}}".to_owned()) |
1335 | | ); |
1336 | | } else { |
1337 | | panic!("helper template not found"); |
1338 | | } |
1339 | | } |
1340 | | _ => { |
1341 | | panic!("Unexpected element type"); |
1342 | | } |
1343 | | } |
1344 | | } |
1345 | | Err(e) => { |
1346 | | panic!("{}", e); |
1347 | | } |
1348 | | } |
1349 | | } |
1350 | | |
1351 | | #[test] |
1352 | | fn test_literal_parameter_parser() { |
1353 | | match Template::compile("{{hello 1 name=\"value\" valid=false ref=someref}}") { |
1354 | | Ok(t) => { |
1355 | | if let Expression(ref ht) = t.elements[0] { |
1356 | | assert_eq!(ht.params[0], Parameter::Literal(json!(1))); |
1357 | | assert_eq!( |
1358 | | ht.hash["name"], |
1359 | | Parameter::Literal(Json::String("value".to_owned())) |
1360 | | ); |
1361 | | assert_eq!(ht.hash["valid"], Parameter::Literal(Json::Bool(false))); |
1362 | | assert_eq!( |
1363 | | ht.hash["ref"], |
1364 | | Parameter::Path(Path::with_named_paths(&["someref"])) |
1365 | | ); |
1366 | | } |
1367 | | } |
1368 | | Err(e) => panic!("{}", e), |
1369 | | } |
1370 | | } |
1371 | | |
1372 | | #[test] |
1373 | | fn test_template_mapping() { |
1374 | | match Template::compile("hello\n {{~world}}\n{{#if nice}}\n\thello\n{{/if}}") { |
1375 | | Ok(t) => { |
1376 | | assert_eq!(t.mapping.len(), t.elements.len()); |
1377 | | assert_eq!(t.mapping[0], TemplateMapping(1, 1)); |
1378 | | assert_eq!(t.mapping[1], TemplateMapping(2, 3)); |
1379 | | assert_eq!(t.mapping[3], TemplateMapping(3, 1)); |
1380 | | } |
1381 | | Err(e) => panic!("{}", e), |
1382 | | } |
1383 | | } |
1384 | | |
1385 | | #[test] |
1386 | | fn test_whitespace_elements() { |
1387 | | let c = Template::compile( |
1388 | | " {{elem}}\n\t{{#if true}} \ |
1389 | | {{/if}}\n{{{{raw}}}} {{{{/raw}}}}\n{{{{raw}}}}{{{{/raw}}}}\n", |
1390 | | ); |
1391 | | let r = c.unwrap(); |
1392 | | // the \n after last raw block is dropped by pest |
1393 | | assert_eq!(r.elements.len(), 9); |
1394 | | } |
1395 | | |
1396 | | #[test] |
1397 | | fn test_block_param() { |
1398 | | match Template::compile("{{#each people as |person|}}{{person}}{{/each}}") { |
1399 | | Ok(t) => { |
1400 | | if let HelperBlock(ref ht) = t.elements[0] { |
1401 | | if let Some(BlockParam::Single(Parameter::Name(ref n))) = ht.block_param { |
1402 | | assert_eq!(n, "person"); |
1403 | | } else { |
1404 | | panic!("block param expected.") |
1405 | | } |
1406 | | } else { |
1407 | | panic!("Helper block expected"); |
1408 | | } |
1409 | | } |
1410 | | Err(e) => panic!("{}", e), |
1411 | | } |
1412 | | |
1413 | | match Template::compile("{{#each people as |val key|}}{{person}}{{/each}}") { |
1414 | | Ok(t) => { |
1415 | | if let HelperBlock(ref ht) = t.elements[0] { |
1416 | | if let Some(BlockParam::Pair(( |
1417 | | Parameter::Name(ref n1), |
1418 | | Parameter::Name(ref n2), |
1419 | | ))) = ht.block_param |
1420 | | { |
1421 | | assert_eq!(n1, "val"); |
1422 | | assert_eq!(n2, "key"); |
1423 | | } else { |
1424 | | panic!("helper block param expected."); |
1425 | | } |
1426 | | } else { |
1427 | | panic!("Helper block expected"); |
1428 | | } |
1429 | | } |
1430 | | Err(e) => panic!("{}", e), |
1431 | | } |
1432 | | } |
1433 | | |
1434 | | #[test] |
1435 | | fn test_decorator() { |
1436 | | match Template::compile("hello {{* ssh}} world") { |
1437 | | Err(e) => panic!("{}", e), |
1438 | | Ok(t) => { |
1439 | | if let DecoratorExpression(ref de) = t.elements[1] { |
1440 | | assert_eq!(de.name.as_name(), Some("ssh")); |
1441 | | assert_eq!(de.template, None); |
1442 | | } |
1443 | | } |
1444 | | } |
1445 | | |
1446 | | match Template::compile("hello {{> ssh}} world") { |
1447 | | Err(e) => panic!("{}", e), |
1448 | | Ok(t) => { |
1449 | | if let PartialExpression(ref de) = t.elements[1] { |
1450 | | assert_eq!(de.name.as_name(), Some("ssh")); |
1451 | | assert_eq!(de.template, None); |
1452 | | } |
1453 | | } |
1454 | | } |
1455 | | |
1456 | | match Template::compile("{{#*inline \"hello\"}}expand to hello{{/inline}}{{> hello}}") { |
1457 | | Err(e) => panic!("{}", e), |
1458 | | Ok(t) => { |
1459 | | if let DecoratorBlock(ref db) = t.elements[0] { |
1460 | | assert_eq!(db.name, Parameter::Name("inline".to_owned())); |
1461 | | assert_eq!( |
1462 | | db.params[0], |
1463 | | Parameter::Literal(Json::String("hello".to_owned())) |
1464 | | ); |
1465 | | assert_eq!( |
1466 | | db.template.as_ref().unwrap().elements[0], |
1467 | | TemplateElement::RawString("expand to hello".to_owned()) |
1468 | | ); |
1469 | | } |
1470 | | } |
1471 | | } |
1472 | | |
1473 | | match Template::compile("{{#> layout \"hello\"}}expand to hello{{/layout}}{{> hello}}") { |
1474 | | Err(e) => panic!("{}", e), |
1475 | | Ok(t) => { |
1476 | | if let PartialBlock(ref db) = t.elements[0] { |
1477 | | assert_eq!(db.name, Parameter::Name("layout".to_owned())); |
1478 | | assert_eq!( |
1479 | | db.params[0], |
1480 | | Parameter::Literal(Json::String("hello".to_owned())) |
1481 | | ); |
1482 | | assert_eq!( |
1483 | | db.template.as_ref().unwrap().elements[0], |
1484 | | TemplateElement::RawString("expand to hello".to_owned()) |
1485 | | ); |
1486 | | } |
1487 | | } |
1488 | | } |
1489 | | } |
1490 | | |
1491 | | #[test] |
1492 | | fn test_panic_with_tag_name() { |
1493 | | let s = "{{#>(X)}}{{/X}}"; |
1494 | | let result = Template::compile(s); |
1495 | | assert!(result.is_err()); |
1496 | | assert_eq!("decorator \"Subexpression(Subexpression { element: Expression(HelperTemplate { name: Path(Relative(([Named(\\\"X\\\")], \\\"X\\\"))), params: [], hash: {}, block_param: None, template: None, inverse: None, block: false, chain: false, indent_before_write: false }) })\" was opened, but \"X\" is closing", format!("{}", result.unwrap_err().reason())); |
1497 | | } |
1498 | | } |