Adds support for the URL_ATTRIBUTE_ENTIRE parse context to HtmlTemplateParser.

Review at http://gwt-code-reviews.appspot.com/1396803

Review by: jlabanca@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9945 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java b/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java
index 316a524..54bcd6a 100644
--- a/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java
+++ b/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java
@@ -32,7 +32,7 @@
  *
  * <p>
  * This parser parses templates consisting of HTML markup, with template
- * variables of the form {@code "{n}"}. For example, a template might look like,
+ * variables of the form {@code {n}}. For example, a template might look like,
  *
  * <pre>  {@code
  *   <span style="{0}"><a href="{1}/{2}">{3}</a></span>
@@ -70,10 +70,13 @@
  * <dt>{@link HtmlContext.Type#TEXT}
  * <dd>This context corresponds to basic inner text. In the above example,
  * parameter #3 would be tagged with this context.
- * <dt>{@link HtmlContext.Type#URL_START}
+ * <dt>{@link HtmlContext.Type#URL_ATTRIBUTE_START}
  * <dd>This context corresponds to a parameter that appears at the very start of
  * a URL-valued HTML attribute's value; in the above example this applies to
  * parameter #1.
+ * <dt>{@link HtmlContext.Type#URL_ATTRIBUTE_ENTIRE}
+ * <dd>This context corresponds to a parameter that comprises an entire
+ * URL-valued attribute, for example in {@code <img src='{0}'/>}.
  * <dt>{@link HtmlContext.Type#CSS_ATTRIBUTE_START}
  * <dd>This context corresponds to a parameter that appears at the very
  * beginning of a {@code style} attribute's value; in the above example this
@@ -135,6 +138,18 @@
   private int parsePosition;
 
   /**
+   * The character preceding a template parameter, at the time a template
+   * parameter is being parsed.
+   */
+  private char lookBehind;
+
+  /**
+   * The character succeeding a template parameter, at the time a template
+   * parameter is being parsed.
+   */
+  private char lookAhead;
+
+  /**
    * Creates a {@link HtmlTemplateParser}.
    *
    * @param logger the {@link TreeLogger} to log to
@@ -163,7 +178,9 @@
   // @VisibleForTesting
   void parseTemplate(String template) throws UnableToCompleteException {
     this.template = template;
-    this.parsePosition = 0;
+    parsePosition = 0;
+    lookBehind = 0;
+    lookAhead = 0;
     Matcher match = TEMPLATE_PARAM_PATTERN.matcher(template);
 
     int endOfPreviousMatch = 0;
@@ -174,10 +191,16 @@
         parseAndAppendTemplateSegment(
             template.substring(endOfPreviousMatch, match.start()));
         parsePosition = match.start();
+        lookBehind = template.charAt(parsePosition - 1);
       }
 
       int paramIndex = Integer.parseInt(match.group(1));
       parsePosition = match.end();
+      if (parsePosition < template.length()) {
+        lookAhead = template.charAt(parsePosition);
+      } else {
+        lookAhead = 0;
+      }
       parsedTemplate.addParameter(
           new ParameterChunk(getHtmlContextFromParseState(), paramIndex));
 
@@ -255,8 +278,29 @@
                 + getTemplateParsedSoFar());
         throw new UnableToCompleteException();
       }
+      if ("meta".equals(tag) && "content".equals(attribute)) {
+        logger.log(TreeLogger.ERROR,
+            "Template variables in content attribute of meta tag are not supported: "
+                + getTemplateParsedSoFar());
+        throw new UnableToCompleteException();
+      }
       if (streamHtmlParser.isUrlStart()) {
-        return new HtmlContext(HtmlContext.Type.URL_START, tag, attribute);
+        // Note that we have established above that the attribute is quoted.
+        // Furthermore, we have ruled out template variables in the content
+        // attribute of a meta tag, which is the only case where isUrlStart()
+        // is true and the URL does not appear at the very beginning of the
+        // attribute.
+        Preconditions.checkState(lookBehind == '"' || lookBehind == '\'',
+            "At the start of a quoted attribute, lookBehind should be a quote character; at %s",
+            getTemplateParsedSoFar());
+        // If the the character immediately succeeding the template parameter is
+        // a quote that matches the one that started the attribute, we know
+        // that the parameter comprises the entire attribute.
+        if (lookAhead == lookBehind) {
+          return new HtmlContext(HtmlContext.Type.URL_ATTRIBUTE_ENTIRE, tag, attribute);
+        } else {
+          return new HtmlContext(HtmlContext.Type.URL_ATTRIBUTE_START, tag, attribute);
+        }
       } else if (streamHtmlParser.inCss()) {
         if (streamHtmlParser.getValueIndex() == 0) {
           return new HtmlContext(HtmlContext.Type.CSS_ATTRIBUTE_START, tag, attribute);
diff --git a/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java b/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java
index dd44da7..7567c16 100644
--- a/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java
+++ b/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java
@@ -56,7 +56,11 @@
       /**
        * At the very start of a URL-valued attribute.
        */
-      URL_START,
+      URL_ATTRIBUTE_START,
+      /**
+       * A template parameter that comprises an entire URL-valued attribute.
+       */
+      URL_ATTRIBUTE_ENTIRE,
       /**
        * CSS (style) context.
        */
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
index 479dac5..871ef53 100644
--- a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
+++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
@@ -144,7 +144,8 @@
       expression = "String.valueOf(" + expression + ")";
     }
 
-    if ((htmlContext.getType() == HtmlContext.Type.URL_START)) {
+    if ((htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_START) ||
+        (htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_ENTIRE)) {
       expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")";
     }
 
@@ -253,7 +254,8 @@
         emitAttributeContextParameterExpression(logger, htmlContext,
             formalParameterName, parameterType);
         break;
-      case URL_START:
+      case URL_ATTRIBUTE_START:
+      case URL_ATTRIBUTE_ENTIRE:
       case ATTRIBUTE_VALUE:
         emitAttributeContextParameterExpression(logger, htmlContext,
             formalParameterName, parameterType);
diff --git a/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java b/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java
index 8ef9f53..7e6e058 100644
--- a/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java
+++ b/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java
@@ -111,15 +111,20 @@
             + "L(</span>)]"),
         "<span>foo&amp;bar<b>{1}</b><![CDATA[foo-cdata <baz>]]>{0}</span>");
 
-    // Check correct handling of ATTRIBUTE_VALUE vs URL_START context.
-    assertParseTemplateResult(("[L(<a href=\"), P((URL_START,a,href),0), "
+    // Check correct handling of ATTRIBUTE_VALUE vs URL_ATTRIBUTE_START and
+    // URL_ATTRIBUTE_ENTIRE context.
+    assertParseTemplateResult(("[L(<a href=\"), P((URL_ATTRIBUTE_ENTIRE,a,href),0), "
         + "L(\">), P((TEXT,null,null),1), L(</a>)]"),
         "<a href=\"{0}\">{1}</a>");
+    // Single quotes work too:
+    assertParseTemplateResult(("[L(<a href='), P((URL_ATTRIBUTE_ENTIRE,a,href),0), "
+        + "L('>), P((TEXT,null,null),1), L(</a>)]"),
+        "<a href='{0}'>{1}</a>");
     assertParseTemplateResult(
         ("[L(<a href=\"http://), P((ATTRIBUTE_VALUE,a,href),0), "
             + "L(\">), P((TEXT,null,null),1), L(</a>)]"),
         "<a href=\"http://{0}\">{1}</a>");
-    assertParseTemplateResult(("[L(<a href=\"), P((URL_START,a,href),0), "
+    assertParseTemplateResult(("[L(<a href=\"), P((URL_ATTRIBUTE_START,a,href),0), "
         + "L(/), P((ATTRIBUTE_VALUE,a,href),1), "
         + "L(\">), P((TEXT,null,null),2), L(</a>)]"),
         "<a href=\"{0}/{1}\">{2}</a>");
@@ -173,6 +178,12 @@
         "<div class=\"{0}\" foo=bar>{1}<a");
     assertParsingTemplateEndingInNonInnerHtmlContextFails(
         "<div class=\"{0}\" foo=bar>{1}<a href=");
+
+    // Check that parseTemplate doesn't walk off the end of the string when
+    // extracting lookAhead: We should be getting an error that the template
+    // ends in non-inner-HTML context, and not an IndexOutOfBoundsException.
+    assertParsingTemplateEndingInNonInnerHtmlContextFails("<a href='{0}'");
+    assertParsingTemplateEndingInNonInnerHtmlContextFails("<a href='{0}");
   }
 
   private void assertTemplateVariableInUnquotedAttributeFails(
@@ -247,6 +258,11 @@
         "<div style=\"{0}\" foo{1}=\"{2}\">", "<div style=\"{0}\" foo{1}");
   }
 
+  public void testParseTemplate_templateVariableInMetaContentFails() {
+    assertParseFails("Template variables in content attribute of meta tag are not supported: ",
+        "<meta http-equiv=\"{0}\" content=\"{1}\">", "<meta http-equiv=\"{0}\" content=\"{1}");
+  }
+
   private void assertParseFails(
       String expectedError, final String template, final String failAtPrefix) {
     UnitTestTreeLogger.Builder loggerBuilder = new UnitTestTreeLogger.Builder();
diff --git a/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java b/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java
index d5d1334..dd8d4ce 100644
--- a/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java
+++ b/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java
@@ -49,7 +49,7 @@
         HtmlContext.Type.TEXT), 0));
 
     List<TemplateChunk> chunks = parsed.getChunks();
-    
+
     ParameterChunk chunk = (ParameterChunk) chunks.get(0);
     assertEquals(TemplateChunk.Kind.PARAMETER, chunk.getKind());
     assertEquals(HtmlContext.Type.TEXT, chunk.getContext().getType());
@@ -108,8 +108,8 @@
   /**
    * Tests that calling addParameter(), addLiteral(), addLiteral(),
    * addParameter() in sequence results in the expected ParsedHtmlTemplate.
-   * 
-   * <p>In particular, two calls to addLiteral() in sequence should result in 
+   *
+   * <p>In particular, two calls to addLiteral() in sequence should result in
    * only a single LiteralChunk.
    */
   public void testAddParameterAddLiteralSequence() {
@@ -120,7 +120,7 @@
     parsed.addLiteral("<a");
     parsed.addLiteral(" href=\"");
     parsed.addParameter(new ParameterChunk(new HtmlContext(
-        HtmlContext.Type.URL_START, "a", "href"), 1));
+        HtmlContext.Type.URL_ATTRIBUTE_START, "a", "href"), 1));
 
     List<TemplateChunk> chunks = parsed.getChunks();
     assertEquals(3, chunks.size());
@@ -145,15 +145,15 @@
     paramChunk = (ParameterChunk) it.next();
     assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind());
     assertEquals(
-        HtmlContext.Type.URL_START, paramChunk.getContext().getType());
+        HtmlContext.Type.URL_ATTRIBUTE_START, paramChunk.getContext().getType());
     assertEquals("a", paramChunk.getContext().getTag());
     assertEquals("href", paramChunk.getContext().getAttribute());
     assertEquals(1, paramChunk.getParameterIndex());
-    assertEquals("P((URL_START,a,href),1)", paramChunk.toString());
+    assertEquals("P((URL_ATTRIBUTE_START,a,href),1)", paramChunk.toString());
 
     assertEquals(
         "[P((TEXT,null,null),0), L(<a href=\"), "
-            + "P((URL_START,a,href),1)]",
+            + "P((URL_ATTRIBUTE_START,a,href),1)]",
         parsed.toString());
   }
 }