We were double escaping the contents of placeholders inside HTML
messages. E.g.,

  <p><ui:msg>I would 'like' a <span ui:field='foo'>'single'</span> quote</ui

rendered as:

  I would 'like' a ''single'' quote

There was already a mechanism in place for this kind of thing, written for t
widgets-in-HTML-messages case (WidgetPlaceholderInterpreter), but I forgot
to use it in HtmlPlaceholderInterpreter.

Added tests and touched up docs while there, hoping to regain my
bearings more quickly next time.


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6100 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java b/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java
index 48b4003..759e009 100644
--- a/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java
@@ -32,14 +32,14 @@
       final UiBinderWriter writer) throws UnableToCompleteException {
     String customTag =
       UiBinderWriter.escapeTextForJavaStringLiteral(elem.consumeAttribute("tag"));
-    
+
     /*
      * Gathers up elements that indicate nested widgets (but only those
      * that are not inside msg elements).
      */
     WidgetInterpreter widgetInterpreter = new WidgetInterpreter(fieldName, writer);
 
-    /* 
+    /*
      * Handles non-widget elements like msg, and dom elements with ui:field
      * attributes. There may be widgets inside a msg, which is why
      * the construction in makeHtmlInterpreter is so complicated.
@@ -65,7 +65,7 @@
 
   /**
    * Creates an HtmlInterpreter with our specialized placeholder interpreter,
-   * which will allow widget instances to be declared inside of m:msg elements.
+   * which will allow widget instances to be declared inside of ui:msg elements.
    */
   private HtmlInterpreter makeHtmlInterpreter(final String fieldName,
       final UiBinderWriter uiWriter) {
diff --git a/user/src/com/google/gwt/uibinder/parsers/HtmlMessageInterpreter.java b/user/src/com/google/gwt/uibinder/parsers/HtmlMessageInterpreter.java
index 4c9d48d..b81fca5 100644
--- a/user/src/com/google/gwt/uibinder/parsers/HtmlMessageInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/parsers/HtmlMessageInterpreter.java
@@ -23,7 +23,7 @@
 import com.google.gwt.uibinder.rebind.messages.PlaceholderInterpreter;
 
 /**
- * Processes <m:msg> elements inside HTML values, which themselves
+ * Processes <ui:msg> elements inside HTML values, which themselves
  * are allowed to contain HTML. That HTML may hold elements with
  * ui:field attributes and computed attributes, which must be
  * replaced by placeholders in the generated message.
@@ -52,7 +52,7 @@
 
   /**
    * Build a normally configured HtmlMessageInterpreter, able to handle
-   * put placeholders around dom elements with m:ph attributes and computed
+   * put placeholders around dom elements with ui:ph attributes and computed
    * attributes.
    */
   public HtmlMessageInterpreter(final UiBinderWriter uiWriter,
diff --git a/user/src/com/google/gwt/uibinder/parsers/HtmlPlaceholderInterpreter.java b/user/src/com/google/gwt/uibinder/parsers/HtmlPlaceholderInterpreter.java
index 81d2f15..9e08199 100644
--- a/user/src/com/google/gwt/uibinder/parsers/HtmlPlaceholderInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/parsers/HtmlPlaceholderInterpreter.java
@@ -60,7 +60,11 @@
           nextPlaceholder(name + "Begin", stripTokens(openTag),
               uiWriter.detokenate(openTag));
 
-      String body = elem.consumeInnerHtml(this);
+      /*
+       * This recursive innerHtml call has already been escaped. Hide it in a
+       * token to avoid double escaping
+       */
+      String body = tokenator.nextToken(elem.consumeInnerHtml(this));
 
       String closeTag = elem.getClosingTag();
       String closePlaceholder =
@@ -79,7 +83,13 @@
   }
 
   /**
-   * @return true if it has an m:ph attribute, or has a token in any attribute
+   * An element will require a placeholder if the user has called it out with a
+   * ui:ph attribute, or if it will require run time swizzling (e.g. has a
+   * ui:field). These latter we can identify easily because they'll have an
+   * attribute that holds a tokenator token that was vended by
+   * {@link UiBinderWriter}, typically in id.
+   *
+   * @return true if it has an ui:ph attribute, or has a token in any attribute
    */
   private boolean isDomPlaceholder(XMLElement elem) {
     MessagesWriter mw = uiWriter.getMessages();
diff --git a/user/src/com/google/gwt/uibinder/rebind/messages/PlaceholderInterpreter.java b/user/src/com/google/gwt/uibinder/rebind/messages/PlaceholderInterpreter.java
index 69ea07b..5578f74 100644
--- a/user/src/com/google/gwt/uibinder/rebind/messages/PlaceholderInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/messages/PlaceholderInterpreter.java
@@ -40,15 +40,14 @@
 
   public String interpretElement(XMLElement elem)
       throws UnableToCompleteException {
-
     if (isPlaceholderElement(elem)) {
       /*
-       * The innerHTML or innerText of the <m:ph> will be provided as the value
+       * The innerHTML or innerText of the <ui:ph> will be provided as the value
        * of the appropriate parameter when the Messages method is called.
        *
        * E.g.
        *
-       *   <m:msg>Able <m:ph name="foo" example"baz">baker</m:ph> charlie</m:msg>
+       *   <ui:msg>Able <ui:ph name="foo" example"baz">baker</ui:ph> charlie</ui:msg>
        *
        * becomes
        *
@@ -78,11 +77,10 @@
       }
 
       /*
-       * Again, if there are TemplateWriter tokens in the value string, we need
-       * it to have it replace them with the real expresions.
+       * Likewise, if there are tokens from the UiWriter in the value string, we
+       * need it to replace them with the real expresions.
        */
       value = uiWriter.detokenate(value);
-
       return nextPlaceholder(name, example, value);
     }
 
@@ -94,9 +92,10 @@
 
   /**
    * Called by various {@link XMLElement} consumeInner*() methods after all
-   * elements have been handed to {@link #interpretElement}. Performs escaping
-   * on the consumed text to make it safe for use as a Messages {@literal @}Default
-   * value.
+   * elements have been handed to {@link #interpretElement}.
+   * <p>
+   * Performs escaping on the consumed text to make it safe for use as a
+   * Messages {@literal @}Default value
    */
   public String postProcess(String consumed) throws UnableToCompleteException {
     return tokenator.detokenate(MessageWriter.escapeMessageFormat(consumed));
@@ -105,10 +104,29 @@
   protected abstract String consumePlaceholderInnards(XMLElement elem)
       throws UnableToCompleteException;
 
+  /**
+   * To be called from {@link #interpretElement(XMLElement)}. Creates the next
+   * placeholder in the {@link MessageWriter} we're building, and returns the
+   * text to stand in its place.
+   *
+   * @param name
+   * @param example
+   * @param value
+   * @return
+   */
   protected String nextPlaceholder(String name, String example, String value) {
     message.addPlaceholder(new PlaceholderWriter(name, example, value));
-    return tokenator.nextToken(String.format("{%d}",
-        message.getPlaceholderCount() - 1));
+
+    /*
+     * We can't just return the {0} placeholder text, because it will be
+     * clobbered by the escaping performed in postProcess. We use a tokenator to
+     * hide the placeholder from the escaping step, and postProcess resolves the
+     * tokens when the escaping is done.
+     */
+    String placeholder = String.format("{%d}",
+        message.getPlaceholderCount() - 1);
+
+    return tokenator.nextToken(placeholder);
   }
 
   protected String stripTokens(String value) {
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
index b32ae43..1fde1b3 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
@@ -74,7 +74,9 @@
   @UiField ClickyLink funnyCharsMessageAttributeWidget;
   @UiField ParagraphElement funnyCharsMessageDomAttributeParagraph;
   @UiField ParagraphElement funnyCharsMessageParagraph;
+  @UiField SpanElement funnyCharsMessageChildSpan;
   @UiField ParagraphElement funnyCharsParagraph;
+  @UiField ParagraphElement funnyCharsProtectedMessageParagraph;
   @UiField Label gwtFieldLabel;
   @UiField ParagraphElement main;
   @UiField Button myButton;
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
index d4d99db..0d2db28 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
@@ -99,7 +99,7 @@
   </gwt:Dock>
   <gwt:Dock direction='CENTER'>
     <gwt:HTMLPanel>
-      <p><ui:msg>This is a demonstration and test bed of GWT's UiBinder
+      <p><ui:msg>This is a demonstration and test bed of GWT's shiny UiBinder
       package. At the moment it works mainly as described in
       <a href="http://code.google.com/p/google-web-toolkit-incubator/wiki/DeclarativeUi"
         ui:ph="oldBlogLink">
@@ -279,6 +279,8 @@
       untranslated paragraph.</p>
       <p ui:field="funnyCharsMessageParagraph"><ui:msg>They might show up
       in body text that has been <b>marked for translation</b>: funny characters " &quot; ' &#39; &amp; &lt; &gt; > { }</ui:msg></p>
+      <p><ui:msg>Or perhaps in a subelement with a ui:field: <span ui:field='funnyCharsMessageChildSpan'>funny characters " &quot; ' &#39; &amp; &lt; &gt; > { }</span></ui:msg></p>
+      <p ui:field="funnyCharsProtectedMessageParagraph"><ui:msg>Don't forget about protected untranslatable blocks: <ui:ph name='francine'>funny characters " &quot; ' &#39; &amp; &lt; &gt; > { }</ui:ph></ui:msg></p>
       <p ui:field="funnyCharsMessageDomAttributeParagraph" title="funny characters &quot; ' &#39; &amp; &lt; &gt; > { }">
         <ui:attribute name="title"/>
         Attributes of dom elements can be translated too, like the
diff --git a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
index 90bf5a4..82a842b 100644
--- a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
+++ b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
@@ -166,6 +166,17 @@
         t);
   }
 
+  public void testProtectedDomTextMessageWithFunnyChars() {
+    String t = widgetUi.funnyCharsProtectedMessageParagraph.getInnerText();
+    assertEquals("Don't forget about protected untranslatable blocks: "
+        + "funny characters \" \" ' ' & < > > { }", t);
+  }
+
+  public void testDomTextInNamedElementMessageWithFunnyChars() {
+    String t = widgetUi.funnyCharsMessageChildSpan.getInnerText();
+    assertEquals("funny characters \" \" ' ' & < > > { }", t);
+  }
+
   public void suppressedForSafari3Fail_testDomTextNoMessageWithFunnyChars() {
     ParagraphElement p = widgetUi.funnyCharsParagraph;
     // WebKit does \n replace thing, so let's do it everywhere