diff options
Diffstat (limited to 'vendor/handlebars/src/template.rs')
-rw-r--r-- | vendor/handlebars/src/template.rs | 1254 |
1 files changed, 1254 insertions, 0 deletions
diff --git a/vendor/handlebars/src/template.rs b/vendor/handlebars/src/template.rs new file mode 100644 index 000000000..87f853799 --- /dev/null +++ b/vendor/handlebars/src/template.rs @@ -0,0 +1,1254 @@ +use std::collections::{HashMap, VecDeque}; +use std::convert::From; +use std::iter::Peekable; +use std::str::FromStr; + +use pest::error::LineColLocation; +use pest::iterators::Pair; +use pest::{Parser, Position, Span}; +use serde_json::value::Value as Json; + +use crate::error::{TemplateError, TemplateErrorReason}; +use crate::grammar::{self, HandlebarsParser, Rule}; +use crate::json::path::{parse_json_path_from_iter, Path}; + +use self::TemplateElement::*; + +#[derive(PartialEq, Clone, Debug)] +pub struct TemplateMapping(pub usize, pub usize); + +/// A handlebars template +#[derive(PartialEq, Clone, Debug, Default)] +pub struct Template { + pub name: Option<String>, + pub elements: Vec<TemplateElement>, + pub mapping: Vec<TemplateMapping>, +} + +#[derive(PartialEq, Clone, Debug)] +pub struct Subexpression { + // we use box here avoid resursive struct definition + pub element: Box<TemplateElement>, +} + +impl Subexpression { + pub fn new( + name: Parameter, + params: Vec<Parameter>, + hash: HashMap<String, Parameter>, + ) -> Subexpression { + Subexpression { + element: Box::new(Expression(Box::new(HelperTemplate { + name, + params, + hash, + template: None, + inverse: None, + block_param: None, + block: false, + }))), + } + } + + pub fn is_helper(&self) -> bool { + match *self.as_element() { + TemplateElement::Expression(ref ht) => !ht.is_name_only(), + _ => false, + } + } + + pub fn as_element(&self) -> &TemplateElement { + self.element.as_ref() + } + + pub fn name(&self) -> &str { + match *self.as_element() { + // FIXME: avoid unwrap here + Expression(ref ht) => ht.name.as_name().unwrap(), + _ => unreachable!(), + } + } + + pub fn params(&self) -> Option<&Vec<Parameter>> { + match *self.as_element() { + Expression(ref ht) => Some(&ht.params), + _ => None, + } + } + + pub fn hash(&self) -> Option<&HashMap<String, Parameter>> { + match *self.as_element() { + Expression(ref ht) => Some(&ht.hash), + _ => None, + } + } +} + +#[derive(PartialEq, Clone, Debug)] +pub enum BlockParam { + Single(Parameter), + Pair((Parameter, Parameter)), +} + +#[derive(PartialEq, Clone, Debug)] +pub struct ExpressionSpec { + pub name: Parameter, + pub params: Vec<Parameter>, + pub hash: HashMap<String, Parameter>, + pub block_param: Option<BlockParam>, + pub omit_pre_ws: bool, + pub omit_pro_ws: bool, +} + +#[derive(PartialEq, Clone, Debug)] +pub enum Parameter { + // for helper name only + Name(String), + // for expression, helper param and hash + Path(Path), + Literal(Json), + Subexpression(Subexpression), +} + +#[derive(PartialEq, Clone, Debug)] +pub struct HelperTemplate { + pub name: Parameter, + pub params: Vec<Parameter>, + pub hash: HashMap<String, Parameter>, + pub block_param: Option<BlockParam>, + pub template: Option<Template>, + pub inverse: Option<Template>, + pub block: bool, +} + +impl HelperTemplate { + // test only + pub(crate) fn with_path(path: Path) -> HelperTemplate { + HelperTemplate { + name: Parameter::Path(path), + params: Vec::with_capacity(5), + hash: HashMap::new(), + block_param: None, + template: None, + inverse: None, + block: false, + } + } + + pub(crate) fn is_name_only(&self) -> bool { + !self.block && self.params.is_empty() && self.hash.is_empty() + } +} + +#[derive(PartialEq, Clone, Debug)] +pub struct DecoratorTemplate { + pub name: Parameter, + pub params: Vec<Parameter>, + pub hash: HashMap<String, Parameter>, + pub template: Option<Template>, +} + +impl Parameter { + pub fn as_name(&self) -> Option<&str> { + match self { + Parameter::Name(ref n) => Some(n), + Parameter::Path(ref p) => Some(p.raw()), + _ => None, + } + } + + pub fn parse(s: &str) -> Result<Parameter, TemplateError> { + let parser = HandlebarsParser::parse(Rule::parameter, s) + .map_err(|_| TemplateError::of(TemplateErrorReason::InvalidParam(s.to_owned())))?; + + let mut it = parser.flatten().peekable(); + Template::parse_param(s, &mut it, s.len() - 1) + } + + fn debug_name(&self) -> String { + if let Some(name) = self.as_name() { + name.to_owned() + } else { + format!("{:?}", self) + } + } +} + +impl Template { + pub fn new() -> Template { + Template::default() + } + + fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) { + self.elements.push(e); + self.mapping.push(TemplateMapping(line, col)); + } + + fn parse_subexpression<'a, I>( + source: &'a str, + it: &mut Peekable<I>, + limit: usize, + ) -> Result<Parameter, TemplateError> + where + I: Iterator<Item = Pair<'a, Rule>>, + { + let espec = Template::parse_expression(source, it.by_ref(), limit)?; + Ok(Parameter::Subexpression(Subexpression::new( + espec.name, + espec.params, + espec.hash, + ))) + } + + fn parse_name<'a, I>( + source: &'a str, + it: &mut Peekable<I>, + _: usize, + ) -> Result<Parameter, TemplateError> + where + I: Iterator<Item = Pair<'a, Rule>>, + { + let name_node = it.next().unwrap(); + let rule = name_node.as_rule(); + let name_span = name_node.as_span(); + match rule { + Rule::identifier | Rule::partial_identifier | Rule::invert_tag_item => { + Ok(Parameter::Name(name_span.as_str().to_owned())) + } + Rule::reference => { + let paths = parse_json_path_from_iter(it, name_span.end()); + Ok(Parameter::Path(Path::new(name_span.as_str(), paths))) + } + Rule::subexpression => { + Template::parse_subexpression(source, it.by_ref(), name_span.end()) + } + _ => unreachable!(), + } + } + + fn parse_param<'a, I>( + source: &'a str, + it: &mut Peekable<I>, + _: usize, + ) -> Result<Parameter, TemplateError> + where + I: Iterator<Item = Pair<'a, Rule>>, + { + let mut param = it.next().unwrap(); + if param.as_rule() == Rule::param { + param = it.next().unwrap(); + } + let param_rule = param.as_rule(); + let param_span = param.as_span(); + let result = match param_rule { + Rule::reference => { + let path_segs = parse_json_path_from_iter(it, param_span.end()); + Parameter::Path(Path::new(param_span.as_str(), path_segs)) + } + Rule::literal => { + let s = param_span.as_str(); + if let Ok(json) = Json::from_str(s) { + Parameter::Literal(json) + } else { + Parameter::Name(s.to_owned()) + } + } + Rule::subexpression => { + Template::parse_subexpression(source, it.by_ref(), param_span.end())? + } + _ => unreachable!(), + }; + + while let Some(n) = it.peek() { + let n_span = n.as_span(); + if n_span.end() > param_span.end() { + break; + } + it.next(); + } + + Ok(result) + } + + fn parse_hash<'a, I>( + source: &'a str, + it: &mut Peekable<I>, + limit: usize, + ) -> Result<(String, Parameter), TemplateError> + where + I: Iterator<Item = Pair<'a, Rule>>, + { + let name = it.next().unwrap(); + let name_node = name.as_span(); + // identifier + let key = name_node.as_str().to_owned(); + + let value = Template::parse_param(source, it.by_ref(), limit)?; + Ok((key, value)) + } + + fn parse_block_param<'a, I>(_: &'a str, it: &mut Peekable<I>, limit: usize) -> BlockParam + where + I: Iterator<Item = Pair<'a, Rule>>, + { + let p1_name = it.next().unwrap(); + let p1_name_span = p1_name.as_span(); + // identifier + let p1 = p1_name_span.as_str().to_owned(); + + let p2 = it.peek().and_then(|p2_name| { + let p2_name_span = p2_name.as_span(); + if p2_name_span.end() <= limit { + Some(p2_name_span.as_str().to_owned()) + } else { + None + } + }); + + if let Some(p2) = p2 { + it.next(); + BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2))) + } else { + BlockParam::Single(Parameter::Name(p1)) + } + } + + fn parse_expression<'a, I>( + source: &'a str, + it: &mut Peekable<I>, + limit: usize, + ) -> Result<ExpressionSpec, TemplateError> + where + I: Iterator<Item = Pair<'a, Rule>>, + { + let mut params: Vec<Parameter> = Vec::new(); + let mut hashes: HashMap<String, Parameter> = HashMap::new(); + let mut omit_pre_ws = false; + let mut omit_pro_ws = false; + let mut block_param = None; + + if it.peek().unwrap().as_rule() == Rule::pre_whitespace_omitter { + omit_pre_ws = true; + it.next(); + } + + let name = Template::parse_name(source, it.by_ref(), limit)?; + + loop { + let rule; + let end; + if let Some(pair) = it.peek() { + let pair_span = pair.as_span(); + if pair_span.end() < limit { + rule = pair.as_rule(); + end = pair_span.end(); + } else { + break; + } + } else { + break; + } + + it.next(); + + match rule { + Rule::param => { + params.push(Template::parse_param(source, it.by_ref(), end)?); + } + Rule::hash => { + let (key, value) = Template::parse_hash(source, it.by_ref(), end)?; + hashes.insert(key, value); + } + Rule::block_param => { + block_param = Some(Template::parse_block_param(source, it.by_ref(), end)); + } + Rule::pro_whitespace_omitter => { + omit_pro_ws = true; + } + _ => {} + } + } + Ok(ExpressionSpec { + name, + params, + hash: hashes, + block_param, + omit_pre_ws, + omit_pro_ws, + }) + } + + fn remove_previous_whitespace(template_stack: &mut VecDeque<Template>) { + let t = template_stack.front_mut().unwrap(); + if let Some(el) = t.elements.last_mut() { + if let RawString(ref mut text) = el { + *text = text.trim_end().to_owned(); + } + } + } + + fn process_standalone_statement( + template_stack: &mut VecDeque<Template>, + source: &str, + current_span: &Span<'_>, + ) -> bool { + let with_trailing_newline = grammar::starts_with_empty_line(&source[current_span.end()..]); + + if with_trailing_newline { + let with_leading_newline = + grammar::ends_with_empty_line(&source[..current_span.start()]); + + if with_leading_newline { + let t = template_stack.front_mut().unwrap(); + // check the last element before current + if let Some(el) = t.elements.last_mut() { + if let RawString(ref mut text) = el { + // trim leading space for standalone statement + *text = text + .trim_end_matches(grammar::whitespace_matcher) + .to_owned(); + } + } + } + + // return true when the item is the first element in root template + current_span.start() == 0 || with_leading_newline + } else { + false + } + } + + fn raw_string<'a>( + source: &'a str, + pair: Option<Pair<'a, Rule>>, + trim_start: bool, + trim_start_line: bool, + ) -> TemplateElement { + let mut s = String::from(source); + + if let Some(pair) = pair { + // the source may contains leading space because of pest's limitation + // we calculate none space start here in order to correct the offset + let pair_span = pair.as_span(); + + let current_start = pair_span.start(); + let span_length = pair_span.end() - current_start; + let leading_space_offset = s.len() - span_length; + + // we would like to iterate pair reversely in order to remove certain + // index from our string buffer so here we convert the inner pairs to + // a vector. + for sub_pair in pair.into_inner().rev() { + // remove escaped backslash + if sub_pair.as_rule() == Rule::escape { + let escape_span = sub_pair.as_span(); + + let backslash_pos = escape_span.start(); + let backslash_rel_pos = leading_space_offset + backslash_pos - current_start; + s.remove(backslash_rel_pos); + } + } + } + + if trim_start { + RawString(s.trim_start().to_owned()) + } else if trim_start_line { + RawString( + s.trim_start_matches(grammar::whitespace_matcher) + .trim_start_matches(grammar::newline_matcher) + .to_owned(), + ) + } else { + RawString(s) + } + } + + pub fn compile<'a>(source: &'a str) -> Result<Template, TemplateError> { + let mut helper_stack: VecDeque<HelperTemplate> = VecDeque::new(); + let mut decorator_stack: VecDeque<DecoratorTemplate> = VecDeque::new(); + let mut template_stack: VecDeque<Template> = VecDeque::new(); + + let mut omit_pro_ws = false; + // flag for newline removal of standalone statements + // this option is marked as true when standalone statement is detected + // then the leading whitespaces and newline of next rawstring will be trimed + let mut trim_line_requiered = false; + + let parser_queue = HandlebarsParser::parse(Rule::handlebars, source).map_err(|e| { + let (line_no, col_no) = match e.line_col { + LineColLocation::Pos(line_col) => line_col, + LineColLocation::Span(line_col, _) => line_col, + }; + TemplateError::of(TemplateErrorReason::InvalidSyntax).at(source, line_no, col_no) + })?; + + // dbg!(parser_queue.clone().flatten()); + + // remove escape from our pair queue + let mut it = parser_queue + .flatten() + .filter(|p| { + // remove rules that should be silent but not for now due to pest limitation + !matches!(p.as_rule(), Rule::escape) + }) + .peekable(); + let mut end_pos: Option<Position<'_>> = None; + loop { + if let Some(pair) = it.next() { + let prev_end = end_pos.as_ref().map(|p| p.pos()).unwrap_or(0); + let rule = pair.as_rule(); + let span = pair.as_span(); + + let is_trailing_string = rule != Rule::template + && span.start() != prev_end + && !omit_pro_ws + && rule != Rule::raw_text + && rule != Rule::raw_block_text; + + if is_trailing_string { + // trailing string check + let (line_no, col_no) = span.start_pos().line_col(); + if rule == Rule::raw_block_end { + let mut t = Template::new(); + t.push_element( + Template::raw_string( + &source[prev_end..span.start()], + None, + false, + trim_line_requiered, + ), + line_no, + col_no, + ); + template_stack.push_front(t); + } else { + let t = template_stack.front_mut().unwrap(); + t.push_element( + Template::raw_string( + &source[prev_end..span.start()], + None, + false, + trim_line_requiered, + ), + line_no, + col_no, + ); + } + } + + let (line_no, col_no) = span.start_pos().line_col(); + match rule { + Rule::template => { + template_stack.push_front(Template::new()); + } + Rule::raw_text => { + // leading space fix + let start = if span.start() != prev_end { + prev_end + } else { + span.start() + }; + + let t = template_stack.front_mut().unwrap(); + t.push_element( + Template::raw_string( + &source[start..span.end()], + Some(pair.clone()), + omit_pro_ws, + trim_line_requiered, + ), + line_no, + col_no, + ); + + // reset standalone statement marker + trim_line_requiered = false; + } + Rule::helper_block_start + | Rule::raw_block_start + | Rule::decorator_block_start + | Rule::partial_block_start => { + let exp = Template::parse_expression(source, it.by_ref(), span.end())?; + + match rule { + Rule::helper_block_start | Rule::raw_block_start => { + let helper_template = HelperTemplate { + name: exp.name, + params: exp.params, + hash: exp.hash, + block_param: exp.block_param, + block: true, + template: None, + inverse: None, + }; + helper_stack.push_front(helper_template); + } + Rule::decorator_block_start | Rule::partial_block_start => { + let decorator = DecoratorTemplate { + name: exp.name, + params: exp.params, + hash: exp.hash, + template: None, + }; + decorator_stack.push_front(decorator); + } + _ => unreachable!(), + } + + if exp.omit_pre_ws { + Template::remove_previous_whitespace(&mut template_stack); + } + omit_pro_ws = exp.omit_pro_ws; + + // standalone statement check, it also removes leading whitespaces of + // previous rawstring when standalone statement detected + trim_line_requiered = Template::process_standalone_statement( + &mut template_stack, + source, + &span, + ); + + let t = template_stack.front_mut().unwrap(); + t.mapping.push(TemplateMapping(line_no, col_no)); + } + Rule::invert_tag => { + // hack: invert_tag structure is similar to ExpressionSpec, so I + // use it here to represent the data + let exp = Template::parse_expression(source, it.by_ref(), span.end())?; + + if exp.omit_pre_ws { + Template::remove_previous_whitespace(&mut template_stack); + } + omit_pro_ws = exp.omit_pro_ws; + + // standalone statement check, it also removes leading whitespaces of + // previous rawstring when standalone statement detected + trim_line_requiered = Template::process_standalone_statement( + &mut template_stack, + source, + &span, + ); + + let t = template_stack.pop_front().unwrap(); + let h = helper_stack.front_mut().unwrap(); + h.template = Some(t); + } + Rule::raw_block_text => { + let mut t = Template::new(); + t.push_element( + Template::raw_string( + span.as_str(), + Some(pair.clone()), + omit_pro_ws, + trim_line_requiered, + ), + line_no, + col_no, + ); + template_stack.push_front(t); + } + Rule::expression + | Rule::html_expression + | Rule::decorator_expression + | Rule::partial_expression + | Rule::helper_block_end + | Rule::raw_block_end + | Rule::decorator_block_end + | Rule::partial_block_end => { + let exp = Template::parse_expression(source, it.by_ref(), span.end())?; + + if exp.omit_pre_ws { + Template::remove_previous_whitespace(&mut template_stack); + } + omit_pro_ws = exp.omit_pro_ws; + + match rule { + Rule::expression | Rule::html_expression => { + let helper_template = HelperTemplate { + name: exp.name, + params: exp.params, + hash: exp.hash, + block_param: exp.block_param, + block: false, + template: None, + inverse: None, + }; + let el = if rule == Rule::expression { + Expression(Box::new(helper_template)) + } else { + HtmlExpression(Box::new(helper_template)) + }; + let t = template_stack.front_mut().unwrap(); + t.push_element(el, line_no, col_no); + } + Rule::decorator_expression | Rule::partial_expression => { + let decorator = DecoratorTemplate { + name: exp.name, + params: exp.params, + hash: exp.hash, + template: None, + }; + let el = if rule == Rule::decorator_expression { + DecoratorExpression(Box::new(decorator)) + } else { + PartialExpression(Box::new(decorator)) + }; + let t = template_stack.front_mut().unwrap(); + t.push_element(el, line_no, col_no); + } + Rule::helper_block_end | Rule::raw_block_end => { + // standalone statement check, it also removes leading whitespaces of + // previous rawstring when standalone statement detected + trim_line_requiered = Template::process_standalone_statement( + &mut template_stack, + source, + &span, + ); + + let mut h = helper_stack.pop_front().unwrap(); + let close_tag_name = exp.name.as_name(); + if h.name.as_name() == close_tag_name { + let prev_t = template_stack.pop_front().unwrap(); + if h.template.is_some() { + h.inverse = Some(prev_t); + } else { + h.template = Some(prev_t); + } + let t = template_stack.front_mut().unwrap(); + t.elements.push(HelperBlock(Box::new(h))); + } else { + return Err(TemplateError::of( + TemplateErrorReason::MismatchingClosedHelper( + h.name.debug_name(), + exp.name.debug_name(), + ), + ) + .at(source, line_no, col_no)); + } + } + Rule::decorator_block_end | Rule::partial_block_end => { + // standalone statement check, it also removes leading whitespaces of + // previous rawstring when standalone statement detected + trim_line_requiered = Template::process_standalone_statement( + &mut template_stack, + source, + &span, + ); + + let mut d = decorator_stack.pop_front().unwrap(); + let close_tag_name = exp.name.as_name(); + if d.name.as_name() == close_tag_name { + let prev_t = template_stack.pop_front().unwrap(); + d.template = Some(prev_t); + let t = template_stack.front_mut().unwrap(); + if rule == Rule::decorator_block_end { + t.elements.push(DecoratorBlock(Box::new(d))); + } else { + t.elements.push(PartialBlock(Box::new(d))); + } + } else { + return Err(TemplateError::of( + TemplateErrorReason::MismatchingClosedDecorator( + d.name.debug_name(), + exp.name.debug_name(), + ), + ) + .at(source, line_no, col_no)); + } + } + _ => unreachable!(), + } + } + Rule::hbs_comment_compact => { + trim_line_requiered = Template::process_standalone_statement( + &mut template_stack, + source, + &span, + ); + + let text = span + .as_str() + .trim_start_matches("{{!") + .trim_end_matches("}}"); + let t = template_stack.front_mut().unwrap(); + t.push_element(Comment(text.to_owned()), line_no, col_no); + } + Rule::hbs_comment => { + trim_line_requiered = Template::process_standalone_statement( + &mut template_stack, + source, + &span, + ); + + let text = span + .as_str() + .trim_start_matches("{{!--") + .trim_end_matches("--}}"); + let t = template_stack.front_mut().unwrap(); + t.push_element(Comment(text.to_owned()), line_no, col_no); + } + _ => {} + } + + if rule != Rule::template { + end_pos = Some(span.end_pos()); + } + } else { + let prev_end = end_pos.as_ref().map(|e| e.pos()).unwrap_or(0); + if prev_end < source.len() { + let text = &source[prev_end..source.len()]; + // is some called in if check + let (line_no, col_no) = end_pos.unwrap().line_col(); + let t = template_stack.front_mut().unwrap(); + t.push_element(RawString(text.to_owned()), line_no, col_no); + } + let root_template = template_stack.pop_front().unwrap(); + return Ok(root_template); + } + } + } + + pub fn compile_with_name<S: AsRef<str>>( + source: S, + name: String, + ) -> Result<Template, TemplateError> { + match Template::compile(source.as_ref()) { + Ok(mut t) => { + t.name = Some(name); + Ok(t) + } + Err(e) => Err(e.in_template(name)), + } + } +} + +#[derive(PartialEq, Clone, Debug)] +pub enum TemplateElement { + RawString(String), + HtmlExpression(Box<HelperTemplate>), + Expression(Box<HelperTemplate>), + HelperBlock(Box<HelperTemplate>), + DecoratorExpression(Box<DecoratorTemplate>), + DecoratorBlock(Box<DecoratorTemplate>), + PartialExpression(Box<DecoratorTemplate>), + PartialBlock(Box<DecoratorTemplate>), + Comment(String), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::error::TemplateErrorReason; + + #[test] + fn test_parse_escaped_tag_raw_string() { + let source = r"foo \{{bar}}"; + let t = Template::compile(source).ok().unwrap(); + assert_eq!(t.elements.len(), 1); + assert_eq!( + *t.elements.get(0).unwrap(), + RawString("foo {{bar}}".to_string()) + ); + } + + #[test] + fn test_pure_backslash_raw_string() { + let source = r"\\\\"; + let t = Template::compile(source).ok().unwrap(); + assert_eq!(t.elements.len(), 1); + assert_eq!(*t.elements.get(0).unwrap(), RawString(source.to_string())); + } + + #[test] + fn test_parse_escaped_block_raw_string() { + let source = r"\{{{{foo}}}} bar"; + let t = Template::compile(source).ok().unwrap(); + assert_eq!(t.elements.len(), 1); + assert_eq!( + *t.elements.get(0).unwrap(), + RawString("{{{{foo}}}} bar".to_string()) + ); + } + + #[test] + fn test_parse_template() { + let source = "<h1>{{title}} 你好</h1> {{{content}}} +{{#if date}}<p>good</p>{{else}}<p>bad</p>{{/if}}<img>{{foo bar}}中文你好 +{{#unless true}}kitkat{{^}}lollipop{{/unless}}"; + let t = Template::compile(source).ok().unwrap(); + + assert_eq!(t.elements.len(), 10); + + assert_eq!(*t.elements.get(0).unwrap(), RawString("<h1>".to_string())); + assert_eq!( + *t.elements.get(1).unwrap(), + Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths( + &["title"] + )))) + ); + + assert_eq!( + *t.elements.get(3).unwrap(), + HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths( + &["content"], + )))) + ); + + match *t.elements.get(5).unwrap() { + HelperBlock(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "if".to_string()); + assert_eq!(h.params.len(), 1); + assert_eq!(h.template.as_ref().unwrap().elements.len(), 1); + } + _ => { + panic!("Helper expected here."); + } + }; + + match *t.elements.get(7).unwrap() { + Expression(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "foo".to_string()); + assert_eq!(h.params.len(), 1); + assert_eq!( + *(h.params.get(0).unwrap()), + Parameter::Path(Path::with_named_paths(&["bar"])) + ) + } + _ => { + panic!("Helper expression here"); + } + }; + + match *t.elements.get(9).unwrap() { + HelperBlock(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "unless".to_string()); + assert_eq!(h.params.len(), 1); + assert_eq!(h.inverse.as_ref().unwrap().elements.len(), 1); + } + _ => { + panic!("Helper expression here"); + } + }; + } + + #[test] + fn test_parse_block_partial_path_identifier() { + let source = "{{#> foo/bar}}{{/foo/bar}}"; + assert!(Template::compile(source).is_ok()); + } + + #[test] + fn test_parse_error() { + let source = "{{#ifequals name compare=\"hello\"}}\nhello\n\t{{else}}\ngood"; + + let terr = Template::compile(source).unwrap_err(); + + assert!(matches!(terr.reason, TemplateErrorReason::InvalidSyntax)); + assert_eq!(terr.line_no.unwrap(), 4); + assert_eq!(terr.column_no.unwrap(), 5); + } + + #[test] + fn test_subexpression() { + let source = + "{{foo (bar)}}{{foo (bar baz)}} hello {{#if (baz bar) then=(bar)}}world{{/if}}"; + let t = Template::compile(source).ok().unwrap(); + + assert_eq!(t.elements.len(), 4); + match *t.elements.get(0).unwrap() { + Expression(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "foo".to_owned()); + assert_eq!(h.params.len(), 1); + if let &Parameter::Subexpression(ref t) = h.params.get(0).unwrap() { + assert_eq!(t.name(), "bar".to_owned()); + } else { + panic!("Subexpression expected"); + } + } + _ => { + panic!("Helper expression expected"); + } + }; + + match *t.elements.get(1).unwrap() { + Expression(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "foo".to_string()); + assert_eq!(h.params.len(), 1); + if let &Parameter::Subexpression(ref t) = h.params.get(0).unwrap() { + assert_eq!(t.name(), "bar".to_owned()); + if let Some(Parameter::Path(p)) = t.params().unwrap().get(0) { + assert_eq!(p, &Path::with_named_paths(&["baz"])); + } else { + panic!("non-empty param expected "); + } + } else { + panic!("Subexpression expected"); + } + } + _ => { + panic!("Helper expression expected"); + } + }; + + match *t.elements.get(3).unwrap() { + HelperBlock(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "if".to_string()); + assert_eq!(h.params.len(), 1); + assert_eq!(h.hash.len(), 1); + + if let &Parameter::Subexpression(ref t) = h.params.get(0).unwrap() { + assert_eq!(t.name(), "baz".to_owned()); + if let Some(Parameter::Path(p)) = t.params().unwrap().get(0) { + assert_eq!(p, &Path::with_named_paths(&["bar"])); + } else { + panic!("non-empty param expected "); + } + } else { + panic!("Subexpression expected (baz bar)"); + } + + if let &Parameter::Subexpression(ref t) = h.hash.get("then").unwrap() { + assert_eq!(t.name(), "bar".to_owned()); + } else { + panic!("Subexpression expected (bar)"); + } + } + _ => { + panic!("HelperBlock expected"); + } + } + } + + #[test] + fn test_white_space_omitter() { + let source = "hello~ {{~world~}} \n !{{~#if true}}else{{/if~}}"; + let t = Template::compile(source).ok().unwrap(); + + assert_eq!(t.elements.len(), 4); + + assert_eq!(t.elements[0], RawString("hello~".to_string())); + assert_eq!( + t.elements[1], + Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths( + &["world"] + )))) + ); + assert_eq!(t.elements[2], RawString("!".to_string())); + + let t2 = Template::compile("{{#if true}}1 {{~ else ~}} 2 {{~/if}}") + .ok() + .unwrap(); + assert_eq!(t2.elements.len(), 1); + match t2.elements[0] { + HelperBlock(ref h) => { + assert_eq!( + h.template.as_ref().unwrap().elements[0], + RawString("1".to_string()) + ); + assert_eq!( + h.inverse.as_ref().unwrap().elements[0], + RawString("2".to_string()) + ); + } + _ => unreachable!(), + } + } + + #[test] + fn test_unclosed_expression() { + let sources = ["{{invalid", "{{{invalid", "{{invalid}", "{{!hello"]; + for s in sources.iter() { + let result = Template::compile(s.to_owned()); + if let Err(e) = result { + match e.reason { + TemplateErrorReason::InvalidSyntax => {} + _ => { + panic!("Unexpected error type {}", e); + } + } + } else { + panic!("Undetected error"); + } + } + } + + #[test] + fn test_raw_helper() { + let source = "hello{{{{raw}}}}good{{night}}{{{{/raw}}}}world"; + match Template::compile(source) { + Ok(t) => { + assert_eq!(t.elements.len(), 3); + assert_eq!(t.elements[0], RawString("hello".to_owned())); + assert_eq!(t.elements[2], RawString("world".to_owned())); + match t.elements[1] { + HelperBlock(ref h) => { + assert_eq!(h.name.as_name().unwrap(), "raw".to_owned()); + if let Some(ref ht) = h.template { + assert_eq!(ht.elements.len(), 1); + assert_eq!( + *ht.elements.get(0).unwrap(), + RawString("good{{night}}".to_owned()) + ); + } else { + panic!("helper template not found"); + } + } + _ => { + panic!("Unexpected element type"); + } + } + } + Err(e) => { + panic!("{}", e); + } + } + } + + #[test] + fn test_literal_parameter_parser() { + match Template::compile("{{hello 1 name=\"value\" valid=false ref=someref}}") { + Ok(t) => { + if let Expression(ref ht) = t.elements[0] { + assert_eq!(ht.params[0], Parameter::Literal(json!(1))); + assert_eq!( + ht.hash["name"], + Parameter::Literal(Json::String("value".to_owned())) + ); + assert_eq!(ht.hash["valid"], Parameter::Literal(Json::Bool(false))); + assert_eq!( + ht.hash["ref"], + Parameter::Path(Path::with_named_paths(&["someref"])) + ); + } + } + Err(e) => panic!("{}", e), + } + } + + #[test] + fn test_template_mapping() { + match Template::compile("hello\n {{~world}}\n{{#if nice}}\n\thello\n{{/if}}") { + Ok(t) => { + assert_eq!(t.mapping.len(), t.elements.len()); + assert_eq!(t.mapping[0], TemplateMapping(1, 1)); + assert_eq!(t.mapping[1], TemplateMapping(2, 3)); + assert_eq!(t.mapping[3], TemplateMapping(3, 1)); + } + Err(e) => panic!("{}", e), + } + } + + #[test] + fn test_whitespace_elements() { + let c = Template::compile( + " {{elem}}\n\t{{#if true}} \ + {{/if}}\n{{{{raw}}}} {{{{/raw}}}}\n{{{{raw}}}}{{{{/raw}}}}\n", + ); + let r = c.unwrap(); + // the \n after last raw block is dropped by pest + assert_eq!(r.elements.len(), 9); + } + + #[test] + fn test_block_param() { + match Template::compile("{{#each people as |person|}}{{person}}{{/each}}") { + Ok(t) => { + if let HelperBlock(ref ht) = t.elements[0] { + if let Some(BlockParam::Single(Parameter::Name(ref n))) = ht.block_param { + assert_eq!(n, "person"); + } else { + panic!("block param expected.") + } + } else { + panic!("Helper block expected"); + } + } + Err(e) => panic!("{}", e), + } + + match Template::compile("{{#each people as |val key|}}{{person}}{{/each}}") { + Ok(t) => { + if let HelperBlock(ref ht) = t.elements[0] { + if let Some(BlockParam::Pair(( + Parameter::Name(ref n1), + Parameter::Name(ref n2), + ))) = ht.block_param + { + assert_eq!(n1, "val"); + assert_eq!(n2, "key"); + } else { + panic!("helper block param expected."); + } + } else { + panic!("Helper block expected"); + } + } + Err(e) => panic!("{}", e), + } + } + + #[test] + fn test_decorator() { + match Template::compile("hello {{* ssh}} world") { + Err(e) => panic!("{}", e), + Ok(t) => { + if let DecoratorExpression(ref de) = t.elements[1] { + assert_eq!(de.name.as_name(), Some("ssh")); + assert_eq!(de.template, None); + } + } + } + + match Template::compile("hello {{> ssh}} world") { + Err(e) => panic!("{}", e), + Ok(t) => { + if let PartialExpression(ref de) = t.elements[1] { + assert_eq!(de.name.as_name(), Some("ssh")); + assert_eq!(de.template, None); + } + } + } + + match Template::compile("{{#*inline \"hello\"}}expand to hello{{/inline}}{{> hello}}") { + Err(e) => panic!("{}", e), + Ok(t) => { + if let DecoratorBlock(ref db) = t.elements[0] { + assert_eq!(db.name, Parameter::Name("inline".to_owned())); + assert_eq!( + db.params[0], + Parameter::Literal(Json::String("hello".to_owned())) + ); + assert_eq!( + db.template.as_ref().unwrap().elements[0], + TemplateElement::RawString("expand to hello".to_owned()) + ); + } + } + } + + match Template::compile("{{#> layout \"hello\"}}expand to hello{{/layout}}{{> hello}}") { + Err(e) => panic!("{}", e), + Ok(t) => { + if let PartialBlock(ref db) = t.elements[0] { + assert_eq!(db.name, Parameter::Name("layout".to_owned())); + assert_eq!( + db.params[0], + Parameter::Literal(Json::String("hello".to_owned())) + ); + assert_eq!( + db.template.as_ref().unwrap().elements[0], + TemplateElement::RawString("expand to hello".to_owned()) + ); + } + } + } + } + + #[test] + fn test_panic_with_tag_name() { + let s = "{{#>(X)}}{{/X}}"; + let result = Template::compile(s); + assert!(result.is_err()); + 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)); + } +} |