Index: pkg/third_party/html5lib/lib/dom.dart |
diff --git a/pkg/third_party/html5lib/lib/dom.dart b/pkg/third_party/html5lib/lib/dom.dart |
index 973e5ae1bc89c003e39e087275280c39fd0e72f7..a87bca96e9eda2f85465dffb42a220c4a9f18e2e 100644 |
--- a/pkg/third_party/html5lib/lib/dom.dart |
+++ b/pkg/third_party/html5lib/lib/dom.dart |
@@ -1,18 +1,26 @@ |
/// A simple tree API that results from parsing html. Intended to be compatible |
-/// with dart:html, but right now it resembles the classic JS DOM. |
+/// with dart:html, but it is missing many types and APIs. |
library dom; |
+// TODO(jmesserly): lots to do here. Originally I wanted to generate this using |
+// our Blink IDL generator, but another idea is to directly use the excellent |
+// http://dom.spec.whatwg.org/ and http://html.spec.whatwg.org/ and just |
+// implement that. |
+ |
import 'dart:collection'; |
import 'package:source_maps/span.dart' show FileSpan; |
import 'src/constants.dart'; |
+import 'src/css_class_set.dart'; |
import 'src/list_proxy.dart'; |
+import 'src/query_selector.dart' as query; |
import 'src/token.dart'; |
import 'src/tokenizer.dart'; |
-import 'src/utils.dart'; |
import 'dom_parsing.dart'; |
import 'parser.dart'; |
+export 'src/css_class_set.dart' show CssClassSet; |
+ |
// TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes |
// that exposes namespace info. |
class AttributeName implements Comparable { |
@@ -60,6 +68,55 @@ class AttributeName implements Comparable { |
} |
} |
+// http://dom.spec.whatwg.org/#parentnode |
+abstract class _ParentNode implements Node { |
+ // TODO(jmesserly): this is only a partial implementation |
+ |
+ /// Seaches for the first descendant node matching the given selectors, using |
+ /// a preorder traversal. |
+ /// |
+ /// NOTE: Not all selectors from |
+ /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/) |
+ /// are implemented. For example, nth-child does not implement An+B syntax |
+ /// and *-of-type is not implemented. If a selector is not implemented this |
+ /// method will throw [UniplmentedError]. |
+ Element querySelector(String selector) => |
+ query.querySelector(this, selector); |
+ |
+ /// Returns all descendant nodes matching the given selectors, using a |
+ /// preorder traversal. |
+ /// |
+ /// NOTE: Not all selectors from |
+ /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/) |
+ /// are implemented. For example, nth-child does not implement An+B syntax |
+ /// and *-of-type is not implemented. If a selector is not implemented this |
+ /// method will throw [UniplmentedError]. |
+ List<Element> querySelectorAll(String selector) => |
+ query.querySelectorAll(this, selector); |
+} |
+ |
+// http://dom.spec.whatwg.org/#interface-nonelementparentnode |
+abstract class _NonElementParentNode implements _ParentNode { |
+ // TODO(jmesserly): could be faster, should throw on invalid id. |
+ Element getElementById(String id) => querySelector('#$id'); |
+} |
+ |
+// This doesn't exist as an interface in the spec, but it's useful to merge |
+// common methods from these: |
+// http://dom.spec.whatwg.org/#interface-document |
+// http://dom.spec.whatwg.org/#element |
+abstract class _ElementAndDocument implements _ParentNode { |
+ // TODO(jmesserly): could be faster, should throw on invalid tag/class names. |
+ |
+ List<Element> getElementsByTagName(String localName) => |
+ querySelectorAll(localName); |
+ |
+ List<Element> getElementsByClassName(String classNames) => |
+ querySelectorAll(classNames.splitMapJoin(' ', |
+ onNonMatch: (m) => m.isNotEmpty ? '.$m' : m, |
+ onMatch: (m) => '')); |
+} |
+ |
/// Really basic implementation of a DOM-core like Node. |
abstract class Node { |
static const int ATTRIBUTE_NODE = 2; |
@@ -75,16 +132,14 @@ abstract class Node { |
static const int PROCESSING_INSTRUCTION_NODE = 7; |
static const int TEXT_NODE = 3; |
- /// Note: For now we use it to implement the deprecated tagName property. |
- final String _tagName; |
- |
- /// *Deprecated* use [Element.localName] instead. |
- /// Note: after removal, this will be replaced by a correct version that |
- /// returns uppercase [as specified](http://dom.spec.whatwg.org/#dom-element-tagname). |
- @deprecated String get tagName => _tagName; |
- |
/// The parent of the current node (or null for the document node). |
- Node parent; |
+ Node parentNode; |
+ |
+ /// The parent element of this node. |
+ /// |
+ /// Returns null if this node either does not have a parent or its parent is |
+ /// not an element. |
+ Element get parent => parentNode is Element ? parentNode : null; |
// TODO(jmesserly): should move to Element. |
/// A map holding name, value pairs for attributes of the node. |
@@ -108,10 +163,7 @@ abstract class Node { |
LinkedHashMap<dynamic, FileSpan> _attributeSpans; |
LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; |
- /// *Deprecated* use [new Element.tag] instead. |
- @deprecated Node(String tagName) : this._(tagName); |
- |
- Node._([this._tagName]) { |
+ Node._() { |
nodes._parent = this; |
} |
@@ -140,29 +192,14 @@ abstract class Node { |
return _elements; |
} |
- // TODO(jmesserly): needs to support deep clone. |
- /// Return a shallow copy of the current node i.e. a node with the same |
- /// name and attributes but with no parent or child nodes. |
- Node clone(); |
- |
- /// *Deprecated* use [Element.namespaceUri] instead. |
- @deprecated String get namespace => null; |
+ /// Returns a copy of this node. |
+ /// |
+ /// If [deep] is `true`, then all of this node's children and decendents are |
+ /// copied as well. If [deep] is `false`, then only this node is copied. |
+ Node clone(bool deep); |
int get nodeType; |
- /// *Deprecated* use [text], [Text.data] or [Comment.data]. |
- @deprecated String get value => null; |
- |
- /// *Deprecated* use [nodeType]. |
- @deprecated int get $dom_nodeType => nodeType; |
- |
- /// *Deprecated* use [Element.outerHtml] |
- @deprecated String get outerHtml => _outerHtml; |
- |
- /// *Deprecated* use [Element.innerHtml] |
- @deprecated String get innerHtml => _innerHtml; |
- @deprecated set innerHtml(String value) { _innerHtml = value; } |
- |
// http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface |
String get _outerHtml { |
var str = new StringBuffer(); |
@@ -176,13 +213,6 @@ abstract class Node { |
return str.toString(); |
} |
- set _innerHtml(String value) { |
- nodes.clear(); |
- // TODO(jmesserly): should be able to get the same effect by adding the |
- // fragment directly. |
- nodes.addAll(parseFragment(value, container: _tagName).nodes); |
- } |
- |
// Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent |
String get text => null; |
set text(String value) {} |
@@ -199,8 +229,8 @@ abstract class Node { |
Node remove() { |
// TODO(jmesserly): is parent == null an error? |
- if (parent != null) { |
- parent.nodes.remove(this); |
+ if (parentNode != null) { |
+ parentNode.nodes.remove(this); |
} |
return this; |
} |
@@ -219,10 +249,10 @@ abstract class Node { |
/// Replaces this node with another node. |
Node replaceWith(Node otherNode) { |
- if (parent == null) { |
+ if (parentNode == null) { |
throw new UnsupportedError('Node must have a parent to replace it.'); |
} |
- parent.nodes[parent.nodes.indexOf(this)] = otherNode; |
+ parentNode.nodes[parentNode.nodes.indexOf(this)] = otherNode; |
return this; |
} |
@@ -230,10 +260,6 @@ abstract class Node { |
/// Return true if the node has children or text. |
bool hasContent() => nodes.length > 0; |
- /// *Deprecated* construct a pair using the namespaceUri and the name. |
- @deprecated Pair<String, String> get nameTuple => |
- this is Element ? getElementNameTuple(this) : null; |
- |
/// Move all the children of the current node to [newParent]. |
/// This is needed so that trees that don't store text as nodes move the |
/// text in the correct way. |
@@ -242,42 +268,10 @@ abstract class Node { |
nodes.clear(); |
} |
- /// *Deprecated* use [querySelector] instead. |
- @deprecated |
- Element query(String selectors) => querySelector(selectors); |
- |
- /// *Deprecated* use [querySelectorAll] instead. |
- @deprecated |
- List<Element> queryAll(String selectors) => querySelectorAll(selectors); |
- |
- /// Seaches for the first descendant node matching the given selectors, using a |
- /// preorder traversal. NOTE: right now, this supports only a single type |
- /// selectors, e.g. `node.query('div')`. |
- |
- Element querySelector(String selectors) => |
- _queryType(_typeSelector(selectors)); |
- |
- /// Returns all descendant nodes matching the given selectors, using a |
- /// preorder traversal. NOTE: right now, this supports only a single type |
- /// selectors, e.g. `node.queryAll('div')`. |
- List<Element> querySelectorAll(String selectors) { |
- var results = new List<Element>(); |
- _queryAllType(_typeSelector(selectors), results); |
- return results; |
- } |
- |
bool hasChildNodes() => !nodes.isEmpty; |
bool contains(Node node) => nodes.contains(node); |
- String _typeSelector(String selectors) { |
- selectors = selectors.trim(); |
- if (!_isTypeSelector(selectors)) { |
- throw new UnimplementedError('only type selectors are implemented'); |
- } |
- return selectors; |
- } |
- |
/// Checks if this is a type selector. |
/// See <http://www.w3.org/TR/CSS2/grammar.html>. |
/// Note: this doesn't support '*', the universal selector, non-ascii chars or |
@@ -318,24 +312,6 @@ abstract class Node { |
return true; |
} |
- Element _queryType(String tag) { |
- for (var node in nodes) { |
- if (node is! Element) continue; |
- if (node.localName == tag) return node; |
- var result = node._queryType(tag); |
- if (result != null) return result; |
- } |
- return null; |
- } |
- |
- void _queryAllType(String tag, List<Element> results) { |
- for (var node in nodes) { |
- if (node is! Element) continue; |
- if (node.localName == tag) results.add(node); |
- node._queryAllType(tag, results); |
- } |
- } |
- |
/// Initialize [attributeSpans] using [sourceSpan]. |
void _ensureAttributeSpans() { |
if (_attributeSpans != null) return; |
@@ -363,9 +339,20 @@ abstract class Node { |
} |
} |
} |
+ |
+ _clone(Node shallowClone, bool deep) { |
+ if (deep) { |
+ for (var child in nodes) { |
+ shallowClone.append(child.clone(true)); |
+ } |
+ } |
+ return shallowClone; |
+ } |
} |
-class Document extends Node { |
+class Document extends Node |
+ with _ParentNode, _NonElementParentNode, _ElementAndDocument { |
+ |
Document() : super._(); |
factory Document.html(String html) => parse(html); |
@@ -388,18 +375,41 @@ class Document extends Node { |
void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); |
- Document clone() => new Document(); |
+ Document clone(bool deep) => _clone(new Document(), deep); |
+ |
+ Element createElement(String tag) => new Element.tag(tag); |
+ |
+ // TODO(jmesserly): this is only a partial implementation of: |
+ // http://dom.spec.whatwg.org/#dom-document-createelementns |
+ Element createElementNS(String namespaceUri, String tag) { |
+ if (namespaceUri == '') namespaceUri = null; |
+ return new Element._(tag, namespaceUri); |
+ } |
+ |
+ DocumentFragment createDocumentFragment() => new DocumentFragment(); |
} |
-class DocumentFragment extends Document { |
- DocumentFragment(); |
+class DocumentFragment extends Node |
+ with _ParentNode, _NonElementParentNode { |
+ |
+ DocumentFragment() : super._(); |
factory DocumentFragment.html(String html) => parseFragment(html); |
int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; |
+ /// Returns a fragment of HTML or XML that represents the element and its |
+ /// contents. |
+ // TODO(jmesserly): this API is not specified in: |
+ // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead |
+ // only Element has outerHtml. However it is quite useful. Should we move it |
+ // to dom_parsing, where we keep other custom APIs? |
+ String get outerHtml => _outerHtml; |
+ |
String toString() => "#document-fragment"; |
- DocumentFragment clone() => new DocumentFragment(); |
+ DocumentFragment clone(bool deep) => _clone(new DocumentFragment(), deep); |
+ |
+ void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); |
String get text => _getText(this); |
set text(String value) => _setText(this, value); |
@@ -412,7 +422,7 @@ class DocumentType extends Node { |
DocumentType(String name, this.publicId, this.systemId) |
// Note: once Node.tagName is removed, don't pass "name" to super |
- : name = name, super._(name); |
+ : name = name, super._(); |
int get nodeType => Node.DOCUMENT_TYPE_NODE; |
@@ -433,7 +443,7 @@ class DocumentType extends Node { |
str.write(toString()); |
} |
- DocumentType clone() => new DocumentType(name, publicId, systemId); |
+ DocumentType clone(bool deep) => new DocumentType(name, publicId, systemId); |
} |
class Text extends Node { |
@@ -441,36 +451,29 @@ class Text extends Node { |
Text(this.data) : super._(); |
- /// *Deprecated* use [data]. |
- @deprecated String get value => data; |
- @deprecated set value(String x) { data = x; } |
- |
int get nodeType => Node.TEXT_NODE; |
String toString() => '"$data"'; |
void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this); |
- Text clone() => new Text(data); |
+ Text clone(bool deep) => new Text(data); |
String get text => data; |
set text(String value) { data = value; } |
} |
-class Element extends Node { |
+// TODO(jmesserly): Elements should have a pointer back to their document |
+class Element extends Node with _ParentNode, _ElementAndDocument { |
final String namespaceUri; |
- @deprecated String get namespace => namespaceUri; |
- |
/// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name) |
/// of this element. |
- String get localName => _tagName; |
+ final String localName; |
- // TODO(jmesserly): deprecate in favor of [Document.createElementNS]. |
- // However we need every element to have a Document before this can work. |
- Element(String name, [this.namespaceUri]) : super._(name); |
+ Element._(this.localName, [this.namespaceUri]) : super._(); |
- Element.tag(String name) : namespaceUri = null, super._(name); |
+ Element.tag(this.localName) : namespaceUri = Namespaces.html, super._(); |
static final _START_TAG_REGEXP = new RegExp('<(\\w+)'); |
@@ -528,9 +531,30 @@ class Element extends Node { |
int get nodeType => Node.ELEMENT_NODE; |
+ // TODO(jmesserly): we can make this faster |
+ Element get previousElementSibling { |
+ if (parentNode == null) return null; |
+ var siblings = parentNode.nodes; |
+ for (int i = siblings.indexOf(this) - 1; i >= 0; i--) { |
+ var s = siblings[i]; |
+ if (s is Element) return s; |
+ } |
+ return null; |
+ } |
+ |
+ Element get nextElementSibling { |
+ if (parentNode == null) return null; |
+ var siblings = parentNode.nodes; |
+ for (int i = siblings.indexOf(this) + 1; i < siblings.length; i++) { |
+ var s = siblings[i]; |
+ if (s is Element) return s; |
+ } |
+ return null; |
+ } |
+ |
String toString() { |
- if (namespaceUri == null) return "<$localName>"; |
- return "<${Namespaces.getPrefix(namespaceUri)} $localName>"; |
+ var prefix = Namespaces.getPrefix(namespaceUri); |
+ return "<${prefix == null ? '' : '$prefix '}$localName>"; |
} |
String get text => _getText(this); |
@@ -546,21 +570,17 @@ class Element extends Node { |
String get innerHtml => _innerHtml; |
// TODO(jmesserly): deprecate in favor of: |
// <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id_setInnerHtml> |
- set innerHtml(String value) { _innerHtml = value; } |
+ set innerHtml(String value) { |
+ nodes.clear(); |
+ // TODO(jmesserly): should be able to get the same effect by adding the |
+ // fragment directly. |
+ nodes.addAll(parseFragment(value, container: localName).nodes); |
+ } |
void _addOuterHtml(StringBuffer str) { |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments |
// Element is the most complicated one. |
- if (namespaceUri == null || |
- namespaceUri == Namespaces.html || |
- namespaceUri == Namespaces.mathml || |
- namespaceUri == Namespaces.svg) { |
- str.write('<$localName'); |
- } else { |
- // TODO(jmesserly): the spec doesn't define "qualified name". |
- // I'm not sure if this is correct, but it should parse reasonably. |
- str.write('<${Namespaces.getPrefix(namespaceUri)}:$localName'); |
- } |
+ str.write('<${_getSerializationPrefix(namespaceUri)}$localName'); |
if (attributes.length > 0) { |
attributes.forEach((key, v) { |
@@ -591,21 +611,56 @@ class Element extends Node { |
if (!isVoidElement(localName)) str.write('</$localName>'); |
} |
- Element clone() => new Element(localName, namespaceUri) |
- ..attributes = new LinkedHashMap.from(attributes); |
+ static String _getSerializationPrefix(String uri) { |
+ if (uri == null || |
+ uri == Namespaces.html || |
+ uri == Namespaces.mathml || |
+ uri == Namespaces.svg) { |
+ return ''; |
+ } |
+ var prefix = Namespaces.getPrefix(uri); |
+ // TODO(jmesserly): the spec doesn't define "qualified name". |
+ // I'm not sure if this is correct, but it should parse reasonably. |
+ return prefix == null ? '' : '$prefix:'; |
+ } |
+ |
+ Element clone(bool deep) { |
+ var result = new Element._(localName, namespaceUri) |
+ ..attributes = new LinkedHashMap.from(attributes); |
+ return _clone(result, deep); |
+ } |
+ // http://dom.spec.whatwg.org/#dom-element-id |
String get id { |
var result = attributes['id']; |
return result != null ? result : ''; |
} |
set id(String value) { |
- if (value == null) { |
- attributes.remove('id'); |
- } else { |
- attributes['id'] = value; |
- } |
+ attributes['id'] = '$value'; |
} |
+ |
+ // http://dom.spec.whatwg.org/#dom-element-classname |
+ String get className { |
+ var result = attributes['class']; |
+ return result != null ? result : ''; |
+ } |
+ |
+ set className(String value) { |
+ attributes['class'] = '$value'; |
+ } |
+ |
+ /** |
+ * The set of CSS classes applied to this element. |
+ * |
+ * This set makes it easy to add, remove or toggle the classes applied to |
+ * this element. |
+ * |
+ * element.classes.add('selected'); |
+ * element.classes.toggle('isOnline'); |
+ * element.classes.remove('selected'); |
+ */ |
+ CssClassSet get classes => new ElementCssClassSet(this); |
} |
class Comment extends Node { |
@@ -621,7 +676,7 @@ class Comment extends Node { |
str.write("<!--$data-->"); |
} |
- Comment clone() => new Comment(data); |
+ Comment clone(bool deep) => new Comment(data); |
String get text => data; |
set text(String value) { |
@@ -646,7 +701,7 @@ class NodeList extends ListProxy<Node> { |
// Note: we need to remove the node from its previous parent node, if any, |
// before updating its parent pointer to point at our parent. |
node.remove(); |
- node.parent = _parent; |
+ node.parentNode = _parent; |
return node; |
} |
@@ -681,12 +736,12 @@ class NodeList extends ListProxy<Node> { |
} |
} |
- Node removeLast() => super.removeLast()..parent = null; |
+ Node removeLast() => super.removeLast()..parentNode = null; |
- Node removeAt(int i) => super.removeAt(i)..parent = null; |
+ Node removeAt(int i) => super.removeAt(i)..parentNode = null; |
void clear() { |
- for (var node in this) node.parent = null; |
+ for (var node in this) node.parentNode = null; |
super.clear(); |
} |
@@ -695,7 +750,7 @@ class NodeList extends ListProxy<Node> { |
removeAt(index); |
insertAll(index, value.nodes); |
} else { |
- this[index].parent = null; |
+ this[index].parentNode = null; |
super[index] = _setParent(value); |
} |
} |
@@ -721,20 +776,20 @@ class NodeList extends ListProxy<Node> { |
} |
void removeRange(int start, int rangeLength) { |
- for (int i = start; i < rangeLength; i++) this[i].parent = null; |
+ for (int i = start; i < rangeLength; i++) this[i].parentNode = null; |
super.removeRange(start, rangeLength); |
} |
void removeWhere(bool test(Element e)) { |
for (var node in where(test)) { |
- node.parent = null; |
+ node.parentNode = null; |
} |
super.removeWhere(test); |
} |
void retainWhere(bool test(Element e)) { |
for (var node in where((n) => !test(n))) { |
- node.parent = null; |
+ node.parentNode = null; |
} |
super.retainWhere(test); |
} |