Re-roll issue 1442804: SafeHtmlRenderer code gen for UiBinder

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

Review by: rjrjr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10345 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/text/shared/UiRenderer.java b/user/src/com/google/gwt/text/shared/UiRenderer.java
new file mode 100644
index 0000000..c2064b8
--- /dev/null
+++ b/user/src/com/google/gwt/text/shared/UiRenderer.java
@@ -0,0 +1,25 @@
+/*
+ * 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.text.shared;
+
+/**
+ * Marker interface for SafeHtmlRenderer implementations to be code generated
+ * from ui.xml files.
+ * 
+ * @param <T> the type to render
+ */
+public interface UiRenderer<T> extends SafeHtmlRenderer<T> {
+}
diff --git a/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml b/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml
index 50cbc5b..3e2b6db 100644
--- a/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml
+++ b/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml
@@ -34,6 +34,10 @@
   <set-configuration-property name="UiBinder.useLazyWidgetBuilders" value="false"/>
 
   <generate-with class="com.google.gwt.uibinder.rebind.UiBinderGenerator">
+    <when-type-assignable class="com.google.gwt.text.shared.UiRenderer"/>
+  </generate-with>
+
+  <generate-with class="com.google.gwt.uibinder.rebind.UiBinderGenerator">
     <when-type-assignable class="com.google.gwt.uibinder.client.UiBinder"/>
   </generate-with>
 </module>
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java b/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
index bb7d54b..7dfe858 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
@@ -69,7 +69,8 @@
         String innerHtml = child.consumeInnerHtml(interpreter);
         if (innerHtml.length() > 0) {
           writer.addStatement("%s.%s().setHTML(%s);", fieldName,
-              faceNameGetter(faceName), writer.declareTemplateCall(innerHtml));
+              faceNameGetter(faceName), writer.declareTemplateCall(innerHtml,
+                  fieldName));
         }
 
         if (child.hasAttribute("image")) {
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java b/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
index 7f764de..211aa80 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
@@ -77,7 +77,8 @@
     handleConstructorArgs(elem, fieldName, type, writer, customCaption);
 
     if (caption != null) {
-      writer.addStatement("%s.setHTML(%s);", fieldName, writer.declareTemplateCall(caption));
+      writer.addStatement("%s.setHTML(%s);", fieldName,
+          writer.declareTemplateCall(caption, fieldName));
     }
     if (body != null) {
       writer.addStatement("%s.setWidget(%s);", fieldName, body);
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java b/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
index ee40b47..3428485 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
@@ -42,6 +42,6 @@
 
     writer.setFieldInitializer(fieldName, String.format(
         "(%1$s) UiBinderUtil.fromHtml(%2$s)",
-        type.getQualifiedSourceName(), writer.declareTemplateCall(html)));
+        type.getQualifiedSourceName(), writer.declareTemplateCall(html, fieldName)));
   }
 }
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java b/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
index ae4c658..37d94ba 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
@@ -128,7 +128,7 @@
             writer.addStatement("%s.setHTML(%s, %s, %s);", fieldName,
                 Integer.toString(matrix.indexOf(row)),
                 Integer.toString(row.getColumns().indexOf(column)),
-                writer.declareTemplateCall(column.getContent()));
+                writer.declareTemplateCall(column.getContent(), fieldName));
           }
           if (column.getTagName().equals(CUSTOMCELL_TAG)) {
             writer.addStatement("%s.setWidget(%s, %s, %s);", fieldName,
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
index 0aca4c1..7ab49b0 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
@@ -67,10 +67,11 @@
     String customTag = elem.consumeStringAttribute("tag", null);
 
     if (null == customTag) {
-      writer.setFieldInitializerAsConstructor(fieldName, type, writer.declareTemplateCall(html));
+      writer.setFieldInitializerAsConstructor(fieldName, type,
+          writer.declareTemplateCall(html, fieldName));
     } else {
       writer.setFieldInitializerAsConstructor(fieldName, type, customTag,
-          writer.declareTemplateCall(html));
+          writer.declareTemplateCall(html, fieldName));
     }
   }
 
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java b/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
index bcc24bf..cfdf40f 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
@@ -35,7 +35,8 @@
     writer.endAttachedSection();
     // TODO(jgw): throw an error if there's a conflicting 'html' attribute.
     if (html.trim().length() > 0) {
-      writer.genPropertySet(fieldName, "HTML", writer.declareTemplateCall(html));
+      writer.genPropertySet(fieldName, "HTML", writer.declareTemplateCall(html,
+          fieldName));
     }
   }
 }
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/RenderablePanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/RenderablePanelParser.java
index 7dc6ee6..4b1b258 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/RenderablePanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/RenderablePanelParser.java
@@ -85,7 +85,8 @@
           "RenderablePanel does not support custom root elements yet.");
     }
 
