Repository: sunng87/handlebars-rust (rust)

Issue:
Incorrectly parses \\ in templates.
The template:

    {{FOO}} {{BAR}} {{FOO}}{{BAR}} {{FOO}}#{{BAR}} {{FOO}}//{{BAR}} {{FOO}}\\{{FOO}} {{FOO}}\\\\{{FOO}}\\\\{{FOO}}

Renders:

    foo bar foobar foo#bar foo//bar foo\\{{FOO}} foo{{FOO}}{{FOO}}

I have been looking through the handlebars spec regarding `\`, but I can't see anything specific about this in it; it seems like a bug.

Note that other various character variations, eg. "{{FOO}}xxxx{{BAR}}" are fine, it's just for `\`.

This tangibly manifests itself as a bug when rendering windows paths in templates, eg:

    {{BUILD_PATH}}\{{PROJECT_FOLDER_XXX}}
    Invalid template, ""while parsing a quoted scalar, found unknown escape character"

but: 

    {{BUILD_PATH}}\\{{PROJECT_FOLDER_XXX}}
    renders: C:\projects\builds\source\artifacts\{{PROJECT_FOLDER_XXX}}

^ --- This is very surprising, and I'm pretty sure, not the correct behavior.

Pull request:
Fix mustach escape
Fixes #216 

There are issues with current backslash escape implementation. This patch changed both parser and template processing to ensure backslash was correctly treated. 

Patch:
diff --git a/src/grammar.pest b/src/grammar.pest
index 0b3aaf0..eaaa451 100644
--- a/src/grammar.pest
+++ b/src/grammar.pest
@@ -1,8 +1,9 @@
 whitespace = _{ " "|"\t"|"\n"|"\r" }
 keywords = @{ "as" | "else" }
 
-raw_text = @{ ( "\\{{{{"? ~ "\\{{"? ~ !"{{" ~ any )+ }
-raw_block_text = @{ ( !"{{{{" ~ any )* }
+escape = @{ ("\\" ~ "{{" ~ "{{"?) | ("\\" ~ "\\"+ ~ &"{{") }
+raw_text = ${ ( escape | (!"{{" ~ any) )+ }
+raw_block_text = ${ ( escape | (!"{{{{" ~ any) )* }
 
 literal = { string_literal |
             array_literal |
@@ -37,52 +38,51 @@ subexpression = { "(" ~ name ~ (hash|param)* ~ ")" }
 
 pre_whitespace_omitter = { "~" }
 pro_whitespace_omitter = { "~" }
-escape = { "\\" }
 
-expression = { !escape ~ !invert_tag ~ "{{" ~ pre_whitespace_omitter? ~ name ~
+expression = { !invert_tag ~ "{{" ~ pre_whitespace_omitter? ~ name ~
 pro_whitespace_omitter? ~ "}}" }
 
-html_expression = { !escape ~ "{{{" ~ pre_whitespace_omitter? ~ name ~
+html_expression = { "{{{" ~ pre_whitespace_omitter? ~ name ~
 pro_whitespace_omitter? ~ "}}}" }
 
 helper_expression = { !invert_tag ~ "{{" ~ pre_whitespace_omitter? ~ exp_line ~
 pro_whitespace_omitter? ~ "}}" }
 
-directive_expression = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "*" ~ exp_line ~
+directive_expression = { "{{" ~ pre_whitespace_omitter? ~ "*" ~ exp_line ~
 pro_whitespace_omitter? ~ "}}" }
-partial_expression = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ ">" ~ partial_exp_line
+partial_expression = { "{{" ~ pre_whitespace_omitter? ~ ">" ~ partial_exp_line
                      ~ pro_whitespace_omitter? ~ "}}" }
 invert_tag_item = { "else"|"^" }
 invert_tag = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ invert_tag_item
              ~ pro_whitespace_omitter? ~ "}}"}
-helper_block_start = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "#" ~ exp_line ~
+helper_block_start = { "{{" ~ pre_whitespace_omitter? ~ "#" ~ exp_line ~
                      pro_whitespace_omitter? ~ "}}" }
-helper_block_end = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
+helper_block_end = { "{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
                    pro_whitespace_omitter? ~ "}}" }
 helper_block = _{ helper_block_start ~ template ~
                   (invert_tag ~ template)? ~ helper_block_end }
 
-directive_block_start = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "#" ~ "*"
+directive_block_start = { "{{" ~ pre_whitespace_omitter? ~ "#" ~ "*"
                         ~ exp_line ~ pro_whitespace_omitter? ~ "}}" }
-directive_block_end = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
+directive_block_end = { "{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
                         pro_whitespace_omitter? ~ "}}" }
 directive_block = _{ directive_block_start ~ template ~
                      directive_block_end }
 
-partial_block_start = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "#" ~ ">"
+partial_block_start = { "{{" ~ pre_whitespace_omitter? ~ "#" ~ ">"
                         ~ partial_exp_line ~ pro_whitespace_omitter? ~ "}}" }
-partial_block_end = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
+partial_block_end = { "{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
                       pro_whitespace_omitter? ~ "}}" }
 partial_block = _{ partial_block_start ~ template ~ partial_block_end }
 
-raw_block_start = { !escape ~ "{{{{" ~ pre_whitespace_omitter? ~ exp_line ~
+raw_block_start = { "{{{{" ~ pre_whitespace_omitter? ~ exp_line ~
                     pro_whitespace_omitter? ~ "}}}}" }
-raw_block_end = { !escape ~ "{{{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
+raw_block_end = { "{{{{" ~ pre_whitespace_omitter? ~ "/" ~ name ~
                   pro_whitespace_omitter? ~ "}}}}" }
 raw_block = _{ raw_block_start ~ raw_block_text ~ raw_block_end }
 
-hbs_comment = { !escape ~ "{{!--" ~ (!"--}}" ~ any)* ~ "--}}" }
-hbs_comment_compact = { !escape ~ "{{!" ~ (!"}}" ~ any)* ~ "}}" }
+hbs_comment = { "{{!--" ~ (!"--}}" ~ any)* ~ "--}}" }
+hbs_comment_compact = { "{{!" ~ (!"}}" ~ any)* ~ "}}" }
 
 template = { (
             raw_text |
diff --git a/src/grammar.rs b/src/grammar.rs
index d5c4645..e1b992e 100644
--- a/src/grammar.rs
+++ b/src/grammar.rs
@@ -1,4 +1,4 @@
-//const _GRAMMAR: &'static str = include_str!("grammar.pest");
+// const _GRAMMAR: &'static str = include_str!("grammar.pest");
 
 #[derive(Parser)]
 #[grammar = "grammar.pest"]
@@ -22,6 +22,21 @@ macro_rules! assert_rule {
     };
 }
 
+#[cfg(test)]
+macro_rules! assert_not_rule {
+    ($rule:expr, $in:expr) => {
+        assert!(
+            HandlebarsParser::parse($rule, $in).is_err()
+                || HandlebarsParser::parse($rule, $in)
+                    .unwrap()
+                    .last()
+                    .unwrap()
+                    .into_span()
+                    .end() != $in.len()
+        );
+    };
+}
+
 #[cfg(test)]
 macro_rules! assert_rule_match {
     ($rule:expr, $in:expr) => {
@@ -33,13 +48,18 @@ macro_rules! assert_rule_match {
 fn test_raw_text() {
     let s = vec![
         "<h1> helloworld </h1>    ",
-        "hello\\{{world}}",
-        "hello\\{{#if world}}nice\\{{/if}}",
-        "hello \\{{{{raw}}}}hello\\{{{{/raw}}}}",
+        r"hello\{{world}}",
+        r"hello\{{#if world}}nice\{{/if}}",
+        r"hello \{{{{raw}}}}hello\{{{{/raw}}}}",
     ];
     for i in s.iter() {
         assert_rule!(Rule::raw_text, i);
     }
+
+    let s_not_escape = vec![r"\\{{hello}}"];
+    for i in s_not_escape.iter() {
+        assert_not_rule!(Rule::raw_text, i);
+    }
 }
 
 #[test]
diff --git a/src/registry.rs b/src/registry.rs
index ebfaca6..f90fef3 100644
--- a/src/registry.rs
+++ b/src/registry.rs
@@ -483,6 +483,16 @@ mod test {
 
         assert_eq!(
             "{{hello}}",
+            r.render_template(r"\{{hello}}", &data).unwrap()
+        );
+
+        assert_eq!(
+            " {{hello}}",
+            r.render_template(r" \{{hello}}", &data).unwrap()
+        );
+
+        assert_eq!(
+            r"\world",
             r.render_template(r"\\{{hello}}", &data).unwrap()
         );
     }
diff --git a/src/template.rs b/src/template.rs
index 6082034..064451c 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -3,7 +3,7 @@ use std::convert::From;
 use std::iter::Peekable;
 
 use grammar::{HandlebarsParser, Rule};
-use pest::iterators::FlatPairs;
+use pest::iterators::Pair;
 use pest::Error as PestError;
 use pest::{Parser, Position};
 
@@ -129,10 +129,6 @@ impl Template {
         }
     }
 
-    fn unescape_tags(txt: &str) -> String {
-        txt.replace(r"\\{{", "{{")
-    }
-
     fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) {
         self.elements.push(e);
         if let Some(ref mut maps) = self.mapping {
@@ -147,7 +143,7 @@ impl Template {
     #[inline]
     fn parse_subexpression<'a>(
         source: &'a str,
-        it: &mut Peekable<FlatPairs<Rule>>,
+        it: &mut Peekable<impl Iterator<Item = Pair<'a, Rule>>>,
         limit: usize,
     ) -> Result<Parameter, TemplateError> {
         let espec = try!(Template::parse_expression(source, it.by_ref(), limit));
@@ -166,7 +162,7 @@ impl Template {
     #[inline]
     fn parse_name<'a>(
         source: &'a str,
-        it: &mut Peekable<FlatPairs<Rule>>,
+        it: &mut Peekable<impl Iterator<Item = Pair<'a, Rule>>>,
         _: usize,
     ) -> Result<Parameter, TemplateError> {
         let name_node = it.next().unwrap();
@@ -186,7 +182,7 @@ impl Template {
     #[inline]
     fn parse_param<'a>(
         source: &'a str,
-        it: &mut Peekable<FlatPairs<Rule>>,
+        it: &mut Peekable<impl Iterator<Item = Pair<'a, Rule>>>,
         _: usize,
     ) -> Result<Parameter, TemplateError> {
         let mut param = it.next().unwrap();
@@ -232,7 +228,7 @@ impl Template {
     #[inline]
     fn parse_hash<'a>(
         source: &'a str,
-        it: &mut Peekable<FlatPairs<Rule>>,
+        it: &mut Peekable<impl Iterator<Item = Pair<'a, Rule>>>,
         limit: usize,
     ) -> Result<(String, Parameter), TemplateError> {
         let name = it.next().unwrap();
@@ -247,7 +243,7 @@ impl Template {
     #[inline]
     fn parse_block_param<'a>(
         _: &'a str,
-        it: &mut Peekable<FlatPairs<Rule>>,
+        it: &mut Peekable<impl Iterator<Item = Pair<'a, Rule>>>,
         limit: usize,
     ) -> Result<BlockParam, TemplateError> {
         let p1_name = it.next().unwrap();
@@ -278,7 +274,7 @@ impl Template {
     #[inline]
     fn parse_expression<'a>(
         source: &'a str,
-        it: &mut Peekable<FlatPairs<Rule>>,
+        it: &mut Peekable<impl Iterator<Item = Pair<'a, Rule>>>,
         limit: usize,
     ) -> Result<ExpressionSpec, TemplateError> {
         let mut params: Vec<Parameter> = Vec::new();
@@ -350,6 +346,45 @@ impl Template {
         }
     }
 
+    fn raw_string<'a>(
+        source: &'a str,
+        pair: Option<Pair<'a, Rule>>,
+        trim_left: 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.clone().into_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.
+            let pairs: Vec<Pair<'a, Rule>> = pair.into_inner().collect();
+            for sub_pair in pairs.into_iter().rev() {
+                // remove escaped backslash
+                if sub_pair.as_rule() == Rule::escape {
+                    let escape_span = sub_pair.into_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_left {
+            RawString(s.trim_left().to_owned())
+        } else {
+            RawString(s)
+        }
+    }
+
     pub fn compile2<S: AsRef<str>>(source: S, mapping: bool) -> Result<Template, TemplateError> {
         let source = source.as_ref();
         let mut helper_stack: VecDeque<HelperTemplate> = VecDeque::new();
@@ -381,13 +416,19 @@ impl Template {
                 }
             })?;
 
-        let mut it = parser_queue.flatten().peekable();
+        // println!("{:?}", parser_queue.clone());
+
+        // remove escape from our pair queue
+        let mut it = parser_queue
+            .flatten()
+            .filter(|p| 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.into_span();
+                let span = pair.clone().into_span();
 
                 if rule != Rule::template {
                     // trailing string check
@@ -396,19 +437,17 @@ impl Template {
                     {
                         let (line_no, col_no) = span.start_pos().line_col();
                         if rule == Rule::raw_block_end {
-                            let text = &source[prev_end..span.start()];
                             let mut t = Template::new(mapping);
                             t.push_element(
-                                RawString(Template::unescape_tags(text)),
+                                Template::raw_string(&source[prev_end..span.start()], None, false),
                                 line_no,
                                 col_no,
                             );
                             template_stack.push_front(t);
                         } else {
-                            let text = &source[prev_end..span.start()];
                             let t = template_stack.front_mut().unwrap();
                             t.push_element(
-                                RawString(Template::unescape_tags(text)),
+                                Template::raw_string(&source[prev_end..span.start()], None, false),
                                 line_no,
                                 col_no,
                             );
@@ -428,12 +467,17 @@ impl Template {
                         } else {
                             span.start()
                         };
-                        let mut text = &source[start..span.end()];
-                        if omit_pro_ws {
-                            text = text.trim_left();
-                        }
+
                         let t = template_stack.front_mut().unwrap();
-                        t.push_element(RawString(Template::unescape_tags(text)), line_no, col_no);
+                        t.push_element(
+                            Template::raw_string(
+                                &source[start..span.end()],
+                                Some(pair.clone()),
+                                omit_pro_ws,
+                            ),
+                            line_no,
+                            col_no,
+                        );
                     }
                     Rule::helper_block_start
                     | Rule::raw_block_start
@@ -491,12 +535,12 @@ impl Template {
                         h.template = Some(t);
                     }
                     Rule::raw_block_text => {
-                        let mut text = span.as_str();
-                        if omit_pro_ws {
-                            text = text.trim_left();
-                        }
                         let mut t = Template::new(mapping);
-                        t.push_element(RawString(Template::unescape_tags(text)), line_no, col_no);
+                        t.push_element(
+                            Template::raw_string(span.as_str(), Some(pair.clone()), omit_pro_ws),
+                            line_no,
+                            col_no,
+                        );
                         template_stack.push_front(t);
                     }
                     Rule::expression
@@ -665,7 +709,7 @@ pub enum TemplateElement {
 
 #[test]
 fn test_parse_escaped_tag_raw_string() {
-    let source = r"foo \\{{bar}}";
+    let source = r"foo \{{bar}}";
     let t = Template::compile(source.to_string()).ok().unwrap();
     assert_eq!(t.elements.len(), 1);
     assert_eq!(
@@ -674,9 +718,17 @@ fn test_parse_escaped_tag_raw_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 source = r"\{{{{foo}}}} bar";
     let t = Template::compile(source.to_string()).ok().unwrap();
     assert_eq!(t.elements.len(), 1);
     assert_eq!(


Test patch:
diff --git a/tests/escape.rs b/tests/escape.rs
new file mode 100644
index 0000000..360a74f
--- /dev/null
+++ b/tests/escape.rs
@@ -0,0 +1,21 @@
+extern crate handlebars;
+
+#[macro_use]
+extern crate serde_json;
+
+use handlebars::Handlebars;
+
+#[test]
+fn test_escape_216() {
+    let hbs = Handlebars::new();
+
+    let data = json!({
+        "FOO": "foo",
+        "BAR": "bar"
+    });
+
+    assert_eq!(
+        hbs.render_template(r"\\\\ {{FOO}} {{BAR}} {{FOO}}{{BAR}} {{FOO}}#{{BAR}} {{FOO}}//{{BAR}} {{FOO}}\\{{FOO}} {{FOO}}\\\\{{FOO}}\\\{{FOO}} \\\{{FOO}} \{{FOO}} \{{FOO}}", &data).unwrap(),
+        r"\\\\ foo bar foobar foo#bar foo//bar foo\foo foo\\\foo\\foo \\foo {{FOO}} {{FOO}}"
+    );
+}