Index: tools/dom/templates/html/impl/impl_Element.darttemplate |
diff --git a/tools/dom/templates/html/impl/impl_Element.darttemplate b/tools/dom/templates/html/impl/impl_Element.darttemplate |
index cbea8c1aed4623ebbe6782df2bbf15f8827e0620..528471bacf24381207c685653ac8c3612a08ab18 100644 |
--- a/tools/dom/templates/html/impl/impl_Element.darttemplate |
+++ b/tools/dom/templates/html/impl/impl_Element.darttemplate |
@@ -309,20 +309,30 @@ $(ANNOTATIONS)abstract class $CLASSNAME$EXTENDS$IMPLEMENTS$NATIVESPEC { |
/** |
* Creates an HTML element from a valid fragment of HTML. |
* |
- * The [html] fragment must represent valid HTML with a single element root, |
- * which will be parsed and returned. |
+ * var element = new Element.html('<div class="foo">content</div>'); |
* |
- * Important: the contents of [html] should not contain any user-supplied |
- * data. Without strict data validation it is impossible to prevent script |
- * injection exploits. |
+ * The HTML fragment should contain only one single root element, any |
+ * leading or trailing text nodes will be removed. |
* |
- * It is instead recommended that elements be constructed via [Element.tag] |
- * and text be added via [text]. |
+ * The HTML fragment is parsed as if it occurred within the context of a |
+ * `<body>` tag, this means that special elements such as `<caption>` which |
+ * must be parsed within the scope of a `<table>` element will be dropped. Use |
+ * [createFragment] to parse contextual HTML fragments. |
+ * |
+ * Unless a validator is provided this will perform the default validation |
+ * and remove all scriptable elements and attributes. |
+ * |
+ * See also: |
+ * |
+ * * [NodeValidator] |
* |
- * var element = new Element.html('<div class="foo">content</div>'); |
*/ |
- factory $CLASSNAME.html(String html) => |
- _$(CLASSNAME)FactoryProvider.createElement_html(html); |
+ factory Element.html(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ var fragment = document.body.createFragment(html, validator: validator, |
+ treeSanitizer: treeSanitizer); |
+ return fragment.nodes.where((e) => e is Element).single; |
Jennifer Messerly
2013/08/17 06:07:07
how's the performance of this? I dunno if it matte
blois
2013/08/19 22:02:09
Done.
|
+ } |
/** |
* Creates the HTML element specified by the tag name. |
@@ -1170,121 +1180,94 @@ $endif |
Point p = Element._offsetToHelper(parentOffset, parent); |
return new Point(p.x + current.offsetLeft, p.y + current.offsetTop); |
} |
+ |
+ /** |
+ * Create a DocumentFragment from the HTML fragment and ensure that it follows |
+ * the sanitization rules specified by the validator or treeSanitizer. |
+ * |
+ * If the default validation behavior is too restrictive then a new |
+ * NodeValidator should be created, either extending or wrapping a default |
+ * validator and overriding the validation APIs. |
+ * |
+ * The treeSanitizer is used to walk the generated node tree and sanitize it. |
+ * A custom treeSanitizer can also be provided to perform special validation |
+ * rules but since the API is more complex to implement this is discouraged. |
+ * |
+ * The returned tree is guaranteed to only contain nodes and attributes which |
+ * are allowed by the provided validator. |
+ * |
+ * See also: |
+ * |
+ * * [NodeValidator] |
+ * * [NodeTreeSanitizer] |
+ */ |
+ DocumentFragment createFragment(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ if (treeSanitizer == null) { |
+ if (validator == null) { |
+ validator = new NodeValidatorBuilder.common(); |
Jennifer Messerly
2013/08/17 06:07:07
I wonder about performance here, in particular if
blois
2013/08/19 22:02:09
Added caching.
|
+ } |
+ treeSanitizer = new NodeTreeSanitizer(validator); |
+ } else if (validator != null) { |
+ throw new ArgumentError( |
+ 'validator can only be passed if treeSanitizer is null'); |
+ } |
+ return _ElementFactoryProvider._parseHtml(this, html, treeSanitizer); |
Jennifer Messerly
2013/08/17 06:07:07
out of curiosity, why is this method on _ElementFa
blois
2013/08/19 22:02:09
FactoryProviders are a legacy thing that we've bee
Jennifer Messerly
2013/08/26 22:00:31
yay :D
|
+ } |
+ void set innerHtml(String html) { |
+ this.setInnerHtml(html); |
+ } |
+ void setInnerHtml(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ text = null; |
+ append(createFragment( |
+ html, validator: validator, treeSanitizer: treeSanitizer)); |
+ } |
+ String get innerHtml => $dom_innerHtml; |
Jennifer Messerly
2013/08/17 06:07:07
on an unrelated note: is all this $dom_ stuff goin
blois
2013/08/19 22:02:09
That's the plan- been having hiccups with benchmar
|
+ |
$!MEMBERS |
} |
-final _START_TAG_REGEXP = new RegExp('<(\\w+)'); |
+ |
class _ElementFactoryProvider { |
- static const _CUSTOM_PARENT_TAG_MAP = const { |
- 'body' : 'html', |
- 'head' : 'html', |
- 'caption' : 'table', |
- 'td': 'tr', |
- 'th': 'tr', |
- 'colgroup': 'table', |
- 'col' : 'colgroup', |
- 'tr' : 'tbody', |
- 'tbody' : 'table', |
- 'tfoot' : 'table', |
- 'thead' : 'table', |
- 'track' : 'audio', |
- }; |
- @DomName('Document.createElement') |
- static Element createElement_html(String html) { |
- // TODO(jacobr): this method can be made more robust and performant. |
- // 1) Cache the dummy parent elements required to use innerHTML rather than |
- // creating them every call. |
- // 2) Verify that the html does not contain leading or trailing text nodes. |
- // 3) Verify that the html does not contain both <head> and <body> tags. |
- // 4) Detatch the created element from its dummy parent. |
- String parentTag = 'div'; |
- String tag; |
- final match = _START_TAG_REGEXP.firstMatch(html); |
- if (match != null) { |
- tag = match.group(1).toLowerCase(); |
- if (Device.isIE && Element._TABLE_TAGS.containsKey(tag)) { |
- return _createTableForIE(html, tag); |
- } |
- parentTag = _CUSTOM_PARENT_TAG_MAP[tag]; |
- if (parentTag == null) parentTag = 'div'; |
- } |
+ static HtmlDocument _parseDocument; |
- final temp = new Element.tag(parentTag); |
- temp.innerHtml = html; |
+ static DocumentFragment _parseHtml(Element context, String html, |
+ NodeTreeSanitizer treeSanitizer) { |
- Element element; |
- if (temp.children.length == 1) { |
- element = temp.children[0]; |
- } else if (parentTag == 'html' && temp.children.length == 2) { |
- // In html5 the root <html> tag will always have a <body> and a <head>, |
- // even though the inner html only contains one of them. |
- element = temp.children[tag == 'head' ? 0 : 1]; |
+ if (_parseDocument == null) { |
+ _parseDocument = document.implementation.createHtmlDocument(''); |
+ } |
+ var contextElement; |
+ if (context == null || context is BodyElement) { |
Jennifer Messerly
2013/08/17 06:07:07
curious about the special case here for "body". co
blois
2013/08/19 22:02:09
It's defaulting to body and just re-using that ele
|
+ contextElement = _parseDocument.body; |
} else { |
- _singleNode(temp.children); |
+ contextElement = _parseDocument.$dom_createElement(context.tagName); |
+ _parseDocument.body.append(contextElement); |
} |
- element.remove(); |
- return element; |
- } |
+ if (Range.supportsCreateContextualFragment) { |
+ var range = _parseDocument.$dom_createRange(); |
+ range.selectNodeContents(contextElement); |
+ var fragment = range.createContextualFragment(html); |
- /** |
- * IE table elements don't support innerHTML (even in standards mode). |
- * Instead we use a div and inject the table element in the innerHtml string. |
- * This technique works on other browsers too, but it's probably slower, |
- * so we only use it when running on IE. |
- * |
- * See also innerHTML: |
- * <http://msdn.microsoft.com/en-us/library/ie/ms533897(v=vs.85).aspx> |
- * and Building Tables Dynamically: |
- * <http://msdn.microsoft.com/en-us/library/ie/ms532998(v=vs.85).aspx>. |
- */ |
- static Element _createTableForIE(String html, String tag) { |
- var div = new Element.tag('div'); |
- div.innerHtml = '<table>$html</table>'; |
- var table = _singleNode(div.children); |
- Element element; |
- switch (tag) { |
- case 'td': |
- case 'th': |
- TableRowElement row = _singleNode(table.rows); |
- element = _singleNode(row.cells); |
- break; |
- case 'tr': |
- element = _singleNode(table.rows); |
- break; |
- case 'tbody': |
- element = _singleNode(table.tBodies); |
- break; |
- case 'thead': |
- element = table.tHead; |
- break; |
- case 'tfoot': |
- element = table.tFoot; |
- break; |
- case 'caption': |
- element = table.caption; |
- break; |
- case 'colgroup': |
- element = _getColgroup(table); |
- break; |
- case 'col': |
- element = _singleNode(_getColgroup(table).children); |
- break; |
- } |
- element.remove(); |
- return element; |
- } |
+ if (contextElement != _parseDocument.body) { |
+ contextElement.remove(); |
+ } |
- static TableColElement _getColgroup(TableElement table) { |
- // TODO(jmesserly): is there a better way to do this? |
- return _singleNode(table.children.where((n) => n.tagName == 'COLGROUP') |
- .toList()); |
- } |
+ treeSanitizer.sanitizeTree(fragment); |
+ return fragment; |
+ } else { |
+ contextElement.$dom_innerHtml = html; |
- static Node _singleNode(List<Node> list) { |
- if (list.length == 1) return list[0]; |
- throw new ArgumentError('HTML had ${list.length} ' |
- 'top level elements but 1 expected'); |
+ treeSanitizer.sanitizeTree(contextElement); |
+ |
+ var fragment = new DocumentFragment(); |
+ while (contextElement.firstChild != null) { |
+ fragment.append(contextElement.firstChild); |
+ } |
+ return fragment; |
+ } |
} |
@DomName('Document.createElement') |