Revision: 9830
Author: [email protected]
Date: Wed Mar  9 09:01:28 2011
Log: Updates UiBinder HTML rendering to use SafeHtml. Rather than generating generic HTML strings for HTML objects, the generated binder code creates a SafeHtml template based on the *.ui.xml specs and passes arguments to the template as either String or SafeHtml-wrapped objects, depending on the context.

A small size regression is expected as a new SafeHtml template signature and implementation are being generated for each HTML object. However, the safety of the generated HTML provides a clear benefit.

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

Review by: [email protected]
http://code.google.com/p/google-web-toolkit/source/detail?r=9830

Added:
 /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java
/trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplateArgument.java
 /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplates.java
 /trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplateTest.java
/trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplatesTest.java
Modified:
/trunk/user/src/com/google/gwt/uibinder/elementparsers/AttributeMessageInterpreter.java /trunk/user/src/com/google/gwt/uibinder/elementparsers/ComputedAttributeInterpreter.java /trunk/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
 /trunk/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
 /trunk/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
 /trunk/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
 /trunk/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java /trunk/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java /trunk/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
 /trunk/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
 /trunk/user/src/com/google/gwt/uibinder/elementparsers/TextInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java /trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
 /trunk/user/src/com/google/gwt/uibinder/rebind/Tokenator.java
 /trunk/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
/trunk/user/src/com/google/gwt/uibinder/rebind/messages/AttributeMessage.java /trunk/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
 /trunk/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java /trunk/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java /trunk/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java /trunk/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java

