SafeHtml Templates

The SafeHtml Templates were removed from the main SafeHtml review, but are being added again here so that widgets can start using this api, even while a newer parser is in the works. This is a first pass at templates, and the api/usage are up for review.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8657 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml b/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml
index 9721d70..5c2d658 100644
--- a/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml
+++ b/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml
@@ -20,4 +20,7 @@
   <inherits name='com.google.gwt.user.User'/>
   <source path="client"/>
   <source path="shared"/>
+  <generate-with class="com.google.gwt.safehtml.rebind.SafeHtmlTemplatesGenerator">
+    <when-type-assignable class="com.google.gwt.safehtml.client.SafeHtmlTemplates"/>
+  </generate-with>
 </module>
diff --git a/user/src/com/google/gwt/safehtml/client/SafeHtmlTemplates.java b/user/src/com/google/gwt/safehtml/client/SafeHtmlTemplates.java
new file mode 100644
index 0000000..858d0b1
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/client/SafeHtmlTemplates.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010 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.safehtml.client;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A tag interface that facilitates compile-time binding of HTML templates to
+ * generate SafeHtml strings.
+ * 
+ * <p>Example usage:
+ * <pre>
+ *   public interface MyTemplate extends SafeHtmlTemplates {
+ *     @Template("<span class=\"{3}\">{0}: <a href=\"{1}\">{2}</a></span>")
+ *     SafeHtml messageWithLink(SafeHtml message, String url, String linkText,
+ *       String style);
+ *   }
+ *   private static final MyTemplate TEMPLATE = GWT.create(MyTemplate.class);
+ *   public void useTemplate(...) {
+ *     SafeHtml message;
+ *     String url;
+ *     String linkText;
+ *     String style;
+ *     // ...
+ *     SafeHtml messageWithLink =
+ *       TEMPLATE.messageWithLink(message, url, linkText, style);
+ *   }
+ * </pre>
+ * 
+ * Instantiating a SafeHtmlTemplates interface with GWT.create() returns an
+ * instance of an implementation that is generated at compile time. The code
+ * generator parses the value of each template method's @Template annotation as 
+ * a (X)HTML template, with template variables denoted by curly-brace 
+ * placeholders that refer by index to the corresponding template method 
+ * parameter.
+ */
+public interface SafeHtmlTemplates {
+
+  /**
+   * The HTML template.
+   */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.METHOD)
+  @Documented
+  public @interface Template {
+    String value();
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java b/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java
new file mode 100644
index 0000000..8db7e1e
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/rebind/HtmlTemplateParser.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.util.Preconditions;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotSupportedException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A HTML context-aware parser for a simple HTML template language.
+ * 
+ * <p>This parser parses templates consisting of well-formed XML or XHTML
+ * markup, with template parameters of the form {@code "{n}"}. For example, a
+ * template might look like,
+ * <pre>  {@code
+ *   <span class="{0}"><a href="{1}/{2}">{3}</a></span>
+ * }</pre>
+ *
+ * <p>The parser produces a parsed form of the template (returned as a
+ * {@link ParsedHtmlTemplate}) consisting of a sequence of chunks
+ * corresponding to the literal strings and parameters of the template.  The
+ * parser is HTML context aware and tags each parameter with its parameter index
+ * as well as a {@link HtmlContext} that corresponds to the HTML context in
+ * which the parameter occurs in the template.
+ *
+ * <p>The following contexts are recognized and instantiated:
+ * <dl>
+ *   <dt>{@link HtmlContext.Type#TEXT}
+ *   <dd>This context corresponds to basic inner text. In the above example,
+ *       parameter #3 would be tagged with this context.
+ *   <dt>{@link HtmlContext.Type#ATTRIBUTE_START}
+ *   <dd>This context corresponds to a parameter that appears at the very start
+ *       of a HTML attribute's value; in the above example this applies to
+ *       parameters #0 and #1.
+ *   <dt>{@link HtmlContext.Type#ATTRIBUTE}
+ *   <dd>This context corresponds to a parameter that appears within an
+ *       attribute in a position other than at the start of the attribute's
+ *       value. In the above example, this applies to parameter #2.
+ * </dl>
+ *
+ * <p>For both attribute contexts, the {@code tag} and {@code attribute}
+ * properties of the context are set to the name of the enclosing tag and
+ * attribute, respectively.
+ *
+ * <p>Tag and attribute names are converted to lower-case in {@link HtmlContext}
+ * properties and literal string chunks of the parsed form.
+ *
+ * <p>The implementation is subject to the following limitations:
+ * <ul>
+ *   <li>The input template must be well-formed XML/XHTML. If it is not,
+ *       a {@link UnableToCompleteException} is thrown and details regarding
+ *       the source of the parse failure are logged to this parser's logger.
+ *   <li>Template parameters can only appear within inner text and within
+ *       attributes. In particular, parameters cannot appear within a HTML
+ *       tag or attribute name; for example, the following is not a valid
+ *       template:
+ *       <pre>  {@code
+ *         <span><{0} class="xyz" {1}="..."/></span>
+ *       }</pre>
+ *   <li>The output markup will contain separate closing tags for tags without
+ *       content. I.e., an input template
+ *       <pre>  {@code
+ *         <img src="..."/>
+ *       }</pre>
+ *       will result in the corresponding output
+ *       <pre>  {@code
+ *         <img src="..."></img>
+ *       }</pre>
+ *   <li>There is no escaping mechanism for the parameter syntax, i.e. it is
+ *       impossible to write a template that results in a literal output chunk
+ *       containing a substring of the form "{@code {0}}".
+ * </ul>
+ */
+final class HtmlTemplateParser {
+
+  /**
+   * A SAX parser event handler for parsing HTML templates.
+   */
+  private class HtmlTemplateHandler extends DefaultHandler {
+
+    /*
+     * Overrides for relevant SAX event handler methods.
+     */
+
+    @Override
+    public void characters(char[] ch, int start, int length) {
+      parseTemplateString(
+          new HtmlContext(HtmlContext.Type.TEXT),
+          SafeHtmlUtils.htmlEscape(new String(ch, start, length)));
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String name) {
+      Preconditions.checkArgument(uri.equals(""),
+          "Namespace uri unexpectedly non-empty: %s", uri);
+      appendLiteral("</" + name.toLowerCase() + ">");
+    }
+
+    @Override
+    public void endPrefixMapping(String prefix) throws SAXException {
+      throw unsupportedError("Prefix Mapping");
+    }
+
+    @Override
+    public void error(SAXParseException e) {
+      getLogger().log(TreeLogger.ERROR, "Parser error: " + e);
+    }
+
+    @Override
+    public void fatalError(SAXParseException e) throws SAXException {
+      getLogger().log(TreeLogger.ERROR, "Parser fatal error: " + e);
+      throw e;
+    }
+
+    /*
+     * Throw errors on various irrelevant SAX events that we don't want to
+     * handle, and which should not occur in templates.
+     * 
+     * It may be reasonable to just silently ignore these events, but failing
+     * explicitly seems more helpful to developers.
+     */
+    
+    @Override
+    public void notationDecl(String name, String publicId, String systemId)
+        throws SAXException {
+      throw unsupportedError("Notation Declaration");
+    }
+
+    @Override
+    public void processingInstruction(String target, String data)
+        throws SAXException {
+      throw unsupportedError("Processing Instruction");
+    }
+
+    @Override
+    public InputSource resolveEntity(String publicId, String systemId)
+        throws SAXException {
+      throw unsupportedError("External Entity");
+    }
+
+    @Override
+    public void skippedEntity(String name) throws SAXException {
+      throw unsupportedError("Skipped Entity");
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String name,
+        Attributes attributes) {
+      Preconditions.checkArgument(uri.equals(""),
+          "Namespace uri unexpectedly non-empty: %s", uri);
+
+      name = name.toLowerCase();
+      appendLiteral("<" + name);
+      if (attributes != null) {
+        int len = attributes.getLength();
+        for (int i = 0; i < len; i++) {
+          String attribute = attributes.getQName(i).toLowerCase();
+          appendLiteral(" " + attribute + "=\"");
+          parseTemplateString(
+              new HtmlContext(HtmlContext.Type.ATTRIBUTE, name, attribute),
+              new HtmlContext(HtmlContext.Type.ATTRIBUTE_START,
+                  name, attribute),
+                  SafeHtmlUtils.htmlEscape(attributes.getValue(i)));
+          appendLiteral("\"");
+        }
+      }
+      appendLiteral(">");
+    }
+
+    /*
+     * Error handlers.
+     */
+
+    @Override
+    public void startPrefixMapping(String prefix, String uri)
+        throws SAXException {
+      throw unsupportedError("Prefix Mapping");
+    }
+
+    @Override
+    public void unparsedEntityDecl(String name, String publicId,
+        String systemId, String notationName) throws SAXException {
+      throw unsupportedError("Unparsed Entity Declaration");
+    }
+
+    @Override
+    public void warning(SAXParseException e) {
+      getLogger().log(TreeLogger.WARN, "Parser warning: " + e);
+    }
+
+    /**
+     * Returns exception for unsupported event in SafeHtmlTemplates.
+     * 
+     * <p>
+     * Returns an exception indicating that the event in question is not
+     * supported in SafeHtmlTemplates.
+     * 
+     * @param what unsupported SAX event that should not occur in templates
+     * @return exception stating that the event is not allowed
+     */
+    private SAXNotSupportedException unsupportedError(String what) {
+      return new SAXNotSupportedException(
+          "Not allowed in SafeHtmlTemplates: " + what);
+    }
+  }
+
+  /**
+   * Pattern to find template parameters references.
+   */
+  private static final Pattern TEMPLATE_PARAM_PATTERN =
+      Pattern.compile("\\{(\\d+)\\}");
+
+  private final TreeLogger logger;
+
+  private final ParsedHtmlTemplate parsedTemplate;
+
+  /**
+   * Creates a {@link HtmlTemplateParser}.
+   *
+   * @param logger the {@link TreeLogger} to log to
+   */
+  public HtmlTemplateParser(TreeLogger logger) {
+    this.logger = logger;
+    this.parsedTemplate = new ParsedHtmlTemplate();
+  }
+
+  /**
+   * Returns this parser's logger.
+   */
+  public TreeLogger getLogger() {
+    return logger;
+  }
+
+  /**
+   * Returns the parsed representation of the template.
+   */
+  public ParsedHtmlTemplate getParsedTemplate() {
+    return parsedTemplate;
+  }
+
+  /**
+   * Parses a XML/XHTML document.
+   *
+   * @param input a {@link Reader} from which the document to be parsed will be
+   *        read.
+   * @throws UnableToCompleteException if the template cannot be parsed. Details
+   *         on the source of the failure will have been logged to this parser's
+   *         logger.
+   */
+  public void parseXHtml(Reader input) throws UnableToCompleteException {
+    HtmlTemplateHandler saxEventHandler = new HtmlTemplateHandler();
+    XMLReader xmlParser;
+    try {
+      xmlParser = XMLReaderFactory.createXMLReader();
+    } catch (SAXException e) {
+      logger.log(TreeLogger.ERROR, "Couldn't instantiate XML parser", e);
+      throw new UnableToCompleteException();
+    }
+
+    xmlParser.setContentHandler(saxEventHandler);
+    xmlParser.setDTDHandler(saxEventHandler);
+    xmlParser.setEntityResolver(saxEventHandler);
+    xmlParser.setErrorHandler(saxEventHandler);
+
+    try {
+      xmlParser.parse(new InputSource(input));
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Error during template parsing:", e);
+      throw new UnableToCompleteException();
+    } catch (SAXParseException e) {
+      String logMessage = "Parse Error during template parsing, at line "
+          + e.getLineNumber() + ", column " + e.getColumnNumber();
+      // Attempt to extract (some) of the input to provide a more useful 
+      // error message.
+      try {
+        input.reset();
+        char[] buf = new char[200];
+        int len = input.read(buf);
+        if (len > 0) {
+          logMessage += " of input " + new String(buf, 0, len); 
+        }
+      } catch (IOException e1) {
+        // We tried, but resetting/reading from the input stream failed. Sorry.
+        logMessage += " <failed to read input snippet>";
+      }
+      logger.log(TreeLogger.ERROR, logMessage + ": " + e);
+      throw new UnableToCompleteException();
+    } catch (SAXException e) {
+      logger.log(TreeLogger.ERROR, "Error during template parsing:", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Parses a {@link String} that may contain template parameters of the form
+   * {@code {n}} into corresponding literal and parameter
+   * {@link ParsedHtmlTemplate.TemplateChunk}s.
+   *
+   * <p>Parameters will be tagged with the {@link HtmlContext} provided.
+   *
+   * <p>If {@code contextAtStart} is not {@code null} and the parsed template
+   * starts with a parameter (i.e., is of the form {@code "{n}..."}), this first
+   * parameter will be tagged with that context; any other parameters will
+   * be tagged with context {@code context}.
+   *
+   * @param context the context with which to tag parameters occurring in the
+   *        template
+   * @param contextAtStart if not {@code null}, the context with which to tag a
+   *        parameter that occurs at the very beginning of the template
+   * @param template the template {@link String} to parse
+   */
+  // @VisibleForTesting
+  void parseTemplateString(HtmlContext context,
+      HtmlContext contextAtStart, String template) {
+    Matcher match = TEMPLATE_PARAM_PATTERN.matcher(template);
+
+    boolean firstMatch = true;
+    int endOfLastMatch = 0;
+    while (match.find()) {
+      if (match.start() > endOfLastMatch) {
+        // There is a non-empty string between the previous match and this
+        // match; add this as a literal chunk to the parsed representation.
+        appendLiteral(template.substring(endOfLastMatch, match.start()));
+      }
+
+      int paramIndex = Integer.parseInt(match.group(1));
+      if (firstMatch && (match.start() == 0) && (contextAtStart != null)) {
+        parsedTemplate.addParameter(new ParameterChunk(contextAtStart,
+            paramIndex));
+      } else {
+        parsedTemplate.addParameter(new ParameterChunk(context, paramIndex));
+      }
+
+      firstMatch = false;
+      endOfLastMatch = match.end();
+    }
+
+    // Add a literal chunk for the substring after the last match, if any.
+    if (endOfLastMatch < template.length()) {
+      parsedTemplate.addLiteral(template.substring(endOfLastMatch));
+    }
+  }
+
+  /**
+   * Parses a {@link String} that may contain template parameters of the form
+   * {@code {n}} into corresponding literal and parameter
+   * {@link ParsedHtmlTemplate.TemplateChunk}s.
+   *
+   * <p>Parameters will be tagged with the {@link HtmlContext} provided.
+   *
+   * @param context the context with which to tag parameters occurring in the
+   *        template
+   * @param template the template to parse
+   */
+  // @VisibleForTesting
+  void parseTemplateString(HtmlContext context, String template) {
+    parseTemplateString(context, null, template);
+  }
+
+  /**
+   * Appends a literal string to the parsed template representation.
+   *
+   * <p>The {@code literal} will be appended without processing; any XML/XHTML
+   * markup as well as template parameters occurring in the {@code literal} will
+   * not be parsed.
+   *
+   * @param literal the string to append
+   */
+  private void appendLiteral(String literal) {
+    parsedTemplate.addLiteral(literal);
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java b/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java
new file mode 100644
index 0000000..15ec091
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/rebind/ParsedHtmlTemplate.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.dev.util.Preconditions;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A representation of a parsed (X)HTML template.
+ *
+ * <p>A parsed template is represented as a sequence of template chunks.
+ *
+ * <p>A chunk may correspond to a literal string or to a formal template
+ * parameter.
+ *
+ * <p>Parameter chunks have an attribute that refers to the index of the
+ * corresponding template method parameter, as well as an attribute that
+ * represents the HTML context the template parameter appeared in.
+ */
+final class ParsedHtmlTemplate {
+
+  /**
+   * A representation of the context of a point within a HTML document. A
+   * context consists of a type (such as "plain text", "attribute"), as well as
+   * a HTML tag and attribute name where applicable.
+   */
+  static final class HtmlContext {
+    /**
+     * The possible types of HTML context.
+     */
+    // TODO(xtof): Possibly add support for other contexts, such as style.
+    static enum Type {
+      /**
+       * The undefined HTML context. Contexts of this type are meant to be used
+       * in cases where a parser is used that is not HTML context-aware.
+       */
+      UNDEFINED,
+      /**
+       * Regular inner text HTML context.
+       */
+      TEXT,
+      /**
+       * HTML attribute context.
+       */
+      ATTRIBUTE,
+      /**
+       * At the very start of an attribute.
+       */
+      ATTRIBUTE_START,
+    }
+
+    private final Type type;
+    private final String tag;
+    private final String attribute;
+
+    /**
+     * Creates a HTML context.
+     *
+     * @param type the {@link Type} of this context
+     */
+    public HtmlContext(Type type) {
+      this(type, null, null);
+    }
+
+    /**
+     * Creates a HTML context.
+     *
+     * @param type the {@link Type} of this context
+     * @param tag the HTML tag this context corresponds to, if applicable; null
+     *        otherwise
+     * @param attribute the HTML attribute this context corresponds to, if
+     *        applicable; null otherwise
+     */
+    public HtmlContext(Type type, String tag, String attribute) {
+      Preconditions.checkArgument((type != Type.UNDEFINED)
+          || ((tag == null) && (attribute == null)),
+          "tag and attribute must be null for context \"UNDEFINED\"");
+      Preconditions.checkArgument((type != Type.TEXT)
+          || ((tag == null) && (attribute == null)),
+          "tag and attribute must be null for context \"TEXT\"");
+      this.type = type;
+      this.tag = tag;
+      this.attribute = attribute;
+    }
+
+    /**
+     * Returns the attribute of this HTML context.
+     */
+    public String getAttribute() {
+      return attribute;
+    }
+
+    /**
+     * Returns the tag of this HTML context.
+     */
+    public String getTag() {
+      return tag;
+    }
+
+    /**
+     * Returns the type of this HTML context.
+     */
+    public Type getType() {
+      return type;
+    }
+
+    @Override
+    public String toString() {
+      return "(" + getType() + "," + getTag() + "," + getAttribute() + ")";
+    }
+  }
+
+  /**
+   * Represents a template chunk corresponding to a literal string.
+   */
+  static final class LiteralChunk implements TemplateChunk {
+    private final StringBuilder chunk;
+
+    /**
+     * Creates a literal chunk corresponding to an empty string.
+     */
+    public LiteralChunk() {
+      chunk = new StringBuilder();
+    }
+
+    /**
+     * Creates a literal chunk corresponding to the provided string.
+     */
+    public LiteralChunk(String literal) {
+      chunk = new StringBuilder(literal);
+    }
+
+    public Kind getKind() {
+      return Kind.LITERAL;
+    }
+
+    public String getLiteral() {
+      return chunk.toString();
+    }
+
+    @Override
+    public String toString() {
+      return String.format("L(%s)", getLiteral());
+    }
+
+    void append(String s) {
+      chunk.append(s);
+    }
+  }
+
+  /**
+   * Represents a template chunk corresponding to a template parameter.
+   */
+  static final class ParameterChunk implements TemplateChunk {
+
+    private final HtmlContext context;
+    private final int parameterIndex;
+
+    /**
+     * Creates a parameter chunk.
+     *
+     * @param context the HTML context this parameter appears in
+     * @param parameterIndex the index of the template parameter that this chunk
+     *        refers to
+     */
+    public ParameterChunk(HtmlContext context, int parameterIndex) {
+      this.context = context;
+      this.parameterIndex = parameterIndex;
+    }
+
+    /**
+     * Returns the HTML context this parameter appears in.
+     */
+    public HtmlContext getContext() {
+      return context;
+    }
+
+    public Kind getKind() {
+      return Kind.PARAMETER;
+    }
+
+    /**
+     * Returns the index of the template parameter that this chunk refers to.
+     */
+    public int getParameterIndex() {
+      return parameterIndex;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("P(%s,%d)", getContext(), getParameterIndex());
+    }
+  }
+
+  /**
+   * Represents a parsed chunk of a template.
+   * 
+   * <p>There are two kinds of chunks: Those representing literal strings and
+   * those representing template parameters.
+   */
+  interface TemplateChunk {
+    /**
+     * Distinguishes different kinds of {@link TemplateChunk}.
+     */
+    public static enum Kind {
+      LITERAL, PARAMETER,
+    }
+
+    /**
+     * Returns the {@link Kind} of this template chunk.
+     */
+    Kind getKind();
+  }
+
+  /*
+   * The chunks of this parsed template.
+   */
+  private final LinkedList<TemplateChunk> chunks;
+
+  /**
+   * Initializes an empty parsed HTML template.
+   */
+  public ParsedHtmlTemplate() {
+    chunks = new LinkedList<TemplateChunk>();
+  }
+
+  /**
+   * Adds a literal string to the template.
+   * 
+   * If the currently last chunk of the parsed template is a literal chunk, the
+   * provided string literal will be appended to that chunk. I.e., consecutive
+   * literal chunks are automatically coalesced.
+
+   * @param literal the string to be added as a literal chunk
+   */
+  public void addLiteral(String literal) {
+    if (chunks.isEmpty()
+        || (chunks.getLast().getKind() != TemplateChunk.Kind.LITERAL)) {
+      chunks.add(new LiteralChunk(literal));
+    } else {
+      ((LiteralChunk) chunks.getLast()).append(literal);
+    }
+  }
+
+  /**
+   * Adds a parameter chunk to the template.
+   * 
+   * @param chunk the chunk to be added
+   */
+  public void addParameter(ParameterChunk chunk) {
+    chunks.add(chunk);
+  }
+
+  /**
+   * Returns the chunks of this parsed template.
+   * 
+   * <p>The returned list is unmodifiable.
+   */
+  public List<TemplateChunk> getChunks() {
+    return Collections.unmodifiableList(chunks);
+  }
+
+  @Override
+  public String toString() {
+    return chunks.toString();
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesGenerator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesGenerator.java
new file mode 100644
index 0000000..1f0ec8a
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesGenerator.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+
+import java.io.PrintWriter;
+
+/**
+ * Generator for implementations of
+ * {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}.
+ */
+public class SafeHtmlTemplatesGenerator extends Generator {
+
+  @Override
+  public String generate(TreeLogger logger, GeneratorContext genCtx,
+      String fqInterfaceName) throws UnableToCompleteException {
+    TypeOracle oracle = genCtx.getTypeOracle();
+    JClassType interfaceType;
+    try {
+      interfaceType = oracle.getType(fqInterfaceName);
+    } catch (NotFoundException e) {
+      logger.log(TreeLogger.ERROR, "Unexpected error: " + e.getMessage(), e);
+      throw new UnableToCompleteException();
+    }
+
+    String implName = interfaceType.getName();
+    implName = implName.replace('.', '_') + "Impl";
+
+    String packageName = interfaceType.getPackage().getName();
+    PrintWriter printWriter = genCtx.tryCreate(logger, packageName, implName);
+
+    if (printWriter != null) {
+      ClassSourceFileComposerFactory factory =
+          new ClassSourceFileComposerFactory(packageName, implName);
+      factory.addImplementedInterface(interfaceType.getQualifiedSourceName());
+      SourceWriter sourceWriter =
+          factory.createSourceWriter(genCtx, printWriter);
+
+      SafeHtmlTemplatesImplCreator implCreator =
+          new SafeHtmlTemplatesImplCreator(sourceWriter, interfaceType);
+      implCreator.emitClass(logger, null);
+
+      genCtx.commit(logger, printWriter);
+    }
+    return packageName + "." + implName;
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplCreator.java
new file mode 100644
index 0000000..2c52990
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplCreator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.i18n.shared.GwtLocale;
+import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
+import com.google.gwt.user.rebind.SourceWriter;
+
+/**
+ * Code generator for implementations of
+ * {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}.
+ */
+public class SafeHtmlTemplatesImplCreator
+    extends AbstractGeneratorClassCreator {
+
+  public SafeHtmlTemplatesImplCreator(SourceWriter sourceWriter,
+      JClassType interfaceType) {
+    super(sourceWriter, interfaceType);
+  }
+
+  @Override
+  protected void emitMethodBody(TreeLogger logger, JMethod method,
+      GwtLocale locale) throws UnableToCompleteException {
+   SafeHtmlTemplatesImplMethodCreator methodCreator =
+      new SafeHtmlTemplatesImplMethodCreator(this);
+   methodCreator.createMethodFor(logger, method, null, null, locale);
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
new file mode 100644
index 0000000..efaea94
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
+import com.google.gwt.i18n.shared.GwtLocale;
+import com.google.gwt.safehtml.client.SafeHtmlTemplates.Template;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.LiteralChunk;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.TemplateChunk;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.UriUtils;
+import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
+import com.google.gwt.user.rebind.AbstractMethodCreator;
+
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Method body code generator for implementations of
+ * {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}.
+ */
+public class SafeHtmlTemplatesImplMethodCreator extends AbstractMethodCreator {
+
+  /**
+   * Fully-qualified class name of the {@link String} class.
+   */
+  private static final String JAVA_LANG_STRING_FQCN = String.class.getName();
+
+  /**
+   * Fully-qualified class name of the {@link SafeHtml} interface.
+   */
+  public static final String SAFE_HTML_FQCN = SafeHtml.class.getName();
+
+  /**
+   * Fully-qualified class name of the StringBlessedAsSafeHtml class.
+   */
+  private static final String BLESSED_STRING_FQCN =
+      OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.class.getName();
+
+  /**
+   * Fully-qualified class name of the {@link SafeHtmlUtils} class.
+   */
+  private static final String ESCAPE_UTILS_FQCN = SafeHtmlUtils.class.getName();
+  
+  /**
+   * Fully-qualified class name of the {@link UriUtils} class.
+   */
+  private static final String URI_UTILS_FQCN = UriUtils.class.getName();
+
+  /**
+   * The set of the names of the HTML attributes whose values are interpreted
+   * as URIs.
+   *
+   * <p>See <a href="http://www.w3.org/TR/html4/index/attributes.html">Index
+   * of Attributes</a> for reference.
+   */
+  private static final Set<String> URI_VALUED_ATTRIBUTES;
+
+  static {
+    URI_VALUED_ATTRIBUTES = new TreeSet();
+    URI_VALUED_ATTRIBUTES.addAll(Arrays.asList(
+          "action",
+          "archive",
+          "background",
+          "cite",
+          "classid",
+          "codebase",
+          "data",
+          "dynsrc",
+          "href",
+          "longdesc",
+          "src",
+          "usemap"));
+  }
+
+  public SafeHtmlTemplatesImplMethodCreator(
+      AbstractGeneratorClassCreator classCreator) {
+    super(classCreator);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void createMethodFor(TreeLogger logger, JMethod targetMethod,
+      String key, ResourceList resourceList, GwtLocale locale)
+      throws UnableToCompleteException {
+    if (!targetMethod.getReturnType().getQualifiedSourceName().equals(
+        SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN)) {
+      throw error(logger, "All methods in interfaces extending "
+          + "SafeHtmlTemplates must have a return type of "
+          + SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN + ".");
+    }
+    Template templateAnnotation = targetMethod.getAnnotation(Template.class);
+    if (templateAnnotation == null) {
+      throw error(logger, "Required annotation @Template not present "
+          + "on interface method " + targetMethod.toString());
+    }
+
+    String template = templateAnnotation.value();
+    JParameter[] params = targetMethod.getParameters();
+    emitMethodBodyFromTemplate(logger, template, params);
+  }
+
+  /**
+   * Emits an expression corresponding to a template variable in "attribute"
+   * context.
+   *
+   * <p>The expression emitted applies appropriate escaping and/or sanitization
+   * to the parameter's value depending the Java type of the corresponding
+   * template method parameter:
+   *
+   * <ul>
+   *   <li>If the parameter is not of type {@link String}, it is first converted
+   *       to {@link String}.
+   *   <li>If the template parameter occurs at the start of a URI-valued
+   *       attribute within the template, it is sanitized to ensure that it
+   *       is safe in this context.  This is done by passing the value through
+   *       {@link UriUtils#sanitizeUri(String)}.
+   *   <li>The result is then HTML-escaped by passing it through
+   *       {@link SafeHtmlUtils#htmlEscape(String)}.
+   * </ul>
+   *
+   * <i>Note</i>: Template method parameters of type {@link SafeHtml} are
+   * <i>not</i> treated specially in an attribute context, and will be HTML-
+   * escaped like regular strings. This is because {@link SafeHtml} values can
+   * contain non-escaped HTML markup, which is not valid within attributes.
+   *
+   * @param logger the logger to log failures to
+   * @param htmlContext the HTML context in which the corresponding template
+   *        variable occurs in
+   * @param formalParameterName the name of the template method's formal
+   *        parameter corresponding to the expression being emitted
+   * @param parameterType the Java type of the corresponding template method's
+   *        parameter
+   */
+  private void emitAttributeContextParameterExpression(TreeLogger logger,
+      HtmlContext htmlContext, String formalParameterName,
+      JType parameterType) {
+
+    // TODO(xtof): check attr name against a set of attributes we know we can
+    //     handle (i.e., excluding things like onFoo, style, etc).
+
+    /*
+     * Build up the expression from the "inside out", i.e. start with the formal
+     * parameter, convert to string if necessary, then wrap in validators if
+     * necessary, and finally HTML-escape if necessary.
+     */
+    String expression = formalParameterName;
+
+    // The parameter's value must be explicitly converted to String unless it
+    // is already of that type.
+    if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) {
+      expression = "String.valueOf(" + expression + ")";
+    }
+
+    boolean isParameterAtStartOfUriAttribute =
+        htmlContext.getType() == HtmlContext.Type.ATTRIBUTE_START
+            && URI_VALUED_ATTRIBUTES.contains(htmlContext.getAttribute());
+    if (isParameterAtStartOfUriAttribute) {
+      expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")";
+    }
+
+    // TODO(xtof): Handle EscapedString subtype of SafeHtml, once it's been
+    //     introduced.
+    // TODO(xtof): Throw an exception if using SafeHtml within an attribute.
+    expression = ESCAPE_UTILS_FQCN + ".htmlEscape(" + expression + ")";
+
+    print(expression);
+  }
+
+  /**
+   * Generates code that renders the provided HTML template into an instance
+   * of the {@link SafeHtml} type.
+   *
+   * <p>The template is parsed as a (X)HTML template (see
+   * {@link HtmlTemplateParser}).  From the template's parsed form, code is
+   * generated that, when executed, will emit an instantiation of the template.
+   * The generated code appropriately escapes and/or sanitizes template
+   * parameters such that evaluating the emitted string as HTML in a browser
+   * will not result in script execution.
+   *
+   * <p>As such, strings emitted from generated template methods satisfy the
+   * type contract of the {@link SafeHtml} type, and can therefore be returned
+   * wrapped as {@link SafeHtml}.
+   *
+   * @param logger the logger to log failures to
+   * @param template the (X)HTML template to generate code for
+   * @param params the parameters of the corresponding template method
+   * @throws UnableToCompleteException if an error occurred that prevented
+   *         code generation for the template
+   */
+  private void emitMethodBodyFromTemplate(TreeLogger logger, String template,
+      JParameter[] params) throws UnableToCompleteException {
+    println("StringBuilder sb = new java.lang.StringBuilder()");
+    indent();
+    indent();
+
+    HtmlTemplateParser parser = new HtmlTemplateParser(logger);
+    parser.parseXHtml(new StringReader(template));
+
+    for (TemplateChunk chunk : parser.getParsedTemplate().getChunks()) {
+      if (chunk.getKind() == TemplateChunk.Kind.LITERAL) {
+        emitStringLiteral(((LiteralChunk) chunk).getLiteral());
+      } else if (chunk.getKind() == TemplateChunk.Kind.PARAMETER) {
+        ParameterChunk parameterChunk = (ParameterChunk) chunk;
+
+        int formalParameterIndex = parameterChunk.getParameterIndex();
+        if (formalParameterIndex < 0 || formalParameterIndex >= params.length) {
+          throw error(logger, "Argument " + formalParameterIndex
+              + " beyond range of arguments: " + template);
+        }
+        String formalParameterName = "arg" + formalParameterIndex;
+        JType paramType = params[formalParameterIndex].getType();
+
+        emitParameterExpression(logger, parameterChunk.getContext(),
+                                formalParameterName, paramType);
+      } else {
+        throw error(logger, "Unexpected chunk kind in parsed template "
+            + template);
+      }
+    }
+    println(";");
+    outdent();
+    outdent();
+    println("return new " + BLESSED_STRING_FQCN + "(sb.toString());");
+  }
+  
+  /**
+   * Emits an expression corresponding to a template parameter.
+   *
+   * <p>The expression emitted applies appropriate escaping/sanitization to the
+   * parameter's value, depending on the parameter's HTML context, and the Java
+   * type of the corresponding template method parameter.
+   *
+   * @param logger the logger to log failures to
+   * @param htmlContext the HTML context in which the corresponding template
+   *        variable occurs in
+   * @param formalParameterName the name of the template method's formal
+   *        parameter corresponding to the expression being emitted
+   * @param parameterType the Java type of the corresponding template method's
+   *        parameter
+   */
+  private void emitParameterExpression(TreeLogger logger,
+      HtmlContext htmlContext, String formalParameterName,
+      JType parameterType) {
+    print(".append(");
+    if (htmlContext.getType() == HtmlContext.Type.TEXT) {
+      emitTextContextParameterExpression(formalParameterName, parameterType);
+    } else if (htmlContext.getType() == HtmlContext.Type.ATTRIBUTE
+        || htmlContext.getType() == HtmlContext.Type.ATTRIBUTE_START) {
+      emitAttributeContextParameterExpression(logger, htmlContext,
+          formalParameterName, parameterType);
+    } else {
+      throw new IllegalStateException(
+          "unknown HTML context for formal template parameter "
+              + formalParameterName + ": " + htmlContext);
+    }
+    println(")");
+  }
+
+  /**
+   * Emits a string literal.
+   *
+   * @param str the {@link String} to emit as a literal
+   */
+  private void emitStringLiteral(String str) {
+    print(".append(");
+    print(wrap(str));
+    println(")");
+  }
+
+  /**
+   * Emits an expression corresponding to a template variable in "inner text"
+   * context.
+   *
+   * <p>The expression emitted applies appropriate escaping to the parameter's
+   * value depending the Java type of the corresponding template method
+   * parameter:
+   *
+   * <ul>
+   *   <li>If the parameter is of a primitive (e.g., numeric, boolean) type, or
+   *       of type {@link SafeHtml}, it is emitted as is, without escaping.
+   *   <li>Otherwise, an expression that passes the paramter's value through
+   *       {@link EscapeUtils#htmlEscape(String)} is emitted.
+   * </ul>
+   *
+   * @param formalParameterName the name of the template method's formal
+   *        parameter corresponding to the expression being emitted
+   * @param parameterType the Java type of the corresponding template method's
+   *        parameter
+   */
+  private void emitTextContextParameterExpression(String formalParameterName,
+      JType parameterType) {
+    boolean parameterIsPrimitiveType = (parameterType.isPrimitive() != null);
+    boolean parameterIsNotStringTyped = !(JAVA_LANG_STRING_FQCN.equals(
+        parameterType.getQualifiedSourceName()));
+
+    if (SAFE_HTML_FQCN.equals(parameterType.getQualifiedSourceName())) {
+      // The parameter is of type SafeHtml and its wrapped string can
+      // therefore be emitted safely without escaping.
+      print(formalParameterName + ".asString()");
+    } else if (parameterIsPrimitiveType) {
+      // The string representations of primitive types never contain HTML
+      // special characters and can therefore be emitted without escaping.
+      print(formalParameterName);
+    } else {
+      // The parameter is of some other type, and its value must be HTML
+      // escaped. Furthermore, unless the parameter's type is {@link String},
+      // it must be explicitly converted to {@link String}.
+      print(ESCAPE_UTILS_FQCN + ".htmlEscape(");
+      if (parameterIsNotStringTyped) {
+        print("String.valueOf(");
+      }
+      print(formalParameterName);
+      if (parameterIsNotStringTyped) {
+        print(")");
+      }
+      print(")");
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java b/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java
index 1b656e3..4d6e104 100644
--- a/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java
+++ b/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java
@@ -16,6 +16,7 @@
 package com.google.gwt.safehtml;
 
 import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.safehtml.client.SafeHtmlTemplatesTest;
 import com.google.gwt.safehtml.shared.GwtSafeHtmlUtilsTest;
 import com.google.gwt.safehtml.shared.SafeHtmlStringTest;
 
@@ -31,6 +32,7 @@
     
     suite.addTestSuite(GwtSafeHtmlUtilsTest.class);
     suite.addTestSuite(SafeHtmlStringTest.class);
+    suite.addTestSuite(SafeHtmlTemplatesTest.class);
 
     return suite;
   }
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java b/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java
index 3bf970e..e5bb101 100644
--- a/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java
+++ b/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.safehtml;
 
+import com.google.gwt.safehtml.rebind.HtmlTemplateParserTest;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplateTest;
 import com.google.gwt.safehtml.server.UriUtilsTest;
 import com.google.gwt.safehtml.shared.SafeHtmlUtilsTest;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilderTest;
@@ -37,6 +39,8 @@
     suite.addTestSuite(SimpleHtmlSanitizerTest.class);
     suite.addTestSuite(SafeHtmlStringTest.class);
     suite.addTestSuite(UriUtilsTest.class);
+    suite.addTestSuite(HtmlTemplateParserTest.class);
+    suite.addTestSuite(ParsedHtmlTemplateTest.class);
     
     return suite;
   }
diff --git a/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
new file mode 100644
index 0000000..fbb3e2b
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2009 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.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import junit.framework.Assert;
+
+/**
+ * Tests the SafeHtmlTemplates compile-time binding mechanism.
+ */
+public class SafeHtmlTemplatesTest extends GWTTestCase {
+
+  private static final String HTML_MARKUP = "woo <i>whee</i>";
+
+  private static final String GOOD_URL_ESCAPED =
+      "http://foo.com/foo&lt;bar&gt;&amp;baz=dootz";
+  private static final String GOOD_URL = "http://foo.com/foo<bar>&baz=dootz";
+  private static final String BAD_URL = "javascript:evil(1<2)";
+  private static final String BAD_URL_ESCAPED = "javascript:evil(1&lt;2)";
+
+  private TestTemplates templates;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.safehtml.SafeHtml";
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    templates = GWT.create(TestTemplates.class);
+  }
+
+  /**
+   * A SafeHtmlTemplates interface for testing.
+   */
+  public interface TestTemplates extends SafeHtmlTemplates {
+    @Template("<span><b>{0}</b><span>{1}</span></span>")
+    SafeHtml simpleTemplate(String foo, SafeHtml bar);  
+
+    @Template("<span><a href=\"{0}\"><b>{1}</b></a></span>")
+    SafeHtml templateWithUriAttribute(String url, SafeHtml html);
+
+    @Template("<div id=\"{0}\">{1}</div>")
+    SafeHtml templateWithRegularAttribute(String id, SafeHtml html);
+
+    @Template("<span><img src=\"{0}/{1}\"/></span>")
+    SafeHtml templateWithTwoPartUriAttribute(String baseUrl, String urlPart);
+  }
+
+  public void testSimpleTemplate() {
+    Assert.assertEquals(
+        "<span><b>foo&lt;bar</b><span>woo <i>whee</i></span></span>",
+        templates.simpleTemplate("foo<bar",
+            SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+  }
+
+  public void testTemplateWithUriAttribute() {
+    Assert.assertEquals(
+        "<span><a href=\"" + GOOD_URL_ESCAPED + "\"><b>" + HTML_MARKUP
+            + "</b></a></span>",
+        templates.templateWithUriAttribute(
+            GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+    Assert.assertEquals(
+        "<span><a href=\"#\"><b>" + HTML_MARKUP + "</b></a></span>",
+        templates.templateWithUriAttribute(
+            BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+  }
+  
+  public void testTemplateWithRegularAttribute() {
+    Assert.assertEquals(
+        "<div id=\"" + GOOD_URL_ESCAPED + "\">" + HTML_MARKUP + "</div>",
+        templates.templateWithRegularAttribute(
+            GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+    
+    // Values inserted into non-URI-valued attributes should not be sanitized,
+    // but still escaped.
+    Assert.assertEquals(
+        "<div id=\"" + BAD_URL_ESCAPED + "\">" + HTML_MARKUP + "</div>",
+        templates.templateWithRegularAttribute(
+            BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+  }
+  
+  public void testTemplateWithTwoPartUriAttribute() {
+    Assert.assertEquals(
+        "<span><img src=\"" + GOOD_URL_ESCAPED + "/x&amp;y\"></img></span>",
+        templates.templateWithTwoPartUriAttribute(
+            GOOD_URL, "x&y").asString());
+    Assert.assertEquals(
+        "<span><img src=\"#/x&amp;y\"></img></span>",
+        templates.templateWithTwoPartUriAttribute(
+            BAD_URL, "x&y").asString());
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java b/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java
new file mode 100644
index 0000000..bf6f092
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/rebind/HtmlTemplateParserTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+
+import junit.framework.TestCase;
+
+import java.io.StringReader;
+
+/**
+ * Tests for {@link HtmlTemplateParser}.
+ */
+//TODO(xtof): unit tests for parse failures
+public final class HtmlTemplateParserTest extends TestCase {
+
+  private TreeLogger logger;
+
+  @Override
+  public void setUp() {
+    logger = new PrintWriterTreeLogger();
+  }
+  
+  /*
+   * We use the string representation of ParsedHtmlTemplate to express
+   * expected results. The unit test for ParsedHtmlTemplate establishes that
+   * this string representation accurately represents the structure of the
+   * parsed template.
+   */
+  private void assertParseTemplateStringResult(String expected,
+                                               String template) {
+    HtmlTemplateParser parser = new HtmlTemplateParser(logger);
+    HtmlContext ctx = new HtmlContext(HtmlContext.Type.UNDEFINED);
+    parser.parseTemplateString(ctx, template);
+    assertEquals(expected, parser.getParsedTemplate().toString());
+  }
+
+  public void testParseTemplateString() {
+    assertParseTemplateStringResult("[]", "");
+    assertParseTemplateStringResult("[L(foo)]", "foo");
+    assertParseTemplateStringResult(
+        "[L(foo), P((UNDEFINED,null,null),0), L(bar)]",
+        "foo{0}bar");
+    assertParseTemplateStringResult(
+        "[L(foo), P((UNDEFINED,null,null),0), "
+            + "P((UNDEFINED,null,null),1), L(bar)]",
+        "foo{0}{1}bar");
+    assertParseTemplateStringResult(
+        "[L(foo), P((UNDEFINED,null,null),0), L(b), "
+            + "P((UNDEFINED,null,null),1), L(bar)]",
+        "foo{0}b{1}bar");
+    assertParseTemplateStringResult(
+        "[L(foo), P((UNDEFINED,null,null),0), L(baz), "
+            + "P((UNDEFINED,null,null),1), L(bar)]",
+        "foo{0}baz{1}bar");
+    assertParseTemplateStringResult(
+        "[P((UNDEFINED,null,null),0), L(foo), P((UNDEFINED,null,null),0), "
+            + "P((UNDEFINED,null,null),1), L(bar)]",
+        "{0}foo{0}{1}bar");
+    assertParseTemplateStringResult(
+        "[P((UNDEFINED,null,null),0), L(foo), P((UNDEFINED,null,null),0), "
+            + "P((UNDEFINED,null,null),1), L(b)]",
+        "{0}foo{0}{1}b");
+    assertParseTemplateStringResult(
+        "[L(foo), P((UNDEFINED,null,null),0), P((UNDEFINED,null,null),1), "
+            + "L(bar), P((UNDEFINED,null,null),2)]",
+        "foo{0}{1}bar{2}");
+    assertParseTemplateStringResult(
+        "[L(f), P((UNDEFINED,null,null),2), P((UNDEFINED,null,null),1), "
+            + "L(bar), P((UNDEFINED,null,null),2)]",
+        "f{2}{1}bar{2}");
+    
+    // Test degenerate cases with curly braces that don't match a parameter
+    // pattern; these are treated as regular string literals.
+    assertParseTemplateStringResult("[L(foo{)]", "foo{");
+    assertParseTemplateStringResult("[L(}foo)]", "}foo");
+    assertParseTemplateStringResult("[L(foo{text})]", "foo{text}");
+  }
+
+  private void assertParseTemplateStringResultWithAtStartContext(
+      String expected, String template) {
+    HtmlTemplateParser parser = new HtmlTemplateParser(logger);
+    HtmlContext ctx = new HtmlContext(HtmlContext.Type.ATTRIBUTE, "img", "src");
+    HtmlContext ctxAtStart =
+        new HtmlContext(HtmlContext.Type.ATTRIBUTE_START, "img", "src");
+    parser.parseTemplateString(ctx, ctxAtStart, template);
+    assertEquals(expected, parser.getParsedTemplate().toString());
+  }
+
+  public void testParseTemplateStringWithAtStartContext() {
+    assertParseTemplateStringResultWithAtStartContext(
+        "[P((ATTRIBUTE_START,img,src),0)]",
+        "{0}");
+    assertParseTemplateStringResultWithAtStartContext(
+        "[L(x), P((ATTRIBUTE,img,src),0)]",
+        "x{0}");
+    assertParseTemplateStringResultWithAtStartContext(
+        "[L(boo), P((ATTRIBUTE,img,src),0)]",
+        "boo{0}");
+ 
+    assertParseTemplateStringResultWithAtStartContext(
+        "[P((ATTRIBUTE_START,img,src),0), L(foo)]",
+        "{0}foo");
+    assertParseTemplateStringResultWithAtStartContext(
+        "[P((ATTRIBUTE_START,img,src),0), P((ATTRIBUTE,img,src),1)]",
+        "{0}{1}");
+    assertParseTemplateStringResultWithAtStartContext(
+        "[P((ATTRIBUTE_START,img,src),0), L(X), P((ATTRIBUTE,img,src),1)]",
+        "{0}X{1}");
+    assertParseTemplateStringResultWithAtStartContext(
+        "[L(X), P((ATTRIBUTE,img,src),0), P((ATTRIBUTE,img,src),1)]",
+        "X{0}{1}");
+   }
+
+  private void assertParseXHtmlResult(String expected, String template)
+      throws UnableToCompleteException {
+    HtmlTemplateParser parser = new HtmlTemplateParser(logger);
+    parser.parseXHtml(new StringReader(template));
+    assertEquals(expected, parser.getParsedTemplate().toString());
+  }
+
+  public void testParseXHtml() throws UnableToCompleteException {
+    // Basic cases.
+    assertParseXHtmlResult("[L(<b>foo</b>)]", "<b>foo</b>");
+    assertParseXHtmlResult(
+        "[L(<span>foo<b>), P((TEXT,null,null),0), L(</b></span>)]",
+        "<span>foo<b>{0}</b></span>");
+    assertParseXHtmlResult(
+        "[L(<span>foo<b>), P((TEXT,null,null),1), L(</b>), "
+            + "P((TEXT,null,null),0), L(</span>)]",
+        "<span>foo<b>{1}</b>{0}</span>");
+
+    // Check that tags and attributes are lower-cased, but inner text is not.
+    assertParseXHtmlResult("[L(<b id=\"bAr\">fOo</b>)]",
+                           "<B Id=\"bAr\">fOo</B>");
+
+    // Verify correct handling/escaping of HTML metacharacters and
+    // CDATA sections.
+    assertParseXHtmlResult(
+        "[L(<span>foo&amp;bar<b>), P((TEXT,null,null),1), "
+            + "L(</b>foo-cdata &lt;baz&gt;), P((TEXT,null,null),0), "
+            + "L(</span>)]",
+        "<span>foo&amp;bar<b>{1}</b><![CDATA[foo-cdata <baz>]]>{0}</span>");
+
+    // Check correct handling of ATTRIBUTE vs ATTRIBUTE_START context.
+    assertParseXHtmlResult(
+        "[L(<a href=\"), P((ATTRIBUTE_START,a,href),0), "
+            + "L(\">), P((TEXT,null,null),1), L(</a>)]",
+        "<a href=\"{0}\">{1}</a>");
+    assertParseXHtmlResult(
+        "[L(<a href=\"http://), P((ATTRIBUTE,a,href),0), "
+            + "L(\">), P((TEXT,null,null),1), L(</a>)]",
+        "<a href=\"http://{0}\">{1}</a>");
+    assertParseXHtmlResult(
+        "[L(<a href=\"), P((ATTRIBUTE_START,a,href),0), "
+            + "L(/), P((ATTRIBUTE,a,href),1), "
+            + "L(\">), P((TEXT,null,null),2), L(</a>)]",
+        "<a href=\"{0}/{1}\">{2}</a>");
+
+    // Verify correct escaping in attributes.
+    assertParseXHtmlResult(
+        "[L(<a href=\"http://...&amp;), "
+            + "P((ATTRIBUTE,a,href),0), "
+            + "L(=), P((ATTRIBUTE,a,href),1), "
+            + "L(\">), P((TEXT,null,null),2), L(</a>)]",
+        "<a href=\"http://...&amp;{0}={1}\">{2}</a>");
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java b/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java
new file mode 100644
index 0000000..ef05cfb
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/rebind/ParsedHtmlTemplateTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2009 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.safehtml.rebind;
+
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.LiteralChunk;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk;
+import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.TemplateChunk;
+
+import junit.framework.TestCase;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Tests for {@link ParsedHtmlTemplate}.
+ *
+ */
+public final class ParsedHtmlTemplateTest extends TestCase {
+  public void testAddLiteral() {
+    ParsedHtmlTemplate parsed = new ParsedHtmlTemplate();
+    parsed.addLiteral("<foo>");
+
+    List<TemplateChunk> chunks = parsed.getChunks();
+    LiteralChunk chunk = (LiteralChunk) chunks.get(0);
+    assertEquals(TemplateChunk.Kind.LITERAL, chunk.getKind());
+    assertEquals("<foo>", chunk.getLiteral());
+    assertEquals("L(<foo>)", chunk.toString());
+
+    assertEquals("[L(<foo>)]", parsed.toString());
+  }
+
+  public void testAddParameter() {
+    ParsedHtmlTemplate parsed = new ParsedHtmlTemplate();
+    parsed.addParameter(new ParameterChunk(new HtmlContext(
+        HtmlContext.Type.TEXT), 0));
+
+    List<TemplateChunk> chunks = parsed.getChunks();
+    
+    ParameterChunk chunk = (ParameterChunk) chunks.get(0);
+    assertEquals(TemplateChunk.Kind.PARAMETER, chunk.getKind());
+    assertEquals(HtmlContext.Type.TEXT, chunk.getContext().getType());
+    assertNull(chunk.getContext().getTag());
+    assertNull(chunk.getContext().getAttribute());
+    assertEquals(0, chunk.getParameterIndex());
+    assertEquals("P((TEXT,null,null),0)", chunk.toString());
+
+    assertEquals("[P((TEXT,null,null),0)]", parsed.toString());
+  }
+
+  /**
+   * Tests that calling addLiteral(), addParameter(), addLiteral() in sequence
+   * results in the expected ParsedHtmlTemplate.
+   */
+  public void testAddLiteralAddParameterSequence() {
+    ParsedHtmlTemplate parsed = new ParsedHtmlTemplate();
+
+    parsed.addLiteral("<foo>");
+    parsed.addParameter(new ParameterChunk(new HtmlContext(
+        HtmlContext.Type.TEXT), 0));
+    parsed.addLiteral("</foo>");
+
+    List<TemplateChunk> chunks = parsed.getChunks();
+    assertEquals(3, chunks.size());
+    Iterator<TemplateChunk> it = chunks.iterator();
+
+    LiteralChunk litChunk;
+    ParameterChunk paramChunk;
+
+    litChunk = (LiteralChunk) it.next();
+    assertEquals(TemplateChunk.Kind.LITERAL, litChunk.getKind());
+    assertEquals("<foo>", litChunk.getLiteral());
+    assertEquals("L(<foo>)", litChunk.toString());
+
+    paramChunk = (ParameterChunk) it.next();
+    assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind());
+    assertEquals(HtmlContext.Type.TEXT, paramChunk.getContext().getType());
+    assertNull(paramChunk.getContext().getTag());
+    assertNull(paramChunk.getContext().getAttribute());
+    assertEquals(0, paramChunk.getParameterIndex());
+    assertEquals("P((TEXT,null,null),0)", paramChunk.toString());
+
+    litChunk = (LiteralChunk) it.next();
+    assertEquals(TemplateChunk.Kind.LITERAL, litChunk.getKind());
+    assertEquals("</foo>", litChunk.getLiteral());
+    assertEquals("L(</foo>)", litChunk.toString());
+
+    // Assert that the string representation of the parsed template has the
+    // expected format, to allow us to use the string representation in unit
+    // tests for the template parser.
+    assertEquals("[L(<foo>), P((TEXT,null,null),0), L(</foo>)]",
+                 parsed.toString());
+  }
+
+  /**
+   * Tests that calling addParameter(), addLiteral(), addLiteral(),
+   * addParameter() in sequence results in the expected ParsedHtmlTemplate.
+   * 
+   * <p>In particular, two calls to addLiteral() in sequence should result in 
+   * only a single LiteralChunk.
+   */
+  public void testAddParameterAddLiteralSequence() {
+    ParsedHtmlTemplate parsed = new ParsedHtmlTemplate();
+
+    parsed.addParameter(new ParameterChunk(new HtmlContext(
+        HtmlContext.Type.TEXT), 0));
+    parsed.addLiteral("<a");
+    parsed.addLiteral(" href=\"");
+    parsed.addParameter(new ParameterChunk(new HtmlContext(
+        HtmlContext.Type.ATTRIBUTE_START, "a", "href"), 1));
+
+    List<TemplateChunk> chunks = parsed.getChunks();
+    assertEquals(3, chunks.size());
+    Iterator<TemplateChunk> it = chunks.iterator();
+
+    LiteralChunk litChunk;
+    ParameterChunk paramChunk;
+
+    paramChunk = (ParameterChunk) it.next();
+    assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind());
+    assertEquals(HtmlContext.Type.TEXT, paramChunk.getContext().getType());
+    assertNull(paramChunk.getContext().getTag());
+    assertNull(paramChunk.getContext().getAttribute());
+    assertEquals(0, paramChunk.getParameterIndex());
+    assertEquals("P((TEXT,null,null),0)", paramChunk.toString());
+
+    litChunk = (LiteralChunk) it.next();
+    assertEquals(TemplateChunk.Kind.LITERAL, litChunk.getKind());
+    assertEquals("<a href=\"", litChunk.getLiteral());
+    assertEquals("L(<a href=\")", litChunk.toString());
+
+    paramChunk = (ParameterChunk) it.next();
+    assertEquals(TemplateChunk.Kind.PARAMETER, paramChunk.getKind());
+    assertEquals(
+        HtmlContext.Type.ATTRIBUTE_START, paramChunk.getContext().getType());
+    assertEquals("a", paramChunk.getContext().getTag());
+    assertEquals("href", paramChunk.getContext().getAttribute());
+    assertEquals(1, paramChunk.getParameterIndex());
+    assertEquals("P((ATTRIBUTE_START,a,href),1)", paramChunk.toString());
+
+    assertEquals(
+        "[P((TEXT,null,null),0), L(<a href=\"), "
+            + "P((ATTRIBUTE_START,a,href),1)]",
+        parsed.toString());
+  }
+}