Revision: 9830
Author: [email protected]
Date: Wed Mar 9 09:01:28 2011
Log: Updates UiBinder HTML rendering to use SafeHtml. Rather than
generating generic HTML strings for HTML objects, the generated binder code
creates a SafeHtml template based on the *.ui.xml specs and passes
arguments to the template as either String or SafeHtml-wrapped objects,
depending on the context.
A small size regression is expected as a new SafeHtml template signature
and implementation are being generated for each HTML object. However, the
safety of the generated HTML provides a clear benefit.
Review at http://gwt-code-reviews.appspot.com/1305801
Review by: [email protected]
http://code.google.com/p/google-web-toolkit/source/detail?r=9830
Added:
/trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java
/trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplateArgument.java
/trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplates.java
/trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplateTest.java
/trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplatesTest.java
Modified:
/trunk/user/src/com/google/gwt/uibinder/elementparsers/AttributeMessageInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/ComputedAttributeInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TextInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
/trunk/user/src/com/google/gwt/uibinder/rebind/Tokenator.java
/trunk/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
/trunk/user/src/com/google/gwt/uibinder/rebind/messages/AttributeMessage.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
/trunk/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java
=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplate.java
Wed Mar 9 09:01:28 2011
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.IndentedWriter;
+import com.google.gwt.uibinder.rebind.Tokenator;
+import com.google.gwt.uibinder.rebind.Tokenator.Resolver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An individual SafeHtml template to be written by UiBinder.
+ */
+public class HtmlTemplate {
+ private final List<String> template = new ArrayList<String>();
+ private final String methodName;
+ private final ArrayList<HtmlTemplateArgument> methodArgs =
+ new ArrayList<HtmlTemplateArgument>();
+ private final HtmlTemplates templates;
+
+ public HtmlTemplate(String html, Tokenator tokenator, HtmlTemplates
templates)
+ throws IllegalArgumentException {
+ if (html == null) {
+ throw new IllegalArgumentException("Template html cannot be null");
+ }
+ if (tokenator == null) {
+ throw new IllegalArgumentException("Template tokenator cannot be
null");
+ }
+ if (templates == null) {
+ throw new IllegalArgumentException("HtmlTemplates container cannot
be null");
+ }
+
+ this.templates = templates;
+ methodName = "html" + this.templates.nextTemplateId();
+
+ populateArgMap(html,tokenator);
+
+ template.add("@Template(\"" + addTemplatePlaceholders(html) + "\")");
+ template.add("SafeHtml " + methodName + "(" + getTemplateArgs()
+ ");");
+ template.add(" ");
+ }
+
+ public List<String> getTemplate() {
+ return template;
+ }
+
+ /**
+ * Writes all templates to the provided {@link IndentedWriter}.
+ *
+ * @param w the writer to write the template to
+ */
+ public void writeTemplate(IndentedWriter w) {
+ for (String s : template) {
+ w.write(s);
+ }
+ }
+
+ /**
+ * Creates the template method invocation.
+ *
+ * @return String the template method call with parameters
+ */
+ public String writeTemplateCall() {
+ return "template." + methodName + "(" + getSafeHtmlArgs()
+ + ").asString()";
+ }
+
+ /**
+ * Replaces string tokens with {} placeholders for SafeHtml templating.
+ *
+ * @return the rendering string, with tokens replaced by {} placeholders
+ */
+ private String addTemplatePlaceholders(String html) {
+ String rtn = Tokenator.detokenate(html, new Resolver() {
+ int tokenId = 0;
+ public String resolveToken(String token) {
+ return "{" + tokenId++ + "}";
+ }
+ });
+ return rtn;
+ }
+
+ /**
+ * Retrieves the arguments for SafeHtml template function call from
+ * the {@link Tokenator}.
+ */
+ private String getSafeHtmlArgs() {
+ StringBuilder b = new StringBuilder();
+
+ for (HtmlTemplateArgument arg : methodArgs) {
+ if (b.length() > 0) {
+ b.append(", ");
+ }
+ b.append(arg.getArg());
+ }
+
+ return b.toString();
+ }
+
+ /**
+ * Creates the argument string for the generated SafeHtmlTemplate
function.
+ */
+ private String getTemplateArgs() {
+ StringBuilder b = new StringBuilder();
+ int i = 0;
+
+ for (HtmlTemplateArgument arg : methodArgs) {
+ if (b.length() > 0) {
+ b.append(", ");
+ }
+ b.append(arg.getType() + " arg" + i);
+ i++;
+ }
+
+ return b.toString();
+ }
+
+ private void populateArgMap(String s, Tokenator t) {
+ if (t != null) {
+ List<String> args = t.getOrderedValues(s);
+
+ for (String arg : args) {
+ if (templates.isSafeConstant(arg)) {
+ methodArgs.add(HtmlTemplateArgument.forHtml(arg));
+ } else {
+ methodArgs.add(HtmlTemplateArgument.forString(arg.substring(4,
+ arg.length() - 4)));
+ }
+ }
+ }
+ }
+}
+
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplateArgument.java
Wed Mar 9 09:01:28 2011
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+/**
+ * Container class for a UiBinder SafeHtml template argument.
+ */
+public class HtmlTemplateArgument {
+ static HtmlTemplateArgument forHtml(String arg) {
+ return new HtmlTemplateArgument("SafeHtml", arg);
+ }
+ static HtmlTemplateArgument forString(String arg) {
+ return new HtmlTemplateArgument("String", arg);
+ }
+
+ private final String arg;
+
+ private final String type;
+
+ private HtmlTemplateArgument(String type, String arg) {
+ this.type = type;
+ this.arg = arg;
+ }
+
+ public String getArg() {
+ return arg;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
=======================================
--- /dev/null
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/model/HtmlTemplates.java
Wed Mar 9 09:01:28 2011
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.IndentedWriter;
+import com.google.gwt.uibinder.rebind.Tokenator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Model class for SafeHtml templates used in generated
+ * UiBinder rendering implementation.
+ */
+
+public class HtmlTemplates {
+ private final List<HtmlTemplate> htmlTemplates = new
ArrayList<HtmlTemplate>();
+ private final Set<String> safeConstantTokens = new HashSet<String>();
+
+ public HtmlTemplates() {
+ }
+
+ /**
+ * Add a SafeHtml template and an instance method for invoking the
template
+ * to the generated BinderImpl class. These templates are declared at
the
+ * beginning of the class and instantiated with a
GWT.create(Template.class
+ * call.
+ * <p>
+ * Note that the UiBinder#tokenator is used to determine the arguments to
+ * the generated SafeHtml template.
+ *
+ * @return String the function to call this template
+ */
+ public String addSafeHtmlTemplate(String html, Tokenator t)
+ throws IllegalArgumentException {
+ if (html == null) {
+ throw new IllegalArgumentException("Template html cannot be null");
+ }
+ if (t == null) {
+ throw new IllegalArgumentException("Template tokenator cannot be
null");
+ }
+
+ HtmlTemplate ht = new HtmlTemplate(html, t, this);
+ htmlTemplates.add(ht);
+
+ return ht.writeTemplateCall();
+ }
+
+ public int getNumTemplates() {
+ return htmlTemplates.size();
+ }
+
+ public List<HtmlTemplate> getTemplates() {
+ return htmlTemplates;
+ }
+
+ public boolean isEmpty() {
+ return htmlTemplates.isEmpty();
+ }
+
+ public boolean isSafeConstant(String token) {
+ return safeConstantTokens.contains(token);
+ }
+
+ public void noteSafeConstant(String token) {
+ safeConstantTokens.add(token);
+ }
+
+ public void writeTemplates(IndentedWriter w) {
+ for (HtmlTemplate t : htmlTemplates) {
+ t.writeTemplate(w);
+ }
+ }
+
+ /**
+ * Increment the total number of templates.
+ */
+ protected int nextTemplateId() {
+ return htmlTemplates.size() + 1;
+ }
+}
=======================================
--- /dev/null
+++
/trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplateTest.java
Wed Mar 9 09:01:28 2011
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.Tokenator;
+
+import junit.framework.TestCase;
+
+import java.util.Iterator;
+
+/**
+ * HtmlTemplate unit tests.
+ *
+ */
+public class HtmlTemplateTest extends TestCase {
+ public void testHappy() {
+ Tokenator t = new Tokenator();
+ HtmlTemplates templates = new HtmlTemplates();
+
+ HtmlTemplate h = new HtmlTemplate("<DialogBox id=\'"
+ + t.nextToken("\" + domId0 + \"")
+ + "\'>this is a dialog box</DialogBox>", t, templates);
+
+ String[] expectedTemplates = {
+ "@Template(\"<DialogBox id=\'{0}\'>this is a dialog
box</DialogBox>\")",
+ "SafeHtml html1(String arg0);",
+ " ",};
+
+ Iterator<String> j = h.getTemplate().iterator();
+ for (String et : expectedTemplates) {
+ assertEquals(et, j.next());
+ }
+ assertFalse(j.hasNext());
+
+ String expectedCall = "template.html1(domId0).asString()";
+
+ assertEquals(h.writeTemplateCall(), expectedCall);
+ }
+
+ public void testNullHtml() {
+ HtmlTemplates templates = new HtmlTemplates();
+ Tokenator t = new Tokenator();
+
+ try {
+ new HtmlTemplate(null, t, templates);
+ fail("Expected empty html to generate error");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().equals("Template html cannot be null"));
+ }
+ }
+
+ public void testNullTokenator() {
+ HtmlTemplates templates = new HtmlTemplates();
+
+ try {
+ new HtmlTemplate("<p>this is a static string</p>", null,
+ templates);
+ fail("Expected empty tokenator to generate error");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().equals("Template tokenator cannot be
null"));
+ }
+ }
+
+ public void testNullHtmlTemplates() {
+ Tokenator t = new Tokenator();
+
+ try {
+ new HtmlTemplate("<p>this is a static string</p>", t, null);
+ fail("Expected empty html to generate error");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().equals("HtmlTemplates container cannot be
null"));
+ }
+ }
+
+}
=======================================
--- /dev/null
+++
/trunk/user/test/com/google/gwt/uibinder/rebind/model/HtmlTemplatesTest.java
Wed Mar 9 09:01:28 2011
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+package com.google.gwt.uibinder.rebind.model;
+
+import com.google.gwt.uibinder.rebind.IndentedWriter;
+import com.google.gwt.uibinder.rebind.Tokenator;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * HtmlTemplates unit tests.
+ *
+ */
+public class HtmlTemplatesTest extends TestCase {
+ public void testHappy() throws IllegalArgumentException {
+ Tokenator t = new Tokenator();
+ HtmlTemplates h = new HtmlTemplates();
+
+ String call = h.addSafeHtmlTemplate("<DialogBox id=\'"
+ + t.nextToken("\" + domId0 + \"")
+ + "\'>this is a dialog box</DialogBox>", t);
+
+ // Check the generated template
+ String[] expectedTemplates = {
+ "@Template(\"<DialogBox id=\'{0}\'>this is a dialog
box</DialogBox>\")",
+ "SafeHtml html1(String arg0);",
+ " ",};
+
+ List<String> templates = new ArrayList<String>();
+ for (HtmlTemplate ht : h.getTemplates()) {
+ templates.addAll(ht.getTemplate());
+ }
+
+ Iterator<String> i = templates.iterator();
+ for (String et : expectedTemplates) {
+ assertEquals(et, i.next());
+ }
+ assertFalse(i.hasNext());
+
+ // Check the returned template function call
+ String expectedCall = "template.html1(domId0).asString()";
+ assertEquals(call, expectedCall);
+
+ StringWriter s = new StringWriter();
+ IndentedWriter n = new IndentedWriter(new PrintWriter(s));
+
+ // Confirm that IndentedWriter writes the correct template.
+ h.writeTemplates(n);
+
+ assertEquals("@Template(\"<DialogBox id=\'{0}\'>this is a dialog "
+ + "box</DialogBox>\")\nSafeHtml html1(String arg0);\n \n",
s.toString());
+ }
+
+ public void testNullHtml() {
+ HtmlTemplates h = new HtmlTemplates();
+ Tokenator t = new Tokenator();
+
+ try {
+ h.addSafeHtmlTemplate(null, t);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertTrue("Expected empty html to generate error",
+ e.getMessage().equals("Template html cannot be null"));
+ }
+
+ assertTrue(h.isEmpty());
+ }
+
+ public void testNullTokenator() throws IllegalArgumentException {
+ HtmlTemplates h = new HtmlTemplates();
+
+ try {
+ h.addSafeHtmlTemplate("<p>this is a static string</p>", null);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertTrue("Expected empty tokenator to generate error",
+ e.getMessage().equals("Template tokenator cannot be null"));
+ }
+
+ assertTrue(h.isEmpty());
+ }
+}
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/AttributeMessageInterpreter.java
Fri Nov 6 16:08:46 2009
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/AttributeMessageInterpreter.java
Wed Mar 9 09:01:28 2011
@@ -47,7 +47,7 @@
MessagesWriter messages = writer.getMessages();
for (AttributeMessage am : messages.consumeAttributeMessages(elem)) {
elem.setAttribute(am.getAttribute(),
- writer.tokenForExpression(am.getMessageAsHtmlAttribute()));
+ writer.tokenForStringExpression(am.getMessageUnescaped()));
}
/*
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/ComputedAttributeInterpreter.java
Wed Nov 11 22:08:47 2009
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/ComputedAttributeInterpreter.java
Wed Mar 9 09:01:28 2011
@@ -50,7 +50,7 @@
if (parser != null) {
// Legacy res:style='style.pretty'
String parsedValue = parser.parse(att.consumeRawValue());
- String attToken = writer.tokenForExpression(parsedValue);
+ String attToken = writer.tokenForStringExpression(parsedValue);
// Use localName so <div res:style='...'> becomes <div style='...'>
attNameToToken.put(att.getLocalName(), attToken);
@@ -58,7 +58,7 @@
}
if (att.hasComputedValue()) {
- String attToken =
writer.tokenForExpression(att.consumeStringValue());
+ String attToken =
writer.tokenForStringExpression(att.consumeStringValue());
attNameToToken.put(att.getName(), attToken);
} else {
/*
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
Mon Jun 7 12:20:31 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/CustomButtonParser.java
Wed Mar 9 09:01:28 2011
@@ -68,8 +68,8 @@
writer, fieldName);
String innerHtml = child.consumeInnerHtml(interpreter);
if (innerHtml.length() > 0) {
- writer.addStatement("%s.%s().setHTML(\"%s\");", fieldName,
- faceNameGetter(faceName), innerHtml);
+ writer.addStatement("%s.%s().setHTML(%s);", fieldName,
+ faceNameGetter(faceName),
writer.declareTemplateCall(innerHtml));
}
if (child.hasAttribute("image")) {
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
Thu Jan 20 14:28:08 2011
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DialogBoxParser.java
Wed Mar 9 09:01:28 2011
@@ -77,7 +77,7 @@
handleConstructorArgs(elem, fieldName, type, writer, customCaption);
if (caption != null) {
- writer.addStatement("%s.setHTML(\"%s\");", fieldName, caption);
+ writer.addStatement("%s.setHTML(%s);", fieldName,
writer.declareTemplateCall(caption));
}
if (body != null) {
writer.addStatement("%s.setWidget(%s);", fieldName, body);
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
Fri Apr 2 06:00:38 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/DomElementParser.java
Wed Mar 9 09:01:28 2011
@@ -39,8 +39,9 @@
String html = elem.consumeOpeningTag() +
elem.consumeInnerHtml(interpreter)
+ elem.getClosingTag();
writer.endAttachedSection();
+
writer.setFieldInitializer(fieldName, String.format(
- "(%1$s) UiBinderUtil.fromHtml(\"%2$s\")",
- type.getQualifiedSourceName(), html));
+ "(%1$s) UiBinderUtil.fromHtml(%2$s)",
+ type.getQualifiedSourceName(), writer.declareTemplateCall(html)));
}
}
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
Wed Jul 21 11:18:39 2010
+++ /trunk/user/src/com/google/gwt/uibinder/elementparsers/GridParser.java
Wed Mar 9 09:01:28 2011
@@ -93,9 +93,10 @@
for (List<CellContent> row : matrix) {
for (CellContent column : row) {
if (column.getTagName().equals(CELL_TAG)) {
- writer.addStatement("%s.setHTML(%s, %s, \"%s\");", fieldName,
+ writer.addStatement("%s.setHTML(%s, %s, %s);", fieldName,
Integer.toString(matrix.indexOf(row)),
- Integer.toString(row.indexOf(column)), column.getConent());
+ Integer.toString(row.indexOf(column)),
+ writer.declareTemplateCall(column.getConent()));
}
if (column.getTagName().equals(CUSTOMCELL_TAG)) {
writer.addStatement("%s.setWidget(%s, %s, %s);", fieldName,
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
Fri Apr 2 06:00:38 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HTMLPanelParser.java
Wed Mar 9 09:01:28 2011
@@ -57,12 +57,12 @@
* signature (by passing in type).
*/
String customTag = elem.consumeStringAttribute("tag", null);
+
if (null == customTag) {
- writer.setFieldInitializerAsConstructor(fieldName, type, "\"" + html
- + "\"");
+ writer.setFieldInitializerAsConstructor(fieldName, type,
writer.declareTemplateCall(html));
} else {
- writer.setFieldInitializerAsConstructor(fieldName, type,
customTag, "\""
- + html + "\"");
+ writer.setFieldInitializerAsConstructor(fieldName, type, customTag,
+ writer.declareTemplateCall(html));
}
}
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
Fri Apr 2 06:00:38 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HasHTMLParser.java
Wed Mar 9 09:01:28 2011
@@ -35,7 +35,7 @@
writer.endAttachedSection();
// TODO(jgw): throw an error if there's a conflicting 'html' attribute.
if (html.trim().length() > 0) {
- writer.genStringPropertySet(fieldName, "HTML", html);
+ writer.genPropertySet(fieldName, "HTML",
writer.declareTemplateCall(html));
}
}
}
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java
Mon Jun 7 12:20:31 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/HtmlMessageInterpreter.java
Wed Mar 9 09:01:28 2011
@@ -74,7 +74,7 @@
MessageWriter message = messages.newMessage(elem);
message.setDefaultMessage(elem.consumeInnerHtml(phiProvider.get(message)));
- return uiWriter.tokenForExpression(messages.declareMessage(message));
+ return
uiWriter.tokenForSafeHtmlExpression(messages.declareMessage(message));
}
return null;
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
Tue Aug 31 08:46:42 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
Wed Mar 9 09:01:28 2011
@@ -75,8 +75,8 @@
writer, fieldName);
String size =
children.header.consumeRequiredDoubleAttribute("size");
String html = children.header.consumeInnerHtml(htmlInt);
- writer.addStatement("%s.add(%s, \"%s\", true, %s);", fieldName,
- childFieldName, html, size);
+ writer.addStatement("%s.add(%s, %s, true, %s);", fieldName,
+ childFieldName, writer.declareTemplateCall(html), size);
} else if (children.customHeader != null) {
XMLElement headerElement =
children.customHeader.consumeSingleChildElement();
String size =
children.customHeader.consumeRequiredDoubleAttribute("size");
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
Tue Aug 31 08:46:42 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
Wed Mar 9 09:01:28 2011
@@ -78,8 +78,8 @@
HtmlInterpreter htmlInt =
HtmlInterpreter.newInterpreterForUiObject(
writer, fieldName);
String html = children.header.consumeInnerHtml(htmlInt);
- writer.addStatement("%s.add(%s, \"%s\", true);", fieldName,
- childFieldName, html);
+ writer.addStatement("%s.add(%s, %s, true);", fieldName,
+ childFieldName, writer.declareTemplateCall(html));
} else if (children.customHeader != null) {
XMLElement headerElement =
children.customHeader.consumeSingleChildElement();
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
Tue Aug 31 08:46:42 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TabPanelParser.java
Wed Mar 9 09:01:28 2011
@@ -72,8 +72,8 @@
writer.die(tabElem, "Must have a child widget");
}
if (tabHTML != null) {
- writer.addStatement("%1$s.add(%2$s, \"%3$s\", true);", fieldName,
- childFieldName, tabHTML);
+ writer.addStatement("%1$s.add(%2$s, %3$s, true);", fieldName,
+ childFieldName, writer.declareTemplateCall(tabHTML));
} else if (tabCaption != null) {
writer.addStatement("%1$s.add(%2$s, %3$s);", fieldName,
childFieldName,
tabCaption);
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TextInterpreter.java
Mon Jun 7 12:20:31 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/TextInterpreter.java
Wed Mar 9 09:01:28 2011
@@ -43,7 +43,7 @@
MessagesWriter messages = writer.getMessages();
if (messages.isMessage(elem)) {
String messageInvocation = consumeAsTextMessage(elem, messages);
- return writer.tokenForExpression(messageInvocation);
+ return writer.tokenForStringExpression(messageInvocation);
}
return new
UiTextInterpreter(writer.getLogger()).interpretElement(elem);
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
Mon Apr 19 07:56:12 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
Wed Mar 9 09:01:28 2011
@@ -91,7 +91,8 @@
// Create an element to hold the widget.
String tag = getLegalPlaceholderTag(elem);
- return "<" + tag + " id='\" + " + idHolder + " + \"'></" + tag + ">";
+ return "<" + tag + " id='" +
uiWriter.tokenForStringExpression(idHolder)
+ + "'></" + tag + ">";
}
return null;
}
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
Tue Sep 7 12:42:09 2010
+++
/trunk/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
Wed Mar 9 09:01:28 2011
@@ -132,7 +132,8 @@
}
private String genOpenTag(String name, String idHolder) {
- String openTag = String.format("<span id='\" + %s + \"'>", idHolder);
+ String openTag = String.format("<span id='%s'>",
+ uiWriter.tokenForStringExpression(idHolder));
String openPlaceholder = nextOpenPlaceholder(name + "Begin", openTag);
return openPlaceholder;
}
@@ -179,7 +180,8 @@
}
private String handleOpaqueWidgetPlaceholder(String name, String
idHolder) {
- String tag = String.format("<span id='\" + %s + \"'></span>",
idHolder);
+ String tag = String.format("<span id='%s'></span>",
+ uiWriter.tokenForStringExpression(idHolder));
String placeholder = nextPlaceholder(name, "<span></span>", tag);
return placeholder;
}
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/rebind/Tokenator.java Fri Nov
6 15:04:44 2009
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/Tokenator.java Wed Mar
9 09:01:28 2011
@@ -15,7 +15,9 @@
*/
package com.google.gwt.uibinder.rebind;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -53,10 +55,26 @@
detokenated.append(betokened.substring(index));
return detokenated.toString();
}
-
+
public static boolean hasToken(String s) {
return s.matches(".*" + TOKEN_REGEXP + "\\d+" + TOKEN_REGEXP + ".*");
}
+
+ private static List<String> getOrderedValues(String betokened,
+ Resolver resolver) {
+ List<String> values = new ArrayList<String>();
+
+ int index = 0, nextToken = 0;
+ while ((nextToken = betokened.indexOf(TOKEN, index)) > -1) {
+ int endToken = betokened.indexOf(TOKEN, nextToken + TOKEN.length());
+ String token = betokened.substring(nextToken, endToken +
TOKEN.length());
+ values.add(resolver.resolveToken(token));
+
+ index = endToken + TOKEN.length();
+ }
+
+ return values;
+ }
private static String nextToken() {
return TOKEN + (curId++) + TOKEN;
@@ -72,6 +90,14 @@
}
});
}
+
+ public List<String> getOrderedValues(String betokened) {
+ return getOrderedValues(betokened, new Resolver() {
+ public String resolveToken(String token) {
+ return tokenToResolved.get(token);
+ }
+ });
+ }
public String nextToken(final String resolved) {
String token = nextToken();
=======================================
--- /trunk/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java Wed
Feb 9 13:31:48 2011
+++ /trunk/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java Wed
Mar 9 09:01:28 2011
@@ -31,6 +31,7 @@
import com.google.gwt.uibinder.elementparsers.IsEmptyParser;
import com.google.gwt.uibinder.elementparsers.UiChildParser;
import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
+import com.google.gwt.uibinder.rebind.model.HtmlTemplates;
import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
import com.google.gwt.uibinder.rebind.model.ImplicitCssResource;
import com.google.gwt.uibinder.rebind.model.OwnerClass;
@@ -169,11 +170,12 @@
private final List<String> initStatements = new ArrayList<String>();
private final List<String> statements = new ArrayList<String>();
+ private final HtmlTemplates htmlTemplates = new HtmlTemplates();
private final HandlerEvaluator handlerEvaluator;
private final MessagesWriter messages;
private final DesignTimeUtils designTime;
private final Tokenator tokenator = new Tokenator();
-
+
private final String templatePath;
private final TypeOracle oracle;
/**
@@ -205,7 +207,6 @@
private String gwtPrefix;
private String rendered;
-
/**
* Stack of element variable names that have been attached.
*/
@@ -214,14 +215,15 @@
* Maps from field element name to the temporary attach record variable
name.
*/
private final Map<String, String> attachedVars = new HashMap<String,
String>();
- private int nextAttachVar = 0;
-
+
+ private int nextAttachVar = 0;
/**
* Stack of statements to be executed after we detach the current attach
* section.
*/
private final LinkedList<List<String>> detachStatementsStack = new
LinkedList<List<String>>();
private final AttributeParsers attributeParsers;
+
private final BundleAttributeParsers bundleParsers;
private final UiBinderContext uiBinderCtx;
@@ -289,7 +291,7 @@
public void addDetachStatement(String format, Object... args) {
detachStatementsStack.getFirst().add(String.format(format, args));
}
-
+
/**
* Add a statement to be run after everything has been instantiated, in
the
* style of {@link String#format}.
@@ -313,8 +315,8 @@
* moment, HasHTMLParser, HTMLPanelParser, and DomElementParser.).
* <p>
* Succeeding calls made to {@link #ensureAttached} and
- * {@link #ensureFieldAttached} must refer to children of this element,
until
- * {@link #endAttachedSection} is called.
+ * {@link #ensureCurrentFieldAttached} must refer to children of this
element,
+ * until {@link #endAttachedSection} is called.
*
* @param element Java expression for the generated code that will
return the
* dom element to be attached.
@@ -345,7 +347,8 @@
"%s =
com.google.gwt.dom.client.Document.get().getElementById(%s).cast();",
fieldName, name);
addInitStatement("%s.removeAttribute(\"id\");", fieldName);
- return tokenForExpression(name);
+
+ return tokenForStringExpression(name);
}
/**
@@ -404,11 +407,23 @@
}
/**
- * Given a string containing tokens returned by {@link
#tokenForExpression} or
- * {@link #declareDomField}, return a string with those tokens replaced
by the
- * appropriate expressions. (It is not normally necessary for an
- * {@link XMLElement.Interpreter} or {@link ElementParser} to make this
call,
- * as the tokens are typically replaced by the TemplateWriter itself.)
+ * Writes a new SafeHtml template to the generated BinderImpl.
+ *
+ * @return The invocation of the SafeHtml template function with the
arguments
+ * filled in
+ */
+ public String declareTemplateCall(String html)
+ throws IllegalArgumentException {
+ return htmlTemplates.addSafeHtmlTemplate(html, tokenator);
+ }
+
+ /**
+ * Given a string containing tokens returned by {@link
#tokenForStringExpression},
+ * {@link #tokenForSafeHtmlExpression} or {@link #declareDomField},
return a
+ * string with those tokens replaced by the appropriate expressions. (It
is
+ * not normally necessary for an {@link XMLElement.Interpreter} or
+ * {@link ElementParser} to make this call, as the tokens are typically
+ * replaced by the TemplateWriter itself.)
*/
public String detokenate(String betokened) {
return tokenator.detokenate(betokened);
@@ -478,7 +493,7 @@
/**
* Ensure that the specified field is attached to the DOM. The field
must hold
* an object that responds to Element getElement(). Convenience wrapper
for
- * {@link ensureAttached}<code>(field + ".getElement()")</code>.
+ * {@link #ensureAttached}<code>(field + ".getElement()")</code>.
*
* @see #beginAttachedSection(String)
*/
@@ -558,7 +573,7 @@
public DesignTimeUtils getDesignTime() {
return designTime;
}
-
+
/**
* Returns the logger, at least until we get get it handed off to
parsers via
* constructor args.
@@ -658,18 +673,35 @@
setFieldInitializer(fieldName, formatCode("new %s(%s)",
type.getQualifiedSourceName(), asCommaSeparatedList(args)));
}
+
+ /**
+ * Like {@link #tokenForStringExpression}, but used for runtime
expressions
+ * that we trust to be safe to interpret at runtime as HTML without
escaping,
+ * like translated messages with simple formatting. Wrapped in a call to
+ * {@link com.google.gwt.safehtml.shared.SafeHtmlUtils#fromSafeConstant}
to
+ * keep the expression from being escaped by the SafeHtml template.
+ *
+ * @param expression
+ */
+ public String tokenForSafeHtmlExpression(String expression) {
+ String token = tokenator.nextToken("SafeHtmlUtils.fromSafeConstant(" +
+ expression + ")");
+ htmlTemplates.noteSafeConstant("SafeHtmlUtils.fromSafeConstant(" +
+ expression + ")");
+ return token;
+ }
/**
* Returns a string token that can be used in place the given expression
* inside any string literals. Before the generated code is written, the
- * expression will be stiched back into the generated code in place of
the
+ * expression will be stitched back into the generated code in place of
the
* token, surrounded by plus signs. This is useful in strings to be
handed to
* setInnerHTML() and setText() calls, to allow a unique dom id
attribute or
* other runtime expression in the string.
*
* @param expression
*/
- public String tokenForExpression(String expression) {
+ public String tokenForStringExpression(String expression) {
return tokenator.nextToken(("\" + " + expression + " + \""));
}
@@ -686,7 +718,7 @@
public void warn(String message, Object... params) {
logger.warn(message, params);
}
-
+
/**
* Post a warning message.
*/
@@ -716,7 +748,7 @@
this.rendered = tokenator.detokenate(parseDocumentElement(elem));
printWriter.print(rendered);
}
-
+
private void addElementParser(String gwtClass, String parser) {
elementParsers.put(gwtClass, parser);
}
@@ -750,7 +782,7 @@
* Ensures that all of the internal data structures are cleaned up
correctly
* at the end of parsing the document.
*
- * @throws UnableToCompleteException
+ * @throws IllegalStateException
*/
private void ensureAttachmentCleanedUp() {
if (!attachSectionElements.isEmpty()) {
@@ -1017,6 +1049,9 @@
writeClassOpen(w);
writeStatics(w);
w.newline();
+
+ // Create SafeHtml Template
+ writeSafeHtmlTemplates(w);
// createAndBindUi method
w.write("public %s createAndBindUi(final %s owner) {",
@@ -1027,7 +1062,7 @@
writeGwtFields(w);
w.newline();
-
+
designTime.writeAttributes(this);
writeAddedStatements(w);
w.newline();
@@ -1112,6 +1147,11 @@
private void writeImports(IndentedWriter w) {
w.write("import com.google.gwt.core.client.GWT;");
+ if (!(htmlTemplates.isEmpty())) {
+ w.write("import com.google.gwt.safehtml.client.SafeHtmlTemplates;");
+ w.write("import com.google.gwt.safehtml.shared.SafeHtml;");
+ w.write("import com.google.gwt.safehtml.shared.SafeHtmlUtils;");
+ }
w.write("import com.google.gwt.uibinder.client.UiBinder;");
w.write("import com.google.gwt.uibinder.client.UiBinderUtil;");
w.write("import %s.%s;", uiRootType.getPackage().getName(),
@@ -1177,6 +1217,25 @@
}
}
+ /**
+ * Write statements created by {@link
HtmlTemplates#addSafeHtmlTemplate}. This
+ * code must be placed after all instantiation code.
+ */
+ private void writeSafeHtmlTemplates(IndentedWriter w) {
+ if (!(htmlTemplates.isEmpty())) {
+ w.write("interface Template extends SafeHtmlTemplates {");
+ w.indent();
+
+ htmlTemplates.writeTemplates(w);
+
+ w.outdent();
+ w.write("}");
+ w.newline();
+ w.write("Template template = GWT.create(Template.class);");
+ w.newline();
+ }
+ }
+
/**
* Generates instances of any bundle classes that have been referenced
by a
* namespace entry in the top level element. This must be called *after*
all
=======================================
---
/trunk/user/src/com/google/gwt/uibinder/rebind/messages/AttributeMessage.java
Wed Oct 28 09:10:53 2009
+++
/trunk/user/src/com/google/gwt/uibinder/rebind/messages/AttributeMessage.java
Wed Mar 9 09:01:28 2011
@@ -32,15 +32,6 @@
public String getAttribute() {
return attribute;
}
-
- /**
- * Return an expression to fetch the message and escape it, suitable
- * for concatenation into the middle of an innerHTML string.
- */
- public String getMessageAsHtmlAttribute() {
- return message
- + ".replaceAll(\"&\", \"&\").replaceAll(\"'\", \"'\")";
- }
public String getMessageUnescaped() {
return message;
=======================================
---
/trunk/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
Thu Jan 20 14:28:08 2011
+++
/trunk/user/test/com/google/gwt/uibinder/elementparsers/DialogBoxParserTest.java
Wed Mar 9 09:01:28 2011
@@ -79,7 +79,7 @@
b.append("</g:DialogBox> ");
String[] expected = {
- "fieldName.setHTML(\"Hello, I <b>caption</b>you.\");",
+ "fieldName.setHTML(\"@mockToken-Hello, I <b>caption</b>you.\");",
"fieldName.setWidget(<g:Label>);",};
FieldWriter w = tester.parse(b.toString());
@@ -348,7 +348,7 @@
b.append("</ui:UiBinder>");
String[] expected = {
- "fieldName.setHTML(\"Hello, I <b>caption</b>you.\");",
+ "fieldName.setHTML(\"@mockToken-Hello, I <b>caption</b>you.\");",
"fieldName.setWidget(<g:Label>);",};
parser.parse(tester.getElem(b.toString(), "my:MyDialogBox"), "fieldName",
=======================================
---
/trunk/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
Wed Jul 21 11:18:39 2010
+++
/trunk/user/test/com/google/gwt/uibinder/elementparsers/GridParserTest.java
Wed Mar 9 09:01:28 2011
@@ -53,8 +53,8 @@
assertNotNull(tester.logger.died);
}
}
-
- public void testEmtpyChild() throws SAXException {
+
+ public void testEmptyChild() throws SAXException {
StringBuffer b = new StringBuffer();
b.append("<g:Grid>");
b.append(" <g:row>");
@@ -140,8 +140,8 @@
String[] expected = {
"fieldName.resize(2, 2);",
- "fieldName.setHTML(0, 0, \"<div>foo HTML element</div>\");",
- "fieldName.setHTML(0, 1, \"<div>bar HTML element</div>\");",
+ "fieldName.setHTML(0, 0, \"@mockToken-<div>foo HTML
element</div>\");",
+ "fieldName.setHTML(0, 1, \"@mockToken-<div>bar HTML
element</div>\");",
"fieldName.setWidget(1, 0, <g:Label>);",
"fieldName.setWidget(1, 1, <g:Label>);"};
@@ -177,7 +177,7 @@
String[] expected = {
"fieldName.resize(2, 2);",
- "fieldName.setHTML(0, 0, \"<div>foo HTML element</div>\");",
+ "fieldName.setHTML(0, 0, \"@mockToken-<div>foo HTML
element</div>\");",
"fieldName.setWidget(1, 0, <g:Label>);",
"fieldName.setWidget(1, 1, <g:Label>);"};
=======================================
---
/trunk/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java
Wed Feb 9 14:35:38 2011
+++
/trunk/user/test/com/google/gwt/uibinder/elementparsers/MockUiBinderWriter.java
Wed Mar 9 09:01:28 2011
@@ -32,7 +32,7 @@
class MockUiBinderWriter extends UiBinderWriter {
final List<String> statements = new ArrayList<String>();
-
+
public MockUiBinderWriter(JClassType baseClass, String implClassName,
String templatePath, TypeOracle oracle, MortalLogger logger,
FieldManager fieldManager, MessagesWriter messagesWriter,
@@ -46,10 +46,18 @@
public void addStatement(String format, Object... args) {
statements.add(String.format(format, args));
}
-
+
+ /**
+ * Mocked out version of the template declaration. Returns
+ * the template prefixed with "@mockToken-"
+ */
+ public String declareTemplateCall(String html) {
+ return "\"@mockToken-" + html + "\"";
+ }
+
@Override
public String parseElementToField(XMLElement elem)
throws UnableToCompleteException {
return elem.consumeOpeningTag();
- }
-}
+ }
+}
=======================================
---
/trunk/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
Tue Aug 31 08:46:42 2010
+++
/trunk/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
Wed Mar 9 09:01:28 2011
@@ -165,7 +165,7 @@
+ "(com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
assertStatements(
- "fieldName.add(<g:Label id='able'>, \"Re<b>mark</b>able\", true,
3);",
+ "fieldName.add(<g:Label id='able'>,
\"@mockToken-Re<b>mark</b>able\", true, 3);",
"fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>,
3);");
}
=======================================
---
/trunk/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
Tue Aug 31 08:46:42 2010
+++
/trunk/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
Wed Mar 9 09:01:28 2011
@@ -168,7 +168,7 @@
b.append("</g:TabLayoutPanel>");
String[] expected = {
- "fieldName.add(<g:Label id='able'>, \"Re<b>mark</b>able\", true);",
+ "fieldName.add(<g:Label id='able'>,
\"@mockToken-Re<b>mark</b>able\", true);",
"fieldName.add(<g:Label id='baker'>, " + "<g:Label
id='custom'>);",};
FieldWriter w = tester.parse(b.toString());
=======================================
---
/trunk/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java
Tue Aug 31 08:46:42 2010
+++
/trunk/user/test/com/google/gwt/uibinder/elementparsers/TabPanelParserTest.java
Wed Mar 9 09:01:28 2011
@@ -146,7 +146,7 @@
tester.parse(b.toString());
assertStatements("fieldName.add(<g:Label id='0'>, \"Foo\");",
- "fieldName.add(<g:Label id='1'>, \"B<b>a</b>r\", true);");
+ "fieldName.add(<g:Label id='1'>, \"@mockToken-B<b>a</b>r\",
true);");
}
private void assertStatements(String... expected) {
--
http://groups.google.com/group/Google-Web-Toolkit-Contributors