=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java Wed Mar 9 09:01:28 2011
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.IndentedWriter;
+import com.google.gwt.uibinder.rebind.Tokenator;
+import com.google.gwt.uibinder.rebind.Tokenator.Resolver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An individual SafeHtml template to be written by UiBinder.
+ */
+public class HtmlTemplate {
+  private final List<String> template = new ArrayList<String>();
+  private final String methodName;
+  private final ArrayList<HtmlTemplateArgument> methodArgs =
+    new ArrayList<HtmlTemplateArgument>();
+  private final HtmlTemplates templates;
+
+ public HtmlTemplate(String html, Tokenator tokenator, HtmlTemplates templates)
+    throws IllegalArgumentException {
+    if (html == null) {
+      throw new IllegalArgumentException("Template html cannot be null");
+    }
+    if (tokenator == null) {
+ throw new IllegalArgumentException("Template tokenator cannot be null");
+    }
+    if (templates == null) {
+ throw new IllegalArgumentException("HtmlTemplates container cannot be null");
+    }
+
+    this.templates = templates;
+    methodName = "html" + this.templates.nextTemplateId();
+
+    populateArgMap(html,tokenator);
+
+    template.add("@Template(\"" + addTemplatePlaceholders(html) + "\")");
+ template.add("SafeHtml " + methodName + "(" + getTemplateArgs() + ");");
+    template.add(" ");
+  }
+
+  public List<String> getTemplate() {
+    return template;
+  }
+
+  /**
+   * Writes all templates to the provided {@link IndentedWriter}.
+   *
+   * @param w the writer to write the template to
+   */
+  public void writeTemplate(IndentedWriter w) {
+    for (String s : template) {
+      w.write(s);
+    }
+  }
+
+  /**
+   * Creates the template method invocation.
+   *
+   * @return String the template method call with parameters
+   */
+  public String writeTemplateCall() {
+    return "template." + methodName + "(" + getSafeHtmlArgs()
+      + ").asString()";
+  }
+
+  /**
+   *  Replaces string tokens with {} placeholders for SafeHtml templating.
+   *
+   * @return the rendering string, with tokens replaced by {} placeholders
+   */
+  private String addTemplatePlaceholders(String html) {
+    String rtn = Tokenator.detokenate(html, new Resolver() {
+      int tokenId = 0;
+      public String resolveToken(String token) {
+        return "{" + tokenId++ + "}";
+      }
+    });
+    return rtn;
+  }
+
+  /**
+   * Retrieves the arguments for SafeHtml template function call from
+   * the {@link Tokenator}.
+   */
+  private String getSafeHtmlArgs() {
+    StringBuilder b = new StringBuilder();
+
+    for (HtmlTemplateArgument arg : methodArgs) {
+      if (b.length() > 0) {
+        b.append(", ");
+      }
+      b.append(arg.getArg());
+    }
+
+    return b.toString();
+  }
+
+  /**
+ * Creates the argument string for the generated SafeHtmlTemplate function.
+   */
+  private String getTemplateArgs() {
+    StringBuilder b = new StringBuilder();
+    int i = 0;
+
+       for (HtmlTemplateArgument arg : methodArgs) {
+         if (b.length() > 0) {
+           b.append(", ");
+         }
+        b.append(arg.getType() + " arg" + i);
+        i++;
+      }
+
+    return b.toString();
+  }
+
+  private void populateArgMap(String s, Tokenator t) {
+    if (t != null) {
+      List<String> args = t.getOrderedValues(s);
+
+      for (String arg : args) {
+        if (templates.isSafeConstant(arg)) {
+          methodArgs.add(HtmlTemplateArgument.forHtml(arg));
+        } else {
+          methodArgs.add(HtmlTemplateArgument.forString(arg.substring(4,
+              arg.length() - 4)));
+        }
+      }
+    }
+  }
+}
+
=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplateArgument.java Wed Mar 9 09:01:28 2011
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+/**
+ * Container class for a UiBinder SafeHtml template argument.
+ */
+public class HtmlTemplateArgument {
+  static HtmlTemplateArgument forHtml(String arg) {
+    return new HtmlTemplateArgument("SafeHtml", arg);
+  }
+  static HtmlTemplateArgument forString(String arg) {
+    return new HtmlTemplateArgument("String", arg);
+  }
+
+  private final String arg;
+
+  private final String type;
+
+  private HtmlTemplateArgument(String type, String arg) {
+    this.type = type;
+    this.arg = arg;
+  }
+
+  public String getArg() {
+    return arg;
+  }
+
+  public String getType() {
+    return type;
+  }
+}
=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplates.java Wed Mar 9 09:01:28 2011
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.IndentedWriter;
+import com.google.gwt.uibinder.rebind.Tokenator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Model class for SafeHtml templates used in generated
+ * UiBinder rendering implementation.
+ */
+
+public class HtmlTemplates {
+ private final List<HtmlTemplate> htmlTemplates = new ArrayList<HtmlTemplate>();
+  private final Set<String> safeConstantTokens = new HashSet<String>();
+
+  public HtmlTemplates() {
+  }
+
+  /**
+ * Add a SafeHtml template and an instance method for invoking the template + * to the generated BinderImpl class. These templates are declared at the + * beginning of the class and instantiated with a GWT.create(Template.class
+   * call.
+   * <p>
+   * Note that the UiBinder#tokenator is used to determine the arguments to
+   * the generated SafeHtml template.
+   *
+   * @return String the function to call this template
+   */
+  public String addSafeHtmlTemplate(String html, Tokenator t)
+    throws IllegalArgumentException {
+    if (html == null) {
+      throw new IllegalArgumentException("Template html cannot be null");
+    }
+    if (t == null) {
+ throw new IllegalArgumentException("Template tokenator cannot be null");
+    }
+
+    HtmlTemplate ht = new HtmlTemplate(html, t, this);
+    htmlTemplates.add(ht);
+
+    return ht.writeTemplateCall();
+  }
+
+  public int getNumTemplates() {
+    return htmlTemplates.size();
+  }
+
+  public List<HtmlTemplate> getTemplates() {
+    return htmlTemplates;
+  }
+
+  public boolean isEmpty() {
+    return htmlTemplates.isEmpty();
+  }
+
+  public boolean isSafeConstant(String token) {
+    return safeConstantTokens.contains(token);
+  }
+
+  public void noteSafeConstant(String token) {
+    safeConstantTokens.add(token);
+  }
+
+  public void writeTemplates(IndentedWriter w) {
+    for (HtmlTemplate t : htmlTemplates) {
+      t.writeTemplate(w);
+    }
+  }
+
+  /**
+   * Increment the total number of templates.
+   */
+  protected int nextTemplateId() {
+   return htmlTemplates.size() + 1;
+  }
+}
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplateTest.java Wed Mar 9 09:01:28 2011
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.Tokenator;
+
+import junit.framework.TestCase;
+
+import java.util.Iterator;
+
+/**
+ * HtmlTemplate unit tests.
+ *
+ */
+public class HtmlTemplateTest extends TestCase {
+  public void testHappy() {
+    Tokenator t = new Tokenator();
+    HtmlTemplates templates = new HtmlTemplates();
+
+    HtmlTemplate h = new HtmlTemplate("<DialogBox id=\'"
+        + t.nextToken("\" + domId0 + \"")
+        + "\'>this is a dialog box</DialogBox>", t, templates);
+
+    String[] expectedTemplates = {
+ "@Template(\"<DialogBox id=\'{0}\'>this is a dialog box</DialogBox>\")",
+        "SafeHtml html1(String arg0);",
+        " ",};
+
+    Iterator<String> j = h.getTemplate().iterator();
+    for (String et : expectedTemplates) {
+      assertEquals(et, j.next());
+    }
+    assertFalse(j.hasNext());
+
+    String expectedCall = "template.html1(domId0).asString()";
+
+    assertEquals(h.writeTemplateCall(), expectedCall);
+  }
+
+  public void testNullHtml() {
+    HtmlTemplates templates = new HtmlTemplates();
+    Tokenator t = new Tokenator();
+
+    try {
+      new HtmlTemplate(null, t, templates);
+      fail("Expected empty html to generate error");
+    } catch (IllegalArgumentException e) {
+      assertTrue(e.getMessage().equals("Template html cannot be null"));
+    }
+  }
+
+  public void testNullTokenator() {
+    HtmlTemplates templates = new HtmlTemplates();
+
+    try {
+      new HtmlTemplate("<p>this is a static string</p>", null,
+          templates);
+      fail("Expected empty tokenator to generate error");
+      } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().equals("Template tokenator cannot be null"));
+      }
+  }
+
+  public void testNullHtmlTemplates() {
+    Tokenator t = new Tokenator();
+
+    try {
+      new HtmlTemplate("<p>this is a static string</p>", t, null);
+      fail("Expected empty html to generate error");
+    } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().equals("HtmlTemplates container cannot be null"));
+    }
+  }
+
+}
=======================================
--- /dev/null
+++ /trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplatesTest.java Wed Mar 9 09:01:28 2011
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.IndentedWriter;
+import com.google.gwt.uibinder.rebind.Tokenator;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * HtmlTemplates unit tests.
+ *
+ */
+public class HtmlTemplatesTest extends TestCase {
+  public void testHappy() throws IllegalArgumentException {
+    Tokenator t = new Tokenator();
+    HtmlTemplates h = new HtmlTemplates();
+
+    String call = h.addSafeHtmlTemplate("<DialogBox id=\'"
+        + t.nextToken("\" + domId0 + \"")
+        + "\'>this is a dialog box</DialogBox>", t);
+
+    // Check the generated template
+    String[] expectedTemplates = {
+ "@Template(\"<DialogBox id=\'{0}\'>this is a dialog box</DialogBox>\")",
+        "SafeHtml html1(String arg0);",
+        " ",};
+
+    List<String> templates = new ArrayList<String>();
+    for (HtmlTemplate ht : h.getTemplates()) {
+      templates.addAll(ht.getTemplate());
+    }
+
+    Iterator<String> i = templates.iterator();
+    for (String et : expectedTemplates) {
+        assertEquals(et, i.next());
+    }
+    assertFalse(i.hasNext());
+
+    // Check the returned template function call
+    String expectedCall = "template.html1(domId0).asString()";
+    assertEquals(call, expectedCall);
+
+    StringWriter s = new StringWriter();
+    IndentedWriter n = new IndentedWriter(new PrintWriter(s));
+
+    // Confirm that IndentedWriter writes the correct template.
+    h.writeTemplates(n);
+
+    assertEquals("@Template(\"<DialogBox id=\'{0}\'>this is a dialog "
+ + "box</DialogBox>\")\nSafeHtml html1(String arg0);\n \n", s.toString());
+  }
+
+  public void testNullHtml() {
+    HtmlTemplates h = new HtmlTemplates();
+    Tokenator t = new Tokenator();
+
+    try {
+    h.addSafeHtmlTemplate(null, t);
+    fail();
+    } catch (IllegalArgumentException e) {
+      assertTrue("Expected empty html to generate error",
+        e.getMessage().equals("Template html cannot be null"));
+    }
+
+    assertTrue(h.isEmpty());
+  }
+
+  public void testNullTokenator() throws IllegalArgumentException {
+    HtmlTemplates h = new HtmlTemplates();
+
+    try {
+    h.addSafeHtmlTemplate("<p>this is a static string</p>", null);
+    fail();
+    } catch (IllegalArgumentException e) {
+      assertTrue("Expected empty tokenator to generate error",
+          e.getMessage().equals("Template tokenator cannot be null"));
+    }
+
+    assertTrue(h.isEmpty());
+  }
+}
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/AttributeMessageInterpreter.java Fri Nov 6 16:08:46 2009 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/AttributeMessageInterpreter.java Wed Mar 9 09:01:28 2011
@@ -47,7 +47,7 @@
     MessagesWriter messages = writer.getMessages();
     for (AttributeMessage am : messages.consumeAttributeMessages(elem)) {
       elem.setAttribute(am.getAttribute(),
-        writer.tokenForExpression(am.getMessageAsHtmlAttribute()));
+        writer.tokenForStringExpression(am.getMessageUnescaped()));
     }

     /*
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/ComputedAttributeInterpreter.java Wed Nov 11 22:08:47 2009 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/ComputedAttributeInterpreter.java Wed Mar 9 09:01:28 2011
@@ -50,7 +50,7 @@
       if (parser != null) {
         // Legacy res:style='style.pretty'
         String parsedValue = parser.parse(att.consumeRawValue());
-        String attToken = writer.tokenForExpression(parsedValue);
+        String attToken = writer.tokenForStringExpression(parsedValue);

         // Use localName so <div res:style='...'> becomes <div style='...'>
         attNameToToken.put(att.getLocalName(), attToken);
@@ -58,7 +58,7 @@
       }

       if (att.hasComputedValue()) {
- String attToken = writer.tokenForExpression(att.consumeStringValue()); + String attToken = writer.tokenForStringExpression(att.consumeStringValue());
         attNameToToken.put(att.getName(), attToken);
       } else {
         /*
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java Mon Jun 7 12:20:31 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java Wed Mar 9 09:01:28 2011
@@ -68,8 +68,8 @@
             writer, fieldName);
         String innerHtml = child.consumeInnerHtml(interpreter);
         if (innerHtml.length() > 0) {
-          writer.addStatement("%s.%s().setHTML(\"%s\");", fieldName,
-              faceNameGetter(faceName), innerHtml);
+          writer.addStatement("%s.%s().setHTML(%s);", fieldName,
+ faceNameGetter(faceName), writer.declareTemplateCall(innerHtml));
         }

         if (child.hasAttribute("image")) {
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java Thu Jan 20 14:28:08 2011 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java Wed Mar 9 09:01:28 2011
@@ -77,7 +77,7 @@
     handleConstructorArgs(elem, fieldName, type, writer, customCaption);

     if (caption != null) {
-      writer.addStatement("%s.setHTML(\"%s\");", fieldName, caption);
+ writer.addStatement("%s.setHTML(%s);", fieldName, writer.declareTemplateCall(caption));
     }
     if (body != null) {
       writer.addStatement("%s.setWidget(%s);", fieldName, body);
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java Fri Apr 2 06:00:38 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java Wed Mar 9 09:01:28 2011
@@ -39,8 +39,9 @@
String html = elem.consumeOpeningTag() + elem.consumeInnerHtml(interpreter)
         + elem.getClosingTag();
     writer.endAttachedSection();
+
     writer.setFieldInitializer(fieldName, String.format(
-        "(%1$s) UiBinderUtil.fromHtml(\"%2$s\")",
-        type.getQualifiedSourceName(), html));
+        "(%1$s) UiBinderUtil.fromHtml(%2$s)",
+        type.getQualifiedSourceName(), writer.declareTemplateCall(html)));
   }
 }
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java Wed Jul 21 11:18:39 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java Wed Mar 9 09:01:28 2011
@@ -93,9 +93,10 @@
       for (List<CellContent> row : matrix) {
         for (CellContent column : row) {
           if (column.getTagName().equals(CELL_TAG)) {
-            writer.addStatement("%s.setHTML(%s, %s, \"%s\");", fieldName,
+            writer.addStatement("%s.setHTML(%s, %s, %s);", fieldName,
                 Integer.toString(matrix.indexOf(row)),
-                Integer.toString(row.indexOf(column)), column.getConent());
+                Integer.toString(row.indexOf(column)),
+                writer.declareTemplateCall(column.getConent()));
           }
           if (column.getTagName().equals(CUSTOMCELL_TAG)) {
             writer.addStatement("%s.setWidget(%s, %s, %s);", fieldName,
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java Fri Apr 2 06:00:38 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java Wed Mar 9 09:01:28 2011
@@ -57,12 +57,12 @@
      * signature (by passing in type).
      */
     String customTag = elem.consumeStringAttribute("tag", null);
