| use std::collections::{HashMap, VecDeque}; |
| 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::{HandlebarsParser, Rule}; |
| use crate::json::path::{parse_json_path_from_iter, Path}; |
| use crate::support; |
| |
| use self::TemplateElement::*; |
| |
| #[derive(PartialEq, Eq, Clone, Debug)] |
| pub struct TemplateMapping(pub usize, pub usize); |
| |
| /// A handlebars template |
| #[derive(PartialEq, Eq, Clone, Debug, Default)] |
| pub struct Template { |
| pub name: Option<String>, |
| pub elements: Vec<TemplateElement>, |
| pub mapping: Vec<TemplateMapping>, |
| } |
| |
| #[derive(Default)] |
| pub(crate) struct TemplateOptions { |
| pub(crate) prevent_indent: bool, |
| pub(crate) name: Option<String>, |
| } |
| |
| impl TemplateOptions { |
| fn name(&self) -> String { |
| self.name |
| .as_ref() |
| .cloned() |
| .unwrap_or_else(|| "Unnamed".to_owned()) |
| } |
| } |
| |
| #[derive(PartialEq, Eq, 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, |
| chain: 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, Eq, Clone, Debug)] |
| pub enum BlockParam { |
| Single(Parameter), |
| Pair((Parameter, Parameter)), |
| } |
| |
| #[derive(PartialEq, Eq, 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, Eq, 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, Eq, 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, |
| pub chain: bool, |
| } |
| |
| impl HelperTemplate { |
| pub fn new(exp: ExpressionSpec, block: bool) -> HelperTemplate { |
| HelperTemplate { |
| name: exp.name, |
| params: exp.params, |
| hash: exp.hash, |
| block_param: exp.block_param, |
| block, |
| template: None, |
| inverse: None, |
| chain: false, |
| } |
| } |
| |
| pub fn new_chain(exp: ExpressionSpec, block: bool) -> HelperTemplate { |
| HelperTemplate { |
| name: exp.name, |
| params: exp.params, |
| hash: exp.hash, |
| block_param: exp.block_param, |
| block, |
| template: None, |
| inverse: None, |
| chain: true, |
| } |
| } |
| |
| // 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, |
| chain: false, |
| } |
| } |
| |
| pub(crate) fn is_name_only(&self) -> bool { |
| !self.block && self.params.is_empty() && self.hash.is_empty() |
| } |
| |
| fn insert_inverse_node(&mut self, mut node: Box<HelperTemplate>) { |
| // Create a list in "inverse" member to hold the else-chain. |
| // Here we create the new template to save the else-chain node. |
| // The template render could render it successfully without any code add. |
| let mut new_chain_template = Template::new(); |
| node.inverse = self.inverse.take(); |
| new_chain_template.elements.push(HelperBlock(node)); |
| self.inverse = Some(new_chain_template); |
| } |
| |
| fn ref_chain_head_mut(&mut self) -> Option<&mut Box<HelperTemplate>> { |
| if self.chain { |
| if let Some(inverse_tmpl) = &mut self.inverse { |
| assert_eq!(inverse_tmpl.elements.len(), 1); |
| if let HelperBlock(helper) = &mut inverse_tmpl.elements[0] { |
| return Some(helper); |
| } |
| } |
| } |
| None |
| } |
| |
| fn set_chain_template(&mut self, tmpl: Option<Template>) { |
| if let Some(hepler) = self.ref_chain_head_mut() { |
| hepler.template = tmpl; |
| } else { |
| self.template = tmpl; |
| } |
| } |
| |
| fn revert_chain_and_set(&mut self, inverse: Option<Template>) { |
| if self.chain { |
| let mut prev = None; |
| |
| if let Some(head) = self.ref_chain_head_mut() { |
| if head.template.is_some() { |
| // Here the prev will hold the head inverse template. |
| // It will be set when reverse the chain. |
| prev = inverse; |
| } else { |
| // If the head already has template. set the inverse template. |
| head.template = inverse; |
| } |
| } |
| |
| // Reverse the else chain, to the normal list order. |
| while let Some(mut node) = self.inverse.take() { |
| assert_eq!(node.elements.len(), 1); |
| if let HelperBlock(c) = &mut node.elements[0] { |
| self.inverse = c.inverse.take(); |
| c.inverse = prev; |
| prev = Some(node); |
| } |
| } |
| |
| self.inverse = prev; |
| } else { |
| // If the helper has no else chain. |
| // set the template to self. |
| if self.template.is_some() { |
| self.inverse = inverse; |
| } else { |
| self.template = inverse; |
| } |
| } |
| } |
| |
| fn set_chained(&mut self) { |
| self.chain = true; |
| } |
| |
| pub fn is_chained(&self) -> bool { |
| self.chain |
| } |
| } |
| |
| #[derive(PartialEq, Eq, Clone, Debug)] |
| pub struct DecoratorTemplate { |
| pub name: Parameter, |
| pub params: Vec<Parameter>, |
| pub hash: HashMap<String, Parameter>, |
| pub template: Option<Template>, |
| // for partial indent |
| pub indent: Option<String>, |
| } |
| |
| impl DecoratorTemplate { |
| pub fn new(exp: ExpressionSpec) -> DecoratorTemplate { |
| DecoratorTemplate { |
| name: exp.name, |
| params: exp.params, |
| hash: exp.hash, |
| template: None, |
| indent: None, |
| } |
| } |
| } |
| |
| 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::helper_parameter { |
| 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 => { |
| // Parse the parameter as a JSON literal |
| let param_literal = it.next().unwrap(); |
| let json_result = match param_literal.as_rule() { |
| Rule::string_literal |
| if it.peek().unwrap().as_rule() == Rule::string_inner_single_quote => |
| { |
| // ...unless the parameter is a single-quoted string. |
| // In that case, transform it to a double-quoted string |
| // and then parse it as a JSON literal. |
| let string_inner_single_quote = it.next().unwrap(); |
| let double_quoted = format!( |
| "\"{}\"", |
| string_inner_single_quote |
| .as_str() |
| .replace("\\'", "'") |
| .replace('"', "\\\"") |
| ); |
| Json::from_str(&double_quoted) |
| } |
| _ => Json::from_str(param_span.as_str()), |
| }; |
| if let Ok(json) = json_result { |
| Parameter::Literal(json) |
| } else { |
| return Err(TemplateError::of(TemplateErrorReason::InvalidParam( |
| param_span.as_str().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::leading_tilde_to_omit_whitespace { |
| 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::helper_parameter => { |
| 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::trailing_tilde_to_omit_whitespace => { |
| 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(RawString(ref mut text)) = t.elements.last_mut() { |
| text.trim_end().to_owned().clone_into(text); |
| } |
| } |
| |
| // in handlebars, the whitespaces around statement are |
| // automatically trimed. |
| // this function checks if current span has both leading and |
| // trailing whitespaces, which we treat as a standalone statement. |
| // |
| // |
| fn process_standalone_statement( |
| template_stack: &mut VecDeque<Template>, |
| source: &str, |
| current_span: &Span<'_>, |
| prevent_indent: bool, |
| ) -> bool { |
| let with_trailing_newline = |
| support::str::starts_with_empty_line(&source[current_span.end()..]); |
| |
| if with_trailing_newline { |
| let with_leading_newline = |
| support::str::ends_with_empty_line(&source[..current_span.start()]); |
| |
| // prevent_indent: a special toggle for partial expression |
| // (>) that leading whitespaces are kept |
| if prevent_indent && with_leading_newline { |
| let t = template_stack.front_mut().unwrap(); |
| // check the last element before current |
| if let Some(RawString(ref mut text)) = t.elements.last_mut() { |
| // trim leading space for standalone statement |
| text.trim_end_matches(support::str::whitespace_matcher) |
| .to_owned() |
| .clone_into(text); |
| } |
| } |
| |
| // 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 { |
| let s = s.trim_start_matches(support::str::whitespace_matcher); |
| RawString(support::str::strip_first_newline(s).to_owned()) |
| } else { |
| RawString(s) |
| } |
| } |
| |
| pub(crate) fn compile2( |
| source: &str, |
| options: TemplateOptions, |
| ) -> 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_required = 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( |
| e.variant.message().to_string(), |
| )) |
| .at(source, line_no, col_no) |
| .in_template(options.name()) |
| })?; |
| |
| // 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_required, |
| ), |
| 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_required, |
| ), |
| line_no, |
| col_no, |
| ); |
| } |
| |
| // reset standalone statement marker |
| trim_line_required = false; |
| } |
| |
| 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_required, |
| ), |
| line_no, |
| col_no, |
| ); |
| |
| // reset standalone statement marker |
| trim_line_required = 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::new(exp.clone(), true); |
| helper_stack.push_front(helper_template); |
| } |
| Rule::decorator_block_start | Rule::partial_block_start => { |
| let decorator = DecoratorTemplate::new(exp.clone()); |
| 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_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| true, |
| ); |
| |
| let t = template_stack.front_mut().unwrap(); |
| t.mapping.push(TemplateMapping(line_no, col_no)); |
| } |
| Rule::invert_tag | Rule::invert_chain_tag => { |
| // hack: invert_tag structure is similar to ExpressionSpec, so I |
| // use it here to represent the data |
| |
| if rule == Rule::invert_chain_tag { |
| let _ = Template::parse_name(source, &mut it, span.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; |
| |
| // standalone statement check, it also removes leading whitespaces of |
| // previous rawstring when standalone statement detected |
| trim_line_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| true, |
| ); |
| |
| let t = template_stack.pop_front().unwrap(); |
| let h = helper_stack.front_mut().unwrap(); |
| |
| if rule == Rule::invert_chain_tag { |
| h.set_chained(); |
| } |
| |
| h.set_chain_template(Some(t)); |
| if rule == Rule::invert_chain_tag { |
| h.insert_inverse_node(Box::new(HelperTemplate::new_chain(exp, true))); |
| } |
| } |
| |
| 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_required, |
| ), |
| 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::new(exp.clone(), false); |
| 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 => { |
| // do not auto trim ident spaces for |
| // partial_expression(>) |
| let prevent_indent = rule != Rule::partial_expression; |
| trim_line_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| prevent_indent, |
| ); |
| |
| // indent for partial expression > |
| let mut indent = None; |
| if rule == Rule::partial_expression |
| && !options.prevent_indent |
| && !exp.omit_pre_ws |
| { |
| indent = support::str::find_trailing_whitespace_chars( |
| &source[..span.start()], |
| ); |
| } |
| |
| let mut decorator = DecoratorTemplate::new(exp.clone()); |
| decorator.indent = indent.map(|s| s.to_owned()); |
| |
| 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_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| true, |
| ); |
| |
| 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(); |
| h.revert_chain_and_set(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) |
| .in_template(options.name())); |
| } |
| } |
| 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_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| true, |
| ); |
| |
| 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) |
| .in_template(options.name())); |
| } |
| } |
| _ => unreachable!(), |
| } |
| } |
| Rule::hbs_comment_compact => { |
| trim_line_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| true, |
| ); |
| |
| 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_required = Template::process_standalone_statement( |
| &mut template_stack, |
| source, |
| &span, |
| true, |
| ); |
| |
| 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 mut root_template = template_stack.pop_front().unwrap(); |
| root_template.name = options.name; |
| return Ok(root_template); |
| } |
| } |
| } |
| |
| // These two compile functions are kept for compatibility with 4.x |
| // Template APIs in case that some developers are using them |
| // without registry. |
| |
| pub fn compile(source: &str) -> Result<Template, TemplateError> { |
| Self::compile2(source, TemplateOptions::default()) |
| } |
| |
| pub fn compile_with_name<S: AsRef<str>>( |
| source: S, |
| name: String, |
| ) -> Result<Template, TemplateError> { |
| Self::compile2( |
| source.as_ref(), |
| TemplateOptions { |
| name: Some(name), |
| ..Default::default() |
| }, |
| ) |
| } |
| } |
| |
| #[derive(PartialEq, Eq, 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.pos(), Some((4, 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()); |
| let err = result.expect_err("expected a syntax error"); |
| let syntax_error_msg = match err.reason() { |
| TemplateErrorReason::InvalidSyntax(s) => s, |
| _ => panic!("InvalidSyntax expected"), |
| }; |
| assert!( |
| syntax_error_msg.contains("expected identifier"), |
| "{}", |
| syntax_error_msg |
| ); |
| } |
| } |
| |
| #[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, chain: false }) })\" was opened, but \"X\" is closing", format!("{}", result.unwrap_err().reason())); |
| } |
| } |