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