+
     if (null == customTag) {
-      writer.setFieldInitializerAsConstructor(fieldName, type, "\"" + html
-          + "\"");
+ writer.setFieldInitializerAsConstructor(fieldName, type, writer.declareTemplateCall(html));
     } else {
- writer.setFieldInitializerAsConstructor(fieldName, type, customTag, "\""
-          + html + "\"");
+      writer.setFieldInitializerAsConstructor(fieldName, type, customTag,
+          writer.declareTemplateCall(html));
     }
   }

=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java Fri Apr 2 06:00:38 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java Wed Mar 9 09:01:28 2011
@@ -35,7 +35,7 @@
     writer.endAttachedSection();
     // TODO(jgw): throw an error if there's a conflicting 'html' attribute.
     if (html.trim().length() > 0) {
-      writer.genStringPropertySet(fieldName, "HTML", html);
+ writer.genPropertySet(fieldName, "HTML", writer.declareTemplateCall(html));
     }
   }
 }
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java Mon Jun 7 12:20:31 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java Wed Mar 9 09:01:28 2011
@@ -74,7 +74,7 @@

       MessageWriter message = messages.newMessage(elem);
message.setDefaultMessage(elem.consumeInnerHtml(phiProvider.get(message)));
-      return uiWriter.tokenForExpression(messages.declareMessage(message));
+ return uiWriter.tokenForSafeHtmlExpression(messages.declareMessage(message));
     }

     return null;
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java Tue Aug 31 08:46:42 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java Wed Mar 9 09:01:28 2011
@@ -75,8 +75,8 @@
             writer, fieldName);