-    writer.setFieldInitializerAsConstructor(fieldName, type, writer.declareTemplateCall(html));
+    writer.setFieldInitializerAsConstructor(fieldName, type, writer.declareTemplateCall(html,
+        fieldName));
   }
 
   /**
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
index c4bbd2e..00f3ffb 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
@@ -76,7 +76,7 @@
         String size = children.header.consumeRequiredDoubleAttribute("size");
         String html = children.header.consumeInnerHtml(htmlInt);
         writer.addStatement("%s.add(%s, %s, true, %s);", fieldName,
-            childFieldName, writer.declareTemplateCall(html), size);
+            childFieldName, writer.declareTemplateCall(html, fieldName), size);
       } else if (children.customHeader != null) {
         XMLElement headerElement = children.customHeader.consumeSingleChildElement();
         String size = children.customHeader.consumeRequiredDoubleAttribute("size");
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
index e5447df..0eceec0 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
@@ -79,7 +79,7 @@
             writer, fieldName);
         String html = children.header.consumeInnerHtml(htmlInt);
         writer.addStatement("%s.add(%s, %s, true);", fieldName,
-            childFieldName, writer.declareTemplateCall(html));
+            childFieldName, writer.declareTemplateCall(html, fieldName));
       } else if (children.customHeader != null) {
         XMLElement headerElement = children.customHeader.consumeSingleChildElement();
 
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
index c7e45f6..a1736b0 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
@@ -70,7 +70,7 @@
       }
       if (tabHTML != null) {
         writer.addStatement("%1$s.add(%2$s, %3$s, true);", fieldName,
-            childFieldName, writer.declareTemplateCall(tabHTML));
+            childFieldName, writer.declareTemplateCall(tabHTML, fieldName));
       } else if (tabCaption != null) {
         writer.addStatement("%1$s.add(%2$s, %3$s);", fieldName, childFieldName,
             tabCaption);
diff --git a/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java b/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java
index 507305c..b89a0f5 100644
--- a/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java
@@ -60,6 +60,7 @@
   private int buildPrecedence;
   private final MortalLogger logger;
   private final FieldWriterType fieldType;
+  private String html;
 
   public AbstractFieldWriter(String name, FieldWriterType fieldType, MortalLogger logger) {
     if (name == null) {
@@ -96,6 +97,10 @@
     return fieldType;
   }
 
+  public String getHtml() {
+    return html + ".asString()";
+  }
+
   public String getInitializer() {
     return initializer;
   }
@@ -115,6 +120,10 @@
     return getReturnType(getAssignableType(), pathList, logger);
   }
 
+  public String getSafeHtml() {
+    return html;
+  }
+
   public void needs(FieldWriter f) {
     needs.add(f);
   }
@@ -124,6 +133,10 @@
     this.buildPrecedence = precedence;
   }
 
+  public void setHtml(String html) {
+    this.html = html;
+  }
+
   public void setInitializer(String initializer) {
     this.initializer = initializer;
   }
diff --git a/user/src/com/google/gwt/uibinder/rebind/FieldWriter.java b/user/src/com/google/gwt/uibinder/rebind/FieldWriter.java
index 6ed3527..dd9851d 100644
--- a/user/src/com/google/gwt/uibinder/rebind/FieldWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/FieldWriter.java
@@ -99,6 +99,11 @@
   FieldWriterType getFieldType();
 
   /**
+   * Returns the string html representation of the field.
+   */
+  String getHtml();
+
+  /**
    * Returns the custom initializer for this field, or null if it is not set.
    */
   String getInitializer();
@@ -127,6 +132,11 @@
   JType getReturnType(String[] path, MonitoredLogger logger);
 
   /**
+   * Returns the string SafeHtml representation of the field.
+   */
+  String getSafeHtml();
+
+  /**
    * Declares that the receiver depends upon the given field.
    */
   void needs(FieldWriter f);
@@ -140,6 +150,11 @@
   void setBuildPrecedence(int precedence);
 
   /**
+   * Sets the html representation of the field for applicable field types.
+   */
+  void setHtml(String html);
+
+  /**
    * Used to provide an initializer string to use instead of a
    * {@link com.google.gwt.core.client.GWT#create} call. Note that this is an
    * RHS expression. Don't include the leading '=', and don't end it with ';'.
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
index 791aba1..2d5f550 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -23,6 +23,7 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dom.client.TagName;
 import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.text.shared.UiRenderer;
 import com.google.gwt.uibinder.attributeparsers.AttributeParser;
 import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
 import com.google.gwt.uibinder.attributeparsers.BundleAttributeParser;
@@ -242,6 +243,7 @@
   private final UiBinderContext uiBinderCtx;
 
   private final String binderUri;
+  private final boolean isRenderer;
 
   public UiBinderWriter(JClassType baseClass, String implClassName,
       String templatePath, TypeOracle oracle, MortalLogger logger,
@@ -279,13 +281,42 @@
     JClassType uiBinderType = uiBinderTypes[0];
 
     JClassType[] typeArgs = uiBinderType.isParameterized().getTypeArgs();
-    if (typeArgs.length < 2) {
-      throw new RuntimeException(
-          "Root and owner type parameters are required for type %s"
-              + uiBinderType.getName());
+
+    String binderType = uiBinderType.getName();
+
+    JClassType uiRendererClass = getOracle().findType(UiRenderer.class.getName());
+    if (uiBinderType.isAssignableTo(uibinderItself)) {
+      if (typeArgs.length < 2) {
+        throw new RuntimeException(
+            "Root and owner type parameters are required for type %s"
+                + binderType);
+      }
+      uiRootType = typeArgs[0];
+      uiOwnerType = typeArgs[1];
+      isRenderer = false;
+    } else if (uiBinderType.isAssignableTo(uiRendererClass)) {
+      if (typeArgs.length < 1) {
+        throw new RuntimeException(
+            "Owner type parameter is required for type %s"
+                + binderType);
+      }
+      if (!useSafeHtmlTemplates) {
+        die("Configuration property UiBinder.useSafeHtmlTemplates\n"
+            + "  must be set to true to generate a UiRenderer");
+      }
+      if (!useLazyWidgetBuilders) {
+        die("Configuration property UiBinder.useLazyWidgetBuilders\n"
+            + "  must be set to true to generate a UiRenderer");
+      }
+
+      uiOwnerType = typeArgs[0];
+      uiRootType = null;
+      isRenderer = true;
+    } else {
+      die(baseClass.getName() + " must implement UiBinder or UiRenderer");
+      // This is unreachable in practice, but silences not initialized errors
+      throw new UnableToCompleteException();
     }
-    uiRootType = typeArgs[0];
-    uiOwnerType = typeArgs[1];
 
     isRenderableClassType = oracle.findType(IsRenderable.class.getCanonicalName());
     lazyDomElementClass = oracle.findType(LazyDomElement.class.getCanonicalName());
@@ -485,13 +516,14 @@
    * @return The invocation of the SafeHtml template function with the arguments
    * filled in
    */
-  public String declareTemplateCall(String html)
+  public String declareTemplateCall(String html, String fieldName)
     throws IllegalArgumentException {
     if (!useSafeHtmlTemplates) {
       return '"' + html + '"';
     }
-
-    return htmlTemplates.addSafeHtmlTemplate(html, tokenator);
+    FieldWriter w = fieldManager.lookup(fieldName);
+    w.setHtml(htmlTemplates.addSafeHtmlTemplate(html, tokenator));
+    return w.getHtml();
   }
 
   /**
@@ -940,11 +972,6 @@
    */
   void parseDocument(Document doc, PrintWriter printWriter)
       throws UnableToCompleteException {
-    JClassType uiBinderClass = getOracle().findType(UiBinder.class.getName());
-    if (!baseClass.isAssignableTo(uiBinderClass)) {
-      die(baseClass.getName() + " must implement UiBinder");
-    }
-
     Element documentElement = doc.getDocumentElement();
     gwtPrefix = documentElement.lookupPrefix(binderUri);
 
@@ -1201,7 +1228,9 @@
     IndentedWriter niceWriter = new IndentedWriter(
         new PrintWriter(stringWriter));
 
-    if (useLazyWidgetBuilders) {
+    if (isRenderer) {
+      writeRenderer(niceWriter, rootField);
+    } else if (useLazyWidgetBuilders) {
       for (ImplicitCssResource css : bundleClass.getCssMethods()) {
         String fieldName = css.getName();
         FieldWriter cssField = fieldManager.require(fieldName);
@@ -1211,7 +1240,6 @@
     } else {
       writeBinder(niceWriter, rootField);
     }
-
     ensureAttachmentCleanedUp();
     return stringWriter.toString();
   }
@@ -1408,10 +1436,16 @@
   }
 
   private void writeClassOpen(IndentedWriter w) {
-    w.write("public class %s implements UiBinder<%s, %s>, %s {", implClassName,
-        uiRootType.getParameterizedQualifiedSourceName(),
-        uiOwnerType.getParameterizedQualifiedSourceName(),
-        baseClass.getParameterizedQualifiedSourceName());
+    if (!isRenderer) {
+      w.write("public class %s implements UiBinder<%s, %s>, %s {", implClassName,
+          uiRootType.getParameterizedQualifiedSourceName(),
+          uiOwnerType.getParameterizedQualifiedSourceName(),
+          baseClass.getParameterizedQualifiedSourceName());
+    } else {
+      w.write("public class %s extends AbstractSafeHtmlRenderer<%s> implements %s {", implClassName,
+          uiOwnerType.getParameterizedQualifiedSourceName(),
+          baseClass.getParameterizedQualifiedSourceName());
+    }
     w.indent();
   }
 
@@ -1455,7 +1489,6 @@
       }
     }
 
-    // Write gwt field declarations.
     fieldManager.writeGwtFieldsDeclaration(niceWriter, uiOwnerType.getName());
   }
 
@@ -1473,11 +1506,18 @@
       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.safehtml.shared.SafeHtmlBuilder;");
+      w.write("import com.google.gwt.uibinder.client.UiBinderUtil;");
     }
-    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(),
-        uiRootType.getName());
+
+    if (!isRenderer) {
+      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(),
+          uiRootType.getName());
+    } else {
+      w.write("import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;");
+    }
   }
 
   /**
@@ -1540,6 +1580,76 @@
   }
 
   /**
+   * Writes the SafeHtmlRenderer's source for the renderable
+   * strategy.
+   */
+  private void writeRenderer(
+      IndentedWriter w, String rootField) throws UnableToCompleteException {
+    writePackage(w);
+
+    writeImports(w);
+    w.newline();
+
+    writeClassOpen(w);
+    writeStatics(w);
+    w.newline();
+
+    // Create SafeHtml Template
+    writeSafeHtmlTemplates(w);
+
+    w.newline();
+
+    w.write("public SafeHtml render(final %s owner) {",
+        uiOwnerType.getParameterizedQualifiedSourceName());
+    w.indent();
+    w.newline();
+
+    w.write("return new Widgets(owner).getSafeHtml();");
+    w.outdent();
+
+    w.write("}");
+    w.newline();
+
+    // Writes the inner class Widgets.
+    w.newline();
+    w.write("/**");
+    w.write(" * Encapsulates the access to all inner widgets");
+    w.write(" */");
+    w.write("class Widgets {");
+    w.indent();
+
+    String ownerClassType = uiOwnerType.getParameterizedQualifiedSourceName();
+    w.write("private final %s owner;", ownerClassType);
+    w.newline();
+
+    w.write("public Widgets(final %s owner) {", ownerClassType);
+    w.indent();
+    w.write("this.owner = owner;");
+    fieldManager.initializeWidgetsInnerClass(w, getOwnerClass());
+    w.outdent();
+    w.write("}");
+    w.newline();
+
+    w.write("public SafeHtml getSafeHtml() {");
+    w.indent();
+    // TODO Find a better way to get the root field name
+    String safeHtml = fieldManager.lookup(rootField.substring(4, rootField.length() - 2)).getSafeHtml();
+    w.write("return %s;", safeHtml);
+    w.outdent();
+    w.write("}");
+
+    fieldManager.writeFieldDefinitions(
+        w, getOracle(), getOwnerClass(), getDesignTime());
+
+    w.outdent();
+    w.write("}");
+
+    // Close class
+    w.outdent();
+    w.write("}");
+  }
+
+  /**
    * Write statements created by {@link HtmlTemplates#addSafeHtmlTemplate}. This
    * code must be placed after all instantiation code.
    */
diff --git a/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java b/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java
index f8dd711..f8c4994 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java
@@ -77,7 +77,7 @@
    */
   public String writeTemplateCall() {
     return "template." + methodName + "(" + getSafeHtmlArgs() 
-      + ").asString()";
+      + ")";
   }
 
   /**
diff --git a/user/test/com/google/gwt/uibinder/LazyWidgetBuilderSuite.java b/user/test/com/google/gwt/uibinder/LazyWidgetBuilderSuite.java
index 6c69e98..a5f1303 100644
--- a/user/test/com/google/gwt/uibinder/LazyWidgetBuilderSuite.java
+++ b/user/test/com/google/gwt/uibinder/LazyWidgetBuilderSuite.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.junit.tools.GWTTestSuite;
 import com.google.gwt.uibinder.test.client.SafeHtmlAsComponentsTest;
+import com.google.gwt.uibinder.test.client.UiRendererTest;
 
 import junit.framework.Test;
 
@@ -29,6 +30,7 @@
         "Tests that rely on the useLazyWidgetBuilders switch");
 
     suite.addTestSuite(SafeHtmlAsComponentsTest.class);
+    suite.addTestSuite(UiRendererTest.class);
 
     return suite;
   }
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
index 4dce3fa..2cdc6af 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
@@ -77,7 +77,8 @@
     b.append("</g:DialogBox> ");
 
     String[] expected = {
-        "fieldName.setHTML(\"@mockToken-Hello, I <b>caption</b>you.\");",
+        "fieldName.setHTML(\"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-Hello, I <b>caption</b>you.\");",
         "fieldName.setWidget(<g:Label>);",};
 
     FieldWriter w = tester.parse(b.toString());
@@ -342,7 +343,8 @@
     b.append("</ui:UiBinder>");
 
     String[] expected = {
-        "fieldName.setHTML(\"@mockToken-Hello, I <b>caption</b>you.\");",
+        "fieldName.setHTML(\"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-Hello, I <b>caption</b>you.\");",
         "fieldName.setWidget(<g:Label>);",};
 
     parser.parse(tester.getElem(b.toString(), "my:MyDialogBox"), "fieldName",
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
index 5a55684..ea11f9e 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
@@ -132,12 +132,16 @@
 
     String[] expected = {"fieldName.resize(2, 2);",
         "fieldName.getRowFormatter().setStyleName(0, \"rowHeaderStyle\");",
-        "fieldName.setHTML(0, 0, \"@mockToken-foo\");",
+        "fieldName.setHTML(0, 0, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-foo\");",
         "fieldName.getCellFormatter().setStyleName(0, 0, \"headerStyle\");",
-        "fieldName.setHTML(0, 1, \"@mockToken-bar\");",
+        "fieldName.setHTML(0, 1, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-bar\");",
         "fieldName.getCellFormatter().setStyleName(0, 1, \"headerStyle\");",
-        "fieldName.setHTML(1, 0, \"@mockToken-foo\");",
-        "fieldName.setHTML(1, 1, \"@mockToken-bar\");"};
+        "fieldName.setHTML(1, 0, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-foo\");",
+        "fieldName.setHTML(1, 1, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-bar\");"};
 
     FieldWriter w = tester.parse(b.toString());
 
@@ -172,8 +176,10 @@
 
     String[] expected = {
         "fieldName.resize(2, 2);",
-        "fieldName.setHTML(0, 0, \"@mockToken-<div>foo HTML element</div>\");",
-        "fieldName.setHTML(0, 1, \"@mockToken-<div>bar HTML element</div>\");",
+        "fieldName.setHTML(0, 0, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-<div>foo HTML element</div>\");",
+        "fieldName.setHTML(0, 1, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-<div>bar HTML element</div>\");",
         "fieldName.setWidget(1, 0, <g:Label>);",
         "fieldName.setWidget(1, 1, <g:Label>);"};
 
@@ -209,7 +215,8 @@
 
     String[] expected = {
         "fieldName.resize(2, 2);",
-        "fieldName.setHTML(0, 0, \"@mockToken-<div>foo HTML element</div>\");",
+        "fieldName.setHTML(0, 0, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-<div>foo HTML element</div>\");",
         "fieldName.setWidget(1, 0, <g:Label>);",
         "fieldName.setWidget(1, 1, <g:Label>);"};
 
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java b/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java
index bcdd90b..2bbe7cf 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java
@@ -45,11 +45,12 @@
   }
 
   /**
-   * Mocked out version of the template declaration. Returns the template
-   * prefixed with "@mockToken-"
+   * Mocked out version of the template declaration. Returns the fieldName and
+   * template separated with a dash, all prefixed with "@mockToken-"
    */
-  public String declareTemplateCall(String html) {
-    return "\"@mockToken-" + html + "\"";
+  @Override
+  public String declareTemplateCall(String html, String fieldName) {
+    return "\"@mockToken-" + fieldName + "-" + html + "\"";
   }
 
   @Override
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
index 6987761..6f41f5e 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
@@ -165,7 +165,8 @@
         + "(com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
 
     assertStatements(
-        "fieldName.add(<g:Label id='able'>, \"@mockToken-Re<b>mark</b>able\", true, 3);",
+        "fieldName.add(<g:Label id='able'>, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-Re<b>mark</b>able\", true, 3);",
         "fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>, 3);");
   }
 
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
index 4bc18ec..1b49860 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
@@ -168,7 +168,8 @@
     b.append("</g:TabLayoutPanel>");
 
     String[] expected = {
-        "fieldName.add(<g:Label id='able'>, \"@mockToken-Re<b>mark</b>able\", true);",
+        "fieldName.add(<g:Label id='able'>, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-Re<b>mark</b>able\", true);",
         "fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>);",};
 
     FieldWriter w = tester.parse(b.toString());
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java
index 2fc5ed7..1117c95 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java
@@ -146,7 +146,8 @@
     tester.parse(b.toString());
 
     assertStatements("fieldName.add(<g:Label id='0'>, \"Foo\");",
-        "fieldName.add(<g:Label id='1'>, \"@mockToken-B<b>a</b>r\", true);");
+        "fieldName.add(<g:Label id='1'>, \"@mockToken-" + ElementParserTester.FIELD_NAME
+            + "-B<b>a</b>r\", true);");
   }
 
   private void assertStatements(String... expected) {
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java b/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java
index f0a063f..8748ecd 100644
--- a/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java
+++ b/user/test/com/google/gwt/uibinder/test/client/UiBinderTest.java
@@ -22,8 +22,8 @@
 import com.google.gwt.dom.client.SpanElement;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.resources.client.CssResource.NotStrict;
+import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.uibinder.test.client.EnumeratedLabel.Suffix;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.AbsolutePanel;
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java b/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java
new file mode 100644
index 0000000..15eabe0
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.test.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.LabelElement;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.safehtml.shared.SafeHtml;
+
+/**
+ * Functional test of UiBinder.
+ */
+public class UiRendererTest extends GWTTestCase {
+  private UiRendererUi safeHtmlUi;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.uibinder.test.LazyWidgetBuilderSuite";
+  }
+
+  @Override
+  public void gwtSetUp() throws Exception {
+    super.gwtSetUp();
+    UiRendererTestApp app = UiRendererTestApp.getInstance();
+    safeHtmlUi = app.getSafeHtmlUi();
+  }
+
+  public void testSafeHtmlRendererText() {
+    SafeHtml render = safeHtmlUi.render();
+
+    LabelElement renderedHtml = Document.get().createLabelElement();
+    renderedHtml.setInnerHTML(render.asString());
+
+    Node innerDiv = renderedHtml.getFirstChild();
+
+    // Was the first span rendered as a "HTML-safe" text string?
+    Node spanWithConstantTextNode = innerDiv.getChild(0);
+    assertEquals("span", spanWithConstantTextNode.getNodeName().toLowerCase());
+    assertEquals(Node.TEXT_NODE, spanWithConstantTextNode.getFirstChild().getNodeType());
+    assertEquals("<b>This text won't be bold!</b>",
+        spanWithConstantTextNode.getFirstChild().getNodeValue());
+
+    Node firstRawTextNode = innerDiv.getChild(1);
+    assertEquals(Node.TEXT_NODE, firstRawTextNode.getNodeType());
+    assertEquals(" Hello, ", firstRawTextNode.getNodeValue());
+
+    // Fields not present in owning class produce no content
+    Node firstFieldNode = innerDiv.getChild(2);
+    assertEquals(Node.ELEMENT_NODE, firstFieldNode.getNodeType());
+    assertEquals("span", firstFieldNode.getNodeName().toLowerCase());
+    assertFalse(firstFieldNode.hasChildNodes());
+
+    // ui:msg tags get rendered but the "<ui:msg>" tag is not
+    Node secondRawTextNode = innerDiv.getChild(3);
+    assertEquals(Node.TEXT_NODE, secondRawTextNode.getNodeType());
+    assertEquals(". How goes it? ", secondRawTextNode.getNodeValue());
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererTestApp.java b/user/test/com/google/gwt/uibinder/test/client/UiRendererTestApp.java
new file mode 100644
index 0000000..5c9de7a
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererTestApp.java
@@ -0,0 +1,57 @@
+/*
+ * 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.test.client;
+
+import com.google.gwt.core.client.EntryPoint;
+
+/**
+ * Demonstration of templated UI. Used by UiRendererTest
+ */
+public class UiRendererTestApp implements EntryPoint {
+  private static UiRendererTestApp instance;
+
+  /**
+   * Ensure the singleton instance has installed its UI, and return it.
+   */
+  public static UiRendererTestApp getInstance() {
+    if (instance == null) {
+      setAndInitInstance(new UiRendererTestApp());
+    }
+
+    return instance;
+  }
+
+  private static void setAndInitInstance(UiRendererTestApp newInstance) {
+    instance = newInstance;
+    instance.safeHtmlUi = new UiRendererUi();
+  }
+
+  private UiRendererUi safeHtmlUi;
+
+  private UiRendererTestApp() {
+  }
+
+  public UiRendererUi getSafeHtmlUi() {
+    return safeHtmlUi;
+  }
+
+  /**
+   * Entry point method, called only when this is run as an application.
+   */
+  public void onModuleLoad() {
+    setAndInitInstance(this);
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.css b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.css
new file mode 100644
index 0000000..d277a99
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.css
@@ -0,0 +1,8 @@
+.bodyColor {
+  color: indigo;
+}
+
+.bodyFont {
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: small;
+}
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java
new file mode 100644
index 0000000..0b26d06
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java
@@ -0,0 +1,54 @@
+/*
+ * 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.test.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.text.shared.UiRenderer;
+
+/**
+ * Sample use of a {@code SafeHtmlRenderer} with no dependency on
+ * com.google.gwt.user.
+ */
+public class UiRendererUi {
+  /**
+   * Resources for this template.
+   */
+  public interface Resources extends ClientBundle {
+    @Source("UiRendererUi.css")
+    Style style();
+  }
+
+  /**
+   * CSS for this template.
+   */
+  public interface Style extends CssResource {
+    String bodyColor();
+    String bodyFont();
+  }
+
+  interface HtmlRenderer extends UiRenderer<String> { }
+  private static final HtmlRenderer renderer = GWT.create(HtmlRenderer.class);
+
+  public UiRendererUi() {
+  }
+
+  public SafeHtml render() {
+    return renderer.render(null);
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml
new file mode 100644
index 0000000..2dea655
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml
@@ -0,0 +1,47 @@
+<!--                                                                        -->
+<!-- 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:res='urn:with:com.google.gwt.uibinder.test.client.DomBasedUi.Resources'
+  >
+  <ui:with field="constants" type="com.google.gwt.uibinder.test.client.Constants"/>
+  <div ui:field='root' res:class="style.bodyColor style.bodyFont"
+      title="The title of this div is localizable">
+    <ui:attribute name='title'/>
+    <span><ui:text from="{constants.getText}" /></span>
+    Hello, <span ui:field="nameSpan" />.
+    <ui:msg>How goes it?</ui:msg>
+      <h2 res:class="style.bodyColor style.bodyFont">Placeholders in localizables</h2>
+      <p><ui:msg>When you mark up your text for translation, there will be bits
+      that you don't want the translators to mess with. You can protect
+      these with <span style="font-weight:bold"
+        ui:ph="boldSpan">placeholders</span><ui:ph name="tm"><sup ui:field="tmElement">TM</sup></ui:ph>.</ui:msg></p>
+  <table>
+    <col ui:field='narrowColumn' width='0%'></col>
+    <col width='100%'></col>
+    <tr ui:field='tr'>
+      <th ui:field='th1'>Tables with col elements</th>
+      <th ui:field='th2' align='left'>are</th>
+      <th ui:field='th3' align='left'>tricky</th>
+    </tr>
+  </table>
+  <table>
+    <tbody ui:field='tbody'>
+    <tr ui:field='tr2'>
+      <th ui:field='th4'>Tables with tbody elements too</th>
+    </tr>
+    </tbody>
+  </table>
+  </div>
+</ui:UiBinder>