String size = children.header.consumeRequiredDoubleAttribute("size");
         String html = children.header.consumeInnerHtml(htmlInt);
-        writer.addStatement("%s.add(%s, \"%s\", true, %s);", fieldName,
-            childFieldName, html, size);
+        writer.addStatement("%s.add(%s, %s, true, %s);", fieldName,
+            childFieldName, writer.declareTemplateCall(html), size);
       } else if (children.customHeader != null) {
XMLElement headerElement = children.customHeader.consumeSingleChildElement(); String size = children.customHeader.consumeRequiredDoubleAttribute("size");
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java Tue Aug 31 08:46:42 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java Wed Mar 9 09:01:28 2011
@@ -78,8 +78,8 @@
HtmlInterpreter htmlInt = HtmlInterpreter.newInterpreterForUiObject(
             writer, fieldName);
         String html = children.header.consumeInnerHtml(htmlInt);
-        writer.addStatement("%s.add(%s, \"%s\", true);", fieldName,
-            childFieldName, html);
+        writer.addStatement("%s.add(%s, %s, true);", fieldName,
+            childFieldName, writer.declareTemplateCall(html));
       } else if (children.customHeader != null) {
XMLElement headerElement = children.customHeader.consumeSingleChildElement();

=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java Tue Aug 31 08:46:42 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java Wed Mar 9 09:01:28 2011
@@ -72,8 +72,8 @@
         writer.die(tabElem, "Must have a child widget");
       }
       if (tabHTML != null) {
-        writer.addStatement("%1$s.add(%2$s, \"%3$s\", true);", fieldName,
-            childFieldName, tabHTML);
+        writer.addStatement("%1$s.add(%2$s, %3$s, true);", fieldName,
+            childFieldName, writer.declareTemplateCall(tabHTML));
       } else if (tabCaption != null) {
writer.addStatement("%1$s.add(%2$s, %3$s);", fieldName, childFieldName,
             tabCaption);
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/TextInterpreter.java Mon Jun 7 12:20:31 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/TextInterpreter.java Wed Mar 9 09:01:28 2011
@@ -43,7 +43,7 @@
     MessagesWriter messages = writer.getMessages();
     if (messages.isMessage(elem)) {
       String messageInvocation = consumeAsTextMessage(elem, messages);
-      return writer.tokenForExpression(messageInvocation);
+      return writer.tokenForStringExpression(messageInvocation);
     }

return new UiTextInterpreter(writer.getLogger()).interpretElement(elem);
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java Mon Apr 19 07:56:12 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java Wed Mar 9 09:01:28 2011
@@ -91,7 +91,8 @@

       // Create an element to hold the widget.
       String tag = getLegalPlaceholderTag(elem);
-      return "<" + tag + " id='\" + " + idHolder + " + \"'></" + tag + ">";
+ return "<" + tag + " id='" + uiWriter.tokenForStringExpression(idHolder)
+      + "'></" + tag + ">";
     }
     return null;
   }
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java Tue Sep 7 12:42:09 2010 +++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java Wed Mar 9 09:01:28 2011
@@ -132,7 +132,8 @@
   }

   private String genOpenTag(String name, String idHolder) {
-    String openTag = String.format("<span id='\" + %s + \"'>", idHolder);
+    String openTag = String.format("<span id='%s'>",
+        uiWriter.tokenForStringExpression(idHolder));
     String openPlaceholder = nextOpenPlaceholder(name + "Begin", openTag);
     return openPlaceholder;
   }
@@ -179,7 +180,8 @@
   }

private String handleOpaqueWidgetPlaceholder(String name, String idHolder) { - String tag = String.format("<span id='\" + %s + \"'></span>", idHolder);
+    String tag = String.format("<span id='%s'></span>",
+      uiWriter.tokenForStringExpression(idHolder));
     String placeholder = nextPlaceholder(name, "<span></span>", tag);
     return placeholder;
   }
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/rebind/Tokenator.java Fri Nov 6 15:04:44 2009 +++ /trunk/user/src/com/google/gwt/uibinder/rebind/Tokenator.java Wed Mar 9 09:01:28 2011
@@ -15,7 +15,9 @@
  */
 package com.google.gwt.uibinder.rebind;

+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;

 /**
@@ -53,10 +55,26 @@
     detokenated.append(betokened.substring(index));
     return detokenated.toString();
   }
-
+
   public static boolean hasToken(String s) {
     return s.matches(".*" + TOKEN_REGEXP + "\\d+" + TOKEN_REGEXP + ".*");
   }
+
+  private static List<String> getOrderedValues(String betokened,
+      Resolver resolver) {
+    List<String> values = new ArrayList<String>();
+
+    int index = 0, nextToken = 0;
+    while ((nextToken = betokened.indexOf(TOKEN, index)) > -1) {
+      int endToken = betokened.indexOf(TOKEN, nextToken + TOKEN.length());
+ String token = betokened.substring(nextToken, endToken + TOKEN.length());
+      values.add(resolver.resolveToken(token));
+
+      index = endToken + TOKEN.length();
+    }
+
+    return values;
+  }

   private static String nextToken() {
     return TOKEN + (curId++) + TOKEN;
@@ -72,6 +90,14 @@
       }
     });
   }
+
+  public List<String> getOrderedValues(String betokened) {
+    return getOrderedValues(betokened, new Resolver() {
+      public String resolveToken(String token) {
+        return tokenToResolved.get(token);
+      }
+    });
+  }

   public String nextToken(final String resolved) {
     String token = nextToken();
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java Wed Feb 9 13:31:48 2011 +++ /trunk/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java Wed Mar 9 09:01:28 2011
@@ -31,6 +31,7 @@
 import com.google.gwt.uibinder.elementparsers.IsEmptyParser;
 import com.google.gwt.uibinder.elementparsers.UiChildParser;
 import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
+import com.google.gwt.uibinder.rebind.model.HtmlTemplates;
 import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
 import com.google.gwt.uibinder.rebind.model.ImplicitCssResource;
 import com.google.gwt.uibinder.rebind.model.OwnerClass;
@@ -169,11 +170,12 @@

   private final List<String> initStatements = new ArrayList<String>();
   private final List<String> statements = new ArrayList<String>();
+  private final HtmlTemplates htmlTemplates = new HtmlTemplates();
   private final HandlerEvaluator handlerEvaluator;
   private final MessagesWriter messages;
   private final DesignTimeUtils designTime;
   private final Tokenator tokenator = new Tokenator();
-
+
   private final String templatePath;
   private final TypeOracle oracle;
   /**
@@ -205,7 +207,6 @@
   private String gwtPrefix;

   private String rendered;
-
   /**
    * Stack of element variable names that have been attached.
    */
@@ -214,14 +215,15 @@
* Maps from field element name to the temporary attach record variable name.
    */
private final Map<String, String> attachedVars = new HashMap<String, String>();
-  private int nextAttachVar = 0;
-
+
+  private int nextAttachVar = 0;
   /**
    * Stack of statements to be executed after we detach the current attach
    * section.
    */
private final LinkedList<List<String>> detachStatementsStack = new LinkedList<List<String>>();
   private final AttributeParsers attributeParsers;
+
   private final BundleAttributeParsers bundleParsers;

   private final UiBinderContext uiBinderCtx;
@@ -289,7 +291,7 @@
   public void addDetachStatement(String format, Object... args) {
     detachStatementsStack.getFirst().add(String.format(format, args));
   }
-
+
   /**
* Add a statement to be run after everything has been instantiated, in the
    * style of {@link String#format}.
@@ -313,8 +315,8 @@
    * moment, HasHTMLParser, HTMLPanelParser, and DomElementParser.).
    * <p>
    * Succeeding calls made to {@link #ensureAttached} and
- * {@link #ensureFieldAttached} must refer to children of this element, until
-   * {@link #endAttachedSection} is called.
+ * {@link #ensureCurrentFieldAttached} must refer to children of this element,
+   * until {@link #endAttachedSection} is called.
    *
* @param element Java expression for the generated code that will return the
    *          dom element to be attached.
@@ -345,7 +347,8 @@
"%s = com.google.gwt.dom.client.Document.get().getElementById(%s).cast();",
         fieldName, name);
     addInitStatement("%s.removeAttribute(\"id\");", fieldName);
-    return tokenForExpression(name);
+
+    return tokenForStringExpression(name);
   }

   /**
@@ -404,11 +407,23 @@
   }

   /**
- * Given a string containing tokens returned by {@link #tokenForExpression} or - * {@link #declareDomField}, return a string with those tokens replaced by the
-   * appropriate expressions. (It is not normally necessary for an
- * {@link XMLElement.Interpreter} or {@link ElementParser} to make this call,
-   * as the tokens are typically replaced by the TemplateWriter itself.)
+   * Writes a new SafeHtml template to the generated BinderImpl.
+   *
+ * @return The invocation of the SafeHtml template function with the arguments
+   * filled in
+   */
+  public String declareTemplateCall(String html)
+    throws IllegalArgumentException {
+    return htmlTemplates.addSafeHtmlTemplate(html, tokenator);
+  }
+
+  /**
+ * Given a string containing tokens returned by {@link #tokenForStringExpression}, + * {@link #tokenForSafeHtmlExpression} or {@link #declareDomField}, return a + * string with those tokens replaced by the appropriate expressions. (It is
+   * not normally necessary for an {@link XMLElement.Interpreter} or
+   * {@link ElementParser} to make this call, as the tokens are typically
+   * replaced by the TemplateWriter itself.)
    */
   public String detokenate(String betokened) {
     return tokenator.detokenate(betokened);
@@ -478,7 +493,7 @@
   /**
* Ensure that the specified field is attached to the DOM. The field must hold * an object that responds to Element getElement(). Convenience wrapper for
-   * {@link ensureAttached}<code>(field + ".getElement()")</code>.
+   * {@link #ensureAttached}<code>(field + ".getElement()")</code>.
    *
    * @see #beginAttachedSection(String)
    */
@@ -558,7 +573,7 @@
   public DesignTimeUtils getDesignTime() {
     return designTime;
   }
-
+
   /**
* Returns the logger, at least until we get get it handed off to parsers via
    * constructor args.
@@ -658,18 +673,35 @@
     setFieldInitializer(fieldName, formatCode("new %s(%s)",
         type.getQualifiedSourceName(), asCommaSeparatedList(args)));
   }
+
+  /**
+ * Like {@link #tokenForStringExpression}, but used for runtime expressions + * that we trust to be safe to interpret at runtime as HTML without escaping,
+   * like translated messages with simple formatting. Wrapped in a call to
+ * {@link com.google.gwt.safehtml.shared.SafeHtmlUtils#fromSafeConstant} to
+   * keep the expression from being escaped by the SafeHtml template.
+   *
+   * @param expression
+   */
+  public String tokenForSafeHtmlExpression(String expression) {
+    String token =  tokenator.nextToken("SafeHtmlUtils.fromSafeConstant(" +
+        expression + ")");
+    htmlTemplates.noteSafeConstant("SafeHtmlUtils.fromSafeConstant(" +
+        expression + ")");
+    return token;
+  }

   /**
    * Returns a string token that can be used in place the given expression
    * inside any string literals. Before the generated code is written, the
- * expression will be stiched back into the generated code in place of the + * expression will be stitched back into the generated code in place of the * token, surrounded by plus signs. This is useful in strings to be handed to * setInnerHTML() and setText() calls, to allow a unique dom id attribute or
    * other runtime expression in the string.
    *
    * @param expression
    */
-  public String tokenForExpression(String expression) {
+  public String tokenForStringExpression(String expression) {
     return tokenator.nextToken(("\" + " + expression + " + \""));
   }

@@ -686,7 +718,7 @@
   public void warn(String message, Object... params) {
     logger.warn(message, params);
   }
-
+
   /**
    * Post a warning message.
    */
@@ -716,7 +748,7 @@
     this.rendered = tokenator.detokenate(parseDocumentElement(elem));
     printWriter.print(rendered);
   }
-
+
   private void addElementParser(String gwtClass, String parser) {
     elementParsers.put(gwtClass, parser);
   }
@@ -750,7 +782,7 @@
* Ensures that all of the internal data structures are cleaned up correctly
    * at the end of parsing the document.
    *
-   * @throws UnableToCompleteException
+   * @throws IllegalStateException
    */
   private void ensureAttachmentCleanedUp() {
     if (!attachSectionElements.isEmpty()) {
@@ -1017,6 +1049,9 @@
     writeClassOpen(w);
     writeStatics(w);
     w.newline();
+
+    // Create SafeHtml Template
+    writeSafeHtmlTemplates(w);

     // createAndBindUi method
     w.write("public %s createAndBindUi(final %s owner) {",
@@ -1027,7 +1062,7 @@

     writeGwtFields(w);
     w.newline();
-
+
     designTime.writeAttributes(this);
     writeAddedStatements(w);
     w.newline();
@@ -1112,6 +1147,11 @@

   private void writeImports(IndentedWriter w) {
     w.write("import com.google.gwt.core.client.GWT;");
+    if (!(htmlTemplates.isEmpty())) {
+      w.write("import com.google.gwt.safehtml.client.SafeHtmlTemplates;");
+      w.write("import com.google.gwt.safehtml.shared.SafeHtml;");
+      w.write("import com.google.gwt.safehtml.shared.SafeHtmlUtils;");
+    }
     w.write("import com.google.gwt.uibinder.client.UiBinder;");
     w.write("import com.google.gwt.uibinder.client.UiBinderUtil;");
     w.write("import %s.%s;", uiRootType.getPackage().getName(),
@@ -1177,6 +1217,25 @@
     }
   }

+  /**
+ * Write statements created by {@link HtmlTemplates#addSafeHtmlTemplate}. This
+   * code must be placed after all instantiation code.
+   */
+  private void writeSafeHtmlTemplates(IndentedWriter w) {
+    if (!(htmlTemplates.isEmpty())) {
+      w.write("interface Template extends SafeHtmlTemplates {");
+      w.indent();
+
+      htmlTemplates.writeTemplates(w);
+
+      w.outdent();
+      w.write("}");
+      w.newline();
+      w.write("Template template = GWT.create(Template.class);");
+      w.newline();
+    }
+  }
+
   /**
* Generates instances of any bundle classes that have been referenced by a * namespace entry in the top level element. This must be called *after* all
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/rebind/messages/AttributeMessage.java Wed Oct 28 09:10:53 2009 +++ /trunk/user/src/com/google/gwt/uibinder/rebind/messages/AttributeMessage.java Wed Mar 9 09:01:28 2011
@@ -32,15 +32,6 @@
   public String getAttribute() {
     return attribute;
   }
-
-  /**
-   * Return an expression to fetch the message and escape it, suitable
-   * for concatenation into the middle of an innerHTML string.
-   */
-  public String getMessageAsHtmlAttribute() {
-    return message
-    + ".replaceAll(\"&\", \"&amp;\").replaceAll(\"'\", \"&#39;\")";
-  }

   public String getMessageUnescaped() {
     return message;
=======================================
--- /trunk/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java Thu Jan 20 14:28:08 2011 +++ /trunk/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java Wed Mar 9 09:01:28 2011
@@ -79,7 +79,7 @@
     b.append("</g:DialogBox> ");

     String[] expected = {
-        "fieldName.setHTML(\"Hello, I <b>caption</b>you.\");",
+        "fieldName.setHTML(\"@mockToken-Hello, I <b>caption</b>you.\");",
         "fieldName.setWidget(<g:Label>);",};

     FieldWriter w = tester.parse(b.toString());
@@ -348,7 +348,7 @@
     b.append("</ui:UiBinder>");

     String[] expected = {
-        "fieldName.setHTML(\"Hello, I <b>caption</b>you.\");",
+        "fieldName.setHTML(\"@mockToken-Hello, I <b>caption</b>you.\");",
         "fieldName.setWidget(<g:Label>);",};

parser.parse(tester.getElem(b.toString(), "my:MyDialogBox"), "fieldName",
=======================================
--- /trunk/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java Wed Jul 21 11:18:39 2010 +++ /trunk/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java Wed Mar 9 09:01:28 2011
@@ -53,8 +53,8 @@
       assertNotNull(tester.logger.died);
     }
   }
-
-  public void testEmtpyChild() throws SAXException {
+
+  public void testEmptyChild() throws SAXException {
     StringBuffer b = new StringBuffer();
     b.append("<g:Grid>");
     b.append("  <g:row>");
@@ -140,8 +140,8 @@

     String[] expected = {
         "fieldName.resize(2, 2);",
-        "fieldName.setHTML(0, 0, \"<div>foo HTML element</div>\");",
-        "fieldName.setHTML(0, 1, \"<div>bar HTML element</div>\");",
+ "fieldName.setHTML(0, 0, \"@mockToken-<div>foo HTML element</div>\");", + "fieldName.setHTML(0, 1, \"@mockToken-<div>bar HTML element</div>\");",
         "fieldName.setWidget(1, 0, <g:Label>);",
         "fieldName.setWidget(1, 1, <g:Label>);"};

@@ -177,7 +177,7 @@

     String[] expected = {
         "fieldName.resize(2, 2);",
-        "fieldName.setHTML(0, 0, \"<div>foo HTML element</div>\");",
+ "fieldName.setHTML(0, 0, \"@mockToken-<div>foo HTML element</div>\");",
         "fieldName.setWidget(1, 0, <g:Label>);",
         "fieldName.setWidget(1, 1, <g:Label>);"};

=======================================
--- /trunk/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java Wed Feb 9 14:35:38 2011 +++ /trunk/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java Wed Mar 9 09:01:28 2011
@@ -32,7 +32,7 @@

 class MockUiBinderWriter extends UiBinderWriter {
   final List<String> statements = new ArrayList<String>();
-
+
   public MockUiBinderWriter(JClassType baseClass, String implClassName,
       String templatePath, TypeOracle oracle, MortalLogger logger,
       FieldManager fieldManager, MessagesWriter messagesWriter,
@@ -46,10 +46,18 @@
   public void addStatement(String format, Object... args) {
     statements.add(String.format(format, args));
   }
-
+
+  /**
+   * Mocked out version of the template declaration. Returns
+   * the template prefixed with "@mockToken-"
+   */
+  public String declareTemplateCall(String html) {
+    return "\"@mockToken-" + html + "\"";
+  }
+
   @Override
   public String parseElementToField(XMLElement elem)
       throws UnableToCompleteException {
     return elem.consumeOpeningTag();
-  }
-}
+  }
+}
=======================================
--- /trunk/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java Tue Aug 31 08:46:42 2010 +++ /trunk/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java Wed Mar 9 09:01:28 2011
@@ -165,7 +165,7 @@
         + "(com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());

     assertStatements(
- "fieldName.add(<g:Label id='able'>, \"Re<b>mark</b>able\", true, 3);", + "fieldName.add(<g:Label id='able'>, \"@mockToken-Re<b>mark</b>able\", true, 3);", "fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>, 3);");
   }

=======================================
--- /trunk/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java Tue Aug 31 08:46:42 2010 +++ /trunk/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java Wed Mar 9 09:01:28 2011
@@ -168,7 +168,7 @@
     b.append("</g:TabLayoutPanel>");

     String[] expected = {
-        "fieldName.add(<g:Label id='able'>, \"Re<b>mark</b>able\", true);",
+ "fieldName.add(<g:Label id='able'>, \"@mockToken-Re<b>mark</b>able\", true);", "fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>);",};

     FieldWriter w = tester.parse(b.toString());
=======================================
--- /trunk/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java Tue Aug 31 08:46:42 2010 +++ /trunk/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java Wed Mar 9 09:01:28 2011
@@ -146,7 +146,7 @@
     tester.parse(b.toString());

     assertStatements("fieldName.add(<g:Label id='0'>, \"Foo\");",
-        "fieldName.add(<g:Label id='1'>, \"B<b>a</b>r\", true);");
+ "fieldName.add(<g:Label id='1'>, \"@mockToken-B<b>a</b>r\", true);");
   }

   private void assertStatements(String... expected) {

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to