Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(145)

Side by Side Diff: pkg/third_party/html5lib/lib/dom.dart

Issue 268623002: [html5lib] implement querySelector/querySelectorAll (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/pkg.status ('k') | pkg/third_party/html5lib/lib/dom_parsing.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 /// A simple tree API that results from parsing html. Intended to be compatible 1 /// A simple tree API that results from parsing html. Intended to be compatible
2 /// with dart:html, but right now it resembles the classic JS DOM. 2 /// with dart:html, but it is missing many types and APIs.
3 library dom; 3 library dom;
4 4
5 // TODO(jmesserly): lots to do here. Originally I wanted to generate this using
6 // our Blink IDL generator, but another idea is to directly use the excellent
7 // http://dom.spec.whatwg.org/ and http://html.spec.whatwg.org/ and just
8 // implement that.
9
5 import 'dart:collection'; 10 import 'dart:collection';
6 import 'package:source_maps/span.dart' show FileSpan; 11 import 'package:source_maps/span.dart' show FileSpan;
7 12
8 import 'src/constants.dart'; 13 import 'src/constants.dart';
14 import 'src/css_class_set.dart';
9 import 'src/list_proxy.dart'; 15 import 'src/list_proxy.dart';
16 import 'src/query_selector.dart' as query;
10 import 'src/token.dart'; 17 import 'src/token.dart';
11 import 'src/tokenizer.dart'; 18 import 'src/tokenizer.dart';
12 import 'src/utils.dart';
13 import 'dom_parsing.dart'; 19 import 'dom_parsing.dart';
14 import 'parser.dart'; 20 import 'parser.dart';
15 21
22 export 'src/css_class_set.dart' show CssClassSet;
23
16 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes 24 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes
17 // that exposes namespace info. 25 // that exposes namespace info.
18 class AttributeName implements Comparable { 26 class AttributeName implements Comparable {
19 /// The namespace prefix, e.g. `xlink`. 27 /// The namespace prefix, e.g. `xlink`.
20 final String prefix; 28 final String prefix;
21 29
22 /// The attribute name, e.g. `title`. 30 /// The attribute name, e.g. `title`.
23 final String name; 31 final String name;
24 32
25 /// The namespace url, e.g. `http://www.w3.org/1999/xlink` 33 /// The namespace url, e.g. `http://www.w3.org/1999/xlink`
(...skipping 27 matching lines...) Expand all
53 if (cmp != 0) return cmp; 61 if (cmp != 0) return cmp;
54 return namespace.compareTo(other.namespace); 62 return namespace.compareTo(other.namespace);
55 } 63 }
56 64
57 bool operator ==(x) { 65 bool operator ==(x) {
58 if (x is! AttributeName) return false; 66 if (x is! AttributeName) return false;
59 return prefix == x.prefix && name == x.name && namespace == x.namespace; 67 return prefix == x.prefix && name == x.name && namespace == x.namespace;
60 } 68 }
61 } 69 }
62 70
71 // http://dom.spec.whatwg.org/#parentnode
72 abstract class _ParentNode implements Node {
73 // TODO(jmesserly): this is only a partial implementation
74
75 /// Seaches for the first descendant node matching the given selectors, using
76 /// a preorder traversal.
77 ///
78 /// NOTE: Not all selectors from
79 /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/)
80 /// are implemented. For example, nth-child does not implement An+B syntax
81 /// and *-of-type is not implemented. If a selector is not implemented this
82 /// method will throw [UniplmentedError].
83 Element querySelector(String selector) =>
84 query.querySelector(this, selector);
85
86 /// Returns all descendant nodes matching the given selectors, using a
87 /// preorder traversal.
88 ///
89 /// NOTE: Not all selectors from
90 /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/)
91 /// are implemented. For example, nth-child does not implement An+B syntax
92 /// and *-of-type is not implemented. If a selector is not implemented this
93 /// method will throw [UniplmentedError].
94 List<Element> querySelectorAll(String selector) =>
95 query.querySelectorAll(this, selector);
96 }
97
98 // http://dom.spec.whatwg.org/#interface-nonelementparentnode
99 abstract class _NonElementParentNode implements _ParentNode {
100 // TODO(jmesserly): could be faster, should throw on invalid id.
101 Element getElementById(String id) => querySelector('#$id');
102 }
103
104 // This doesn't exist as an interface in the spec, but it's useful to merge
105 // common methods from these:
106 // http://dom.spec.whatwg.org/#interface-document
107 // http://dom.spec.whatwg.org/#element
108 abstract class _ElementAndDocument implements _ParentNode {
109 // TODO(jmesserly): could be faster, should throw on invalid tag/class names.
110
111 List<Element> getElementsByTagName(String localName) =>
112 querySelectorAll(localName);
113
114 List<Element> getElementsByClassName(String classNames) =>
115 querySelectorAll(classNames.splitMapJoin(' ',
116 onNonMatch: (m) => m.isNotEmpty ? '.$m' : m,
117 onMatch: (m) => ''));
118 }
119
63 /// Really basic implementation of a DOM-core like Node. 120 /// Really basic implementation of a DOM-core like Node.
64 abstract class Node { 121 abstract class Node {
65 static const int ATTRIBUTE_NODE = 2; 122 static const int ATTRIBUTE_NODE = 2;
66 static const int CDATA_SECTION_NODE = 4; 123 static const int CDATA_SECTION_NODE = 4;
67 static const int COMMENT_NODE = 8; 124 static const int COMMENT_NODE = 8;
68 static const int DOCUMENT_FRAGMENT_NODE = 11; 125 static const int DOCUMENT_FRAGMENT_NODE = 11;
69 static const int DOCUMENT_NODE = 9; 126 static const int DOCUMENT_NODE = 9;
70 static const int DOCUMENT_TYPE_NODE = 10; 127 static const int DOCUMENT_TYPE_NODE = 10;
71 static const int ELEMENT_NODE = 1; 128 static const int ELEMENT_NODE = 1;
72 static const int ENTITY_NODE = 6; 129 static const int ENTITY_NODE = 6;
73 static const int ENTITY_REFERENCE_NODE = 5; 130 static const int ENTITY_REFERENCE_NODE = 5;
74 static const int NOTATION_NODE = 12; 131 static const int NOTATION_NODE = 12;
75 static const int PROCESSING_INSTRUCTION_NODE = 7; 132 static const int PROCESSING_INSTRUCTION_NODE = 7;
76 static const int TEXT_NODE = 3; 133 static const int TEXT_NODE = 3;
77 134
78 /// Note: For now we use it to implement the deprecated tagName property. 135 /// The parent of the current node (or null for the document node).
79 final String _tagName; 136 Node parentNode;
80 137
81 /// *Deprecated* use [Element.localName] instead. 138 /// The parent element of this node.
82 /// Note: after removal, this will be replaced by a correct version that 139 ///
83 /// returns uppercase [as specified](http://dom.spec.whatwg.org/#dom-element-t agname). 140 /// Returns null if this node either does not have a parent or its parent is
84 @deprecated String get tagName => _tagName; 141 /// not an element.
85 142 Element get parent => parentNode is Element ? parentNode : null;
86 /// The parent of the current node (or null for the document node).
87 Node parent;
88 143
89 // TODO(jmesserly): should move to Element. 144 // TODO(jmesserly): should move to Element.
90 /// A map holding name, value pairs for attributes of the node. 145 /// A map holding name, value pairs for attributes of the node.
91 /// 146 ///
92 /// Note that attribute order needs to be stable for serialization, so we use 147 /// Note that attribute order needs to be stable for serialization, so we use
93 /// a LinkedHashMap. Each key is a [String] or [AttributeName]. 148 /// a LinkedHashMap. Each key is a [String] or [AttributeName].
94 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); 149 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap();
95 150
96 /// A list of child nodes of the current node. This must 151 /// A list of child nodes of the current node. This must
97 /// include all elements but not necessarily other node types. 152 /// include all elements but not necessarily other node types.
98 final NodeList nodes = new NodeList._(); 153 final NodeList nodes = new NodeList._();
99 154
100 List<Element> _elements; 155 List<Element> _elements;
101 156
102 // TODO(jmesserly): consider using an Expando for this, and put it in 157 // TODO(jmesserly): consider using an Expando for this, and put it in
103 // dom_parsing. Need to check the performance affect. 158 // dom_parsing. Need to check the performance affect.
104 /// The source span of this node, if it was created by the [HtmlParser]. 159 /// The source span of this node, if it was created by the [HtmlParser].
105 FileSpan sourceSpan; 160 FileSpan sourceSpan;
106 161
107 /// The attribute spans if requested. Otherwise null. 162 /// The attribute spans if requested. Otherwise null.
108 LinkedHashMap<dynamic, FileSpan> _attributeSpans; 163 LinkedHashMap<dynamic, FileSpan> _attributeSpans;
109 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; 164 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans;
110 165
111 /// *Deprecated* use [new Element.tag] instead. 166 Node._() {
112 @deprecated Node(String tagName) : this._(tagName);
113
114 Node._([this._tagName]) {
115 nodes._parent = this; 167 nodes._parent = this;
116 } 168 }
117 169
118 /// If [sourceSpan] is available, this contains the spans of each attribute. 170 /// If [sourceSpan] is available, this contains the spans of each attribute.
119 /// The span of an attribute is the entire attribute, including the name and 171 /// The span of an attribute is the entire attribute, including the name and
120 /// quotes (if any). For example, the span of "attr" in `<a attr="value">` 172 /// quotes (if any). For example, the span of "attr" in `<a attr="value">`
121 /// would be the text `attr="value"`. 173 /// would be the text `attr="value"`.
122 LinkedHashMap<dynamic, FileSpan> get attributeSpans { 174 LinkedHashMap<dynamic, FileSpan> get attributeSpans {
123 _ensureAttributeSpans(); 175 _ensureAttributeSpans();
124 return _attributeSpans; 176 return _attributeSpans;
125 } 177 }
126 178
127 /// If [sourceSpan] is available, this contains the spans of each attribute's 179 /// If [sourceSpan] is available, this contains the spans of each attribute's
128 /// value. Unlike [attributeSpans], this span will inlcude only the value. 180 /// value. Unlike [attributeSpans], this span will inlcude only the value.
129 /// For example, the value span of "attr" in `<a attr="value">` would be the 181 /// For example, the value span of "attr" in `<a attr="value">` would be the
130 /// text `value`. 182 /// text `value`.
131 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans { 183 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans {
132 _ensureAttributeSpans(); 184 _ensureAttributeSpans();
133 return _attributeValueSpans; 185 return _attributeValueSpans;
134 } 186 }
135 187
136 List<Element> get children { 188 List<Element> get children {
137 if (_elements == null) { 189 if (_elements == null) {
138 _elements = new FilteredElementList(this); 190 _elements = new FilteredElementList(this);
139 } 191 }
140 return _elements; 192 return _elements;
141 } 193 }
142 194
143 // TODO(jmesserly): needs to support deep clone. 195 /// Returns a copy of this node.
144 /// Return a shallow copy of the current node i.e. a node with the same 196 ///
145 /// name and attributes but with no parent or child nodes. 197 /// If [deep] is `true`, then all of this node's children and decendents are
146 Node clone(); 198 /// copied as well. If [deep] is `false`, then only this node is copied.
147 199 Node clone(bool deep);
148 /// *Deprecated* use [Element.namespaceUri] instead.
149 @deprecated String get namespace => null;
150 200
151 int get nodeType; 201 int get nodeType;
152 202
153 /// *Deprecated* use [text], [Text.data] or [Comment.data].
154 @deprecated String get value => null;
155
156 /// *Deprecated* use [nodeType].
157 @deprecated int get $dom_nodeType => nodeType;
158
159 /// *Deprecated* use [Element.outerHtml]
160 @deprecated String get outerHtml => _outerHtml;
161
162 /// *Deprecated* use [Element.innerHtml]
163 @deprecated String get innerHtml => _innerHtml;
164 @deprecated set innerHtml(String value) { _innerHtml = value; }
165
166 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface 203 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface
167 String get _outerHtml { 204 String get _outerHtml {
168 var str = new StringBuffer(); 205 var str = new StringBuffer();
169 _addOuterHtml(str); 206 _addOuterHtml(str);
170 return str.toString(); 207 return str.toString();
171 } 208 }
172 209
173 String get _innerHtml { 210 String get _innerHtml {
174 var str = new StringBuffer(); 211 var str = new StringBuffer();
175 _addInnerHtml(str); 212 _addInnerHtml(str);
176 return str.toString(); 213 return str.toString();
177 } 214 }
178 215
179 set _innerHtml(String value) {
180 nodes.clear();
181 // TODO(jmesserly): should be able to get the same effect by adding the
182 // fragment directly.
183 nodes.addAll(parseFragment(value, container: _tagName).nodes);
184 }
185
186 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent 216 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent
187 String get text => null; 217 String get text => null;
188 set text(String value) {} 218 set text(String value) {}
189 219
190 void append(Node node) => nodes.add(node); 220 void append(Node node) => nodes.add(node);
191 221
192 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; 222 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null;
193 223
194 void _addOuterHtml(StringBuffer str); 224 void _addOuterHtml(StringBuffer str);
195 225
196 void _addInnerHtml(StringBuffer str) { 226 void _addInnerHtml(StringBuffer str) {
197 for (Node child in nodes) child._addOuterHtml(str); 227 for (Node child in nodes) child._addOuterHtml(str);
198 } 228 }
199 229
200 Node remove() { 230 Node remove() {
201 // TODO(jmesserly): is parent == null an error? 231 // TODO(jmesserly): is parent == null an error?
202 if (parent != null) { 232 if (parentNode != null) {
203 parent.nodes.remove(this); 233 parentNode.nodes.remove(this);
204 } 234 }
205 return this; 235 return this;
206 } 236 }
207 237
208 /// Insert [node] as a child of the current node, before [refNode] in the 238 /// Insert [node] as a child of the current node, before [refNode] in the
209 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode] 239 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode]
210 /// is not a child of the current node. If refNode is null, this adds to the 240 /// is not a child of the current node. If refNode is null, this adds to the
211 /// end of the list. 241 /// end of the list.
212 void insertBefore(Node node, Node refNode) { 242 void insertBefore(Node node, Node refNode) {
213 if (refNode == null) { 243 if (refNode == null) {
214 nodes.add(node); 244 nodes.add(node);
215 } else { 245 } else {
216 nodes.insert(nodes.indexOf(refNode), node); 246 nodes.insert(nodes.indexOf(refNode), node);
217 } 247 }
218 } 248 }
219 249
220 /// Replaces this node with another node. 250 /// Replaces this node with another node.
221 Node replaceWith(Node otherNode) { 251 Node replaceWith(Node otherNode) {
222 if (parent == null) { 252 if (parentNode == null) {
223 throw new UnsupportedError('Node must have a parent to replace it.'); 253 throw new UnsupportedError('Node must have a parent to replace it.');
224 } 254 }
225 parent.nodes[parent.nodes.indexOf(this)] = otherNode; 255 parentNode.nodes[parentNode.nodes.indexOf(this)] = otherNode;
226 return this; 256 return this;
227 } 257 }
228 258
229 // TODO(jmesserly): should this be a property or remove? 259 // TODO(jmesserly): should this be a property or remove?
230 /// Return true if the node has children or text. 260 /// Return true if the node has children or text.
231 bool hasContent() => nodes.length > 0; 261 bool hasContent() => nodes.length > 0;
232 262
233 /// *Deprecated* construct a pair using the namespaceUri and the name.
234 @deprecated Pair<String, String> get nameTuple =>
235 this is Element ? getElementNameTuple(this) : null;
236
237 /// Move all the children of the current node to [newParent]. 263 /// Move all the children of the current node to [newParent].
238 /// This is needed so that trees that don't store text as nodes move the 264 /// This is needed so that trees that don't store text as nodes move the
239 /// text in the correct way. 265 /// text in the correct way.
240 void reparentChildren(Node newParent) { 266 void reparentChildren(Node newParent) {
241 newParent.nodes.addAll(nodes); 267 newParent.nodes.addAll(nodes);
242 nodes.clear(); 268 nodes.clear();
243 } 269 }
244 270
245 /// *Deprecated* use [querySelector] instead.
246 @deprecated
247 Element query(String selectors) => querySelector(selectors);
248
249 /// *Deprecated* use [querySelectorAll] instead.
250 @deprecated
251 List<Element> queryAll(String selectors) => querySelectorAll(selectors);
252
253 /// Seaches for the first descendant node matching the given selectors, using a
254 /// preorder traversal. NOTE: right now, this supports only a single type
255 /// selectors, e.g. `node.query('div')`.
256
257 Element querySelector(String selectors) =>
258 _queryType(_typeSelector(selectors));
259
260 /// Returns all descendant nodes matching the given selectors, using a
261 /// preorder traversal. NOTE: right now, this supports only a single type
262 /// selectors, e.g. `node.queryAll('div')`.
263 List<Element> querySelectorAll(String selectors) {
264 var results = new List<Element>();
265 _queryAllType(_typeSelector(selectors), results);
266 return results;
267 }
268
269 bool hasChildNodes() => !nodes.isEmpty; 271 bool hasChildNodes() => !nodes.isEmpty;
270 272
271 bool contains(Node node) => nodes.contains(node); 273 bool contains(Node node) => nodes.contains(node);
272 274
273 String _typeSelector(String selectors) {
274 selectors = selectors.trim();
275 if (!_isTypeSelector(selectors)) {
276 throw new UnimplementedError('only type selectors are implemented');
277 }
278 return selectors;
279 }
280
281 /// Checks if this is a type selector. 275 /// Checks if this is a type selector.
282 /// See <http://www.w3.org/TR/CSS2/grammar.html>. 276 /// See <http://www.w3.org/TR/CSS2/grammar.html>.
283 /// Note: this doesn't support '*', the universal selector, non-ascii chars or 277 /// Note: this doesn't support '*', the universal selector, non-ascii chars or
284 /// escape chars. 278 /// escape chars.
285 bool _isTypeSelector(String selector) { 279 bool _isTypeSelector(String selector) {
286 // Parser: 280 // Parser:
287 281
288 // element_name 282 // element_name
289 // : IDENT | '*' 283 // : IDENT | '*'
290 // ; 284 // ;
(...skipping 20 matching lines...) Expand all
311 305
312 for (; i < len; i++) { 306 for (; i < len; i++) {
313 if (!isLetterOrDigit(selector[i]) && selector.codeUnitAt(i) != DASH) { 307 if (!isLetterOrDigit(selector[i]) && selector.codeUnitAt(i) != DASH) {
314 return false; 308 return false;
315 } 309 }
316 } 310 }
317 311
318 return true; 312 return true;
319 } 313 }
320 314
321 Element _queryType(String tag) {
322 for (var node in nodes) {
323 if (node is! Element) continue;
324 if (node.localName == tag) return node;
325 var result = node._queryType(tag);
326 if (result != null) return result;
327 }
328 return null;
329 }
330
331 void _queryAllType(String tag, List<Element> results) {
332 for (var node in nodes) {
333 if (node is! Element) continue;
334 if (node.localName == tag) results.add(node);
335 node._queryAllType(tag, results);
336 }
337 }
338
339 /// Initialize [attributeSpans] using [sourceSpan]. 315 /// Initialize [attributeSpans] using [sourceSpan].
340 void _ensureAttributeSpans() { 316 void _ensureAttributeSpans() {
341 if (_attributeSpans != null) return; 317 if (_attributeSpans != null) return;
342 318
343 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); 319 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>();
344 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); 320 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>();
345 321
346 if (sourceSpan == null) return; 322 if (sourceSpan == null) return;
347 323
348 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true, 324 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true,
349 attributeSpans: true); 325 attributeSpans: true);
350 326
351 tokenizer.moveNext(); 327 tokenizer.moveNext();
352 var token = tokenizer.current as StartTagToken; 328 var token = tokenizer.current as StartTagToken;
353 329
354 if (token.attributeSpans == null) return; // no attributes 330 if (token.attributeSpans == null) return; // no attributes
355 331
356 for (var attr in token.attributeSpans) { 332 for (var attr in token.attributeSpans) {
357 var offset = sourceSpan.start.offset; 333 var offset = sourceSpan.start.offset;
358 _attributeSpans[attr.name] = sourceSpan.file.span( 334 _attributeSpans[attr.name] = sourceSpan.file.span(
359 offset + attr.start, offset + attr.end); 335 offset + attr.start, offset + attr.end);
360 if (attr.startValue != null) { 336 if (attr.startValue != null) {
361 _attributeValueSpans[attr.name] = sourceSpan.file.span( 337 _attributeValueSpans[attr.name] = sourceSpan.file.span(
362 offset + attr.startValue, offset + attr.endValue); 338 offset + attr.startValue, offset + attr.endValue);
363 } 339 }
364 } 340 }
365 } 341 }
342
343 _clone(Node shallowClone, bool deep) {
344 if (deep) {
345 for (var child in nodes) {
346 shallowClone.append(child.clone(true));
347 }
348 }
349 return shallowClone;
350 }
366 } 351 }
367 352
368 class Document extends Node { 353 class Document extends Node
354 with _ParentNode, _NonElementParentNode, _ElementAndDocument {
355
369 Document() : super._(); 356 Document() : super._();
370 factory Document.html(String html) => parse(html); 357 factory Document.html(String html) => parse(html);
371 358
372 int get nodeType => Node.DOCUMENT_NODE; 359 int get nodeType => Node.DOCUMENT_NODE;
373 360
374 // TODO(jmesserly): optmize this if needed 361 // TODO(jmesserly): optmize this if needed
375 Element get documentElement => querySelector('html'); 362 Element get documentElement => querySelector('html');
376 Element get head => documentElement.querySelector('head'); 363 Element get head => documentElement.querySelector('head');
377 Element get body => documentElement.querySelector('body'); 364 Element get body => documentElement.querySelector('body');
378 365
379 /// Returns a fragment of HTML or XML that represents the element and its 366 /// Returns a fragment of HTML or XML that represents the element and its
380 /// contents. 367 /// contents.
381 // TODO(jmesserly): this API is not specified in: 368 // TODO(jmesserly): this API is not specified in:
382 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead 369 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead
383 // only Element has outerHtml. However it is quite useful. Should we move it 370 // only Element has outerHtml. However it is quite useful. Should we move it
384 // to dom_parsing, where we keep other custom APIs? 371 // to dom_parsing, where we keep other custom APIs?
385 String get outerHtml => _outerHtml; 372 String get outerHtml => _outerHtml;
386 373
387 String toString() => "#document"; 374 String toString() => "#document";
388 375
389 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); 376 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
390 377
391 Document clone() => new Document(); 378 Document clone(bool deep) => _clone(new Document(), deep);
379
380 Element createElement(String tag) => new Element.tag(tag);
381
382 // TODO(jmesserly): this is only a partial implementation of:
383 // http://dom.spec.whatwg.org/#dom-document-createelementns
384 Element createElementNS(String namespaceUri, String tag) {
385 if (namespaceUri == '') namespaceUri = null;
386 return new Element._(tag, namespaceUri);
387 }
388
389 DocumentFragment createDocumentFragment() => new DocumentFragment();
392 } 390 }
393 391
394 class DocumentFragment extends Document { 392 class DocumentFragment extends Node
395 DocumentFragment(); 393 with _ParentNode, _NonElementParentNode {
394
395 DocumentFragment() : super._();
396 factory DocumentFragment.html(String html) => parseFragment(html); 396 factory DocumentFragment.html(String html) => parseFragment(html);
397 397
398 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; 398 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE;
399 399
400 /// Returns a fragment of HTML or XML that represents the element and its
401 /// contents.
402 // TODO(jmesserly): this API is not specified in:
403 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead
404 // only Element has outerHtml. However it is quite useful. Should we move it
405 // to dom_parsing, where we keep other custom APIs?
406 String get outerHtml => _outerHtml;
407
400 String toString() => "#document-fragment"; 408 String toString() => "#document-fragment";
401 409
402 DocumentFragment clone() => new DocumentFragment(); 410 DocumentFragment clone(bool deep) => _clone(new DocumentFragment(), deep);
411
412 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
403 413
404 String get text => _getText(this); 414 String get text => _getText(this);
405 set text(String value) => _setText(this, value); 415 set text(String value) => _setText(this, value);
406 } 416 }
407 417
408 class DocumentType extends Node { 418 class DocumentType extends Node {
409 final String name; 419 final String name;
410 final String publicId; 420 final String publicId;
411 final String systemId; 421 final String systemId;
412 422
413 DocumentType(String name, this.publicId, this.systemId) 423 DocumentType(String name, this.publicId, this.systemId)
414 // Note: once Node.tagName is removed, don't pass "name" to super 424 // Note: once Node.tagName is removed, don't pass "name" to super
415 : name = name, super._(name); 425 : name = name, super._();
416 426
417 int get nodeType => Node.DOCUMENT_TYPE_NODE; 427 int get nodeType => Node.DOCUMENT_TYPE_NODE;
418 428
419 String toString() { 429 String toString() {
420 if (publicId != null || systemId != null) { 430 if (publicId != null || systemId != null) {
421 // TODO(jmesserly): the html5 serialization spec does not add these. But 431 // TODO(jmesserly): the html5 serialization spec does not add these. But
422 // it seems useful, and the parser can handle it, so for now keeping it. 432 // it seems useful, and the parser can handle it, so for now keeping it.
423 var pid = publicId != null ? publicId : ''; 433 var pid = publicId != null ? publicId : '';
424 var sid = systemId != null ? systemId : ''; 434 var sid = systemId != null ? systemId : '';
425 return '<!DOCTYPE $name "$pid" "$sid">'; 435 return '<!DOCTYPE $name "$pid" "$sid">';
426 } else { 436 } else {
427 return '<!DOCTYPE $name>'; 437 return '<!DOCTYPE $name>';
428 } 438 }
429 } 439 }
430 440
431 441
432 void _addOuterHtml(StringBuffer str) { 442 void _addOuterHtml(StringBuffer str) {
433 str.write(toString()); 443 str.write(toString());
434 } 444 }
435 445
436 DocumentType clone() => new DocumentType(name, publicId, systemId); 446 DocumentType clone(bool deep) => new DocumentType(name, publicId, systemId);
437 } 447 }
438 448
439 class Text extends Node { 449 class Text extends Node {
440 String data; 450 String data;
441 451
442 Text(this.data) : super._(); 452 Text(this.data) : super._();
443 453
444 /// *Deprecated* use [data].
445 @deprecated String get value => data;
446 @deprecated set value(String x) { data = x; }
447
448 int get nodeType => Node.TEXT_NODE; 454 int get nodeType => Node.TEXT_NODE;
449 455
450 String toString() => '"$data"'; 456 String toString() => '"$data"';
451 457
452 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this); 458 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this);
453 459
454 Text clone() => new Text(data); 460 Text clone(bool deep) => new Text(data);
455 461
456 String get text => data; 462 String get text => data;
457 set text(String value) { data = value; } 463 set text(String value) { data = value; }
458 } 464 }
459 465
460 class Element extends Node { 466 // TODO(jmesserly): Elements should have a pointer back to their document
467 class Element extends Node with _ParentNode, _ElementAndDocument {
461 final String namespaceUri; 468 final String namespaceUri;
462 469
463 @deprecated String get namespace => namespaceUri;
464
465 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name) 470 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name)
466 /// of this element. 471 /// of this element.
467 String get localName => _tagName; 472 final String localName;
468 473
469 // TODO(jmesserly): deprecate in favor of [Document.createElementNS]. 474 Element._(this.localName, [this.namespaceUri]) : super._();
470 // However we need every element to have a Document before this can work.
471 Element(String name, [this.namespaceUri]) : super._(name);
472 475
473 Element.tag(String name) : namespaceUri = null, super._(name); 476 Element.tag(this.localName) : namespaceUri = Namespaces.html, super._();
474 477
475 static final _START_TAG_REGEXP = new RegExp('<(\\w+)'); 478 static final _START_TAG_REGEXP = new RegExp('<(\\w+)');
476 479
477 static final _CUSTOM_PARENT_TAG_MAP = const { 480 static final _CUSTOM_PARENT_TAG_MAP = const {
478 'body': 'html', 481 'body': 'html',
479 'head': 'html', 482 'head': 'html',
480 'caption': 'table', 483 'caption': 'table',
481 'td': 'tr', 484 'td': 'tr',
482 'colgroup': 'table', 485 'colgroup': 'table',
483 'col': 'colgroup', 486 'col': 'colgroup',
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
521 } else { 524 } else {
522 throw new ArgumentError('HTML had ${fragment.children.length} ' 525 throw new ArgumentError('HTML had ${fragment.children.length} '
523 'top level elements but 1 expected'); 526 'top level elements but 1 expected');
524 } 527 }
525 element.remove(); 528 element.remove();
526 return element; 529 return element;
527 } 530 }
528 531
529 int get nodeType => Node.ELEMENT_NODE; 532 int get nodeType => Node.ELEMENT_NODE;
530 533
534 // TODO(jmesserly): we can make this faster
535 Element get previousElementSibling {
536 if (parentNode == null) return null;
537 var siblings = parentNode.nodes;
538 for (int i = siblings.indexOf(this) - 1; i >= 0; i--) {
539 var s = siblings[i];
540 if (s is Element) return s;
541 }
542 return null;
543 }
544
545 Element get nextElementSibling {
546 if (parentNode == null) return null;
547 var siblings = parentNode.nodes;
548 for (int i = siblings.indexOf(this) + 1; i < siblings.length; i++) {
549 var s = siblings[i];
550 if (s is Element) return s;
551 }
552 return null;
553 }
554
531 String toString() { 555 String toString() {
532 if (namespaceUri == null) return "<$localName>"; 556 var prefix = Namespaces.getPrefix(namespaceUri);
533 return "<${Namespaces.getPrefix(namespaceUri)} $localName>"; 557 return "<${prefix == null ? '' : '$prefix '}$localName>";
534 } 558 }
535 559
536 String get text => _getText(this); 560 String get text => _getText(this);
537 set text(String value) => _setText(this, value); 561 set text(String value) => _setText(this, value);
538 562
539 /// Returns a fragment of HTML or XML that represents the element and its 563 /// Returns a fragment of HTML or XML that represents the element and its
540 /// contents. 564 /// contents.
541 String get outerHtml => _outerHtml; 565 String get outerHtml => _outerHtml;
542 566
543 /// Returns a fragment of HTML or XML that represents the element's contents. 567 /// Returns a fragment of HTML or XML that represents the element's contents.
544 /// Can be set, to replace the contents of the element with nodes parsed from 568 /// Can be set, to replace the contents of the element with nodes parsed from
545 /// the given string. 569 /// the given string.
546 String get innerHtml => _innerHtml; 570 String get innerHtml => _innerHtml;
547 // TODO(jmesserly): deprecate in favor of: 571 // TODO(jmesserly): deprecate in favor of:
548 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id _setInnerHtml> 572 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id _setInnerHtml>
549 set innerHtml(String value) { _innerHtml = value; } 573 set innerHtml(String value) {
574 nodes.clear();
575 // TODO(jmesserly): should be able to get the same effect by adding the
576 // fragment directly.
577 nodes.addAll(parseFragment(value, container: localName).nodes);
578 }
550 579
551 void _addOuterHtml(StringBuffer str) { 580 void _addOuterHtml(StringBuffer str) {
552 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments 581 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments
553 // Element is the most complicated one. 582 // Element is the most complicated one.
554 if (namespaceUri == null || 583 str.write('<${_getSerializationPrefix(namespaceUri)}$localName');
555 namespaceUri == Namespaces.html ||
556 namespaceUri == Namespaces.mathml ||
557 namespaceUri == Namespaces.svg) {
558 str.write('<$localName');
559 } else {
560 // TODO(jmesserly): the spec doesn't define "qualified name".
561 // I'm not sure if this is correct, but it should parse reasonably.
562 str.write('<${Namespaces.getPrefix(namespaceUri)}:$localName');
563 }
564 584
565 if (attributes.length > 0) { 585 if (attributes.length > 0) {
566 attributes.forEach((key, v) { 586 attributes.forEach((key, v) {
567 // Note: AttributeName.toString handles serialization of attribute 587 // Note: AttributeName.toString handles serialization of attribute
568 // namespace, if needed. 588 // namespace, if needed.
569 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); 589 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"');
570 }); 590 });
571 } 591 }
572 592
573 str.write('>'); 593 str.write('>');
(...skipping 10 matching lines...) Expand all
584 } 604 }
585 605
586 _addInnerHtml(str); 606 _addInnerHtml(str);
587 } 607 }
588 608
589 // void elements must not have an end tag 609 // void elements must not have an end tag
590 // http://dev.w3.org/html5/markup/syntax.html#void-elements 610 // http://dev.w3.org/html5/markup/syntax.html#void-elements
591 if (!isVoidElement(localName)) str.write('</$localName>'); 611 if (!isVoidElement(localName)) str.write('</$localName>');
592 } 612 }
593 613
594 Element clone() => new Element(localName, namespaceUri) 614 static String _getSerializationPrefix(String uri) {
595 ..attributes = new LinkedHashMap.from(attributes); 615 if (uri == null ||
616 uri == Namespaces.html ||
617 uri == Namespaces.mathml ||
618 uri == Namespaces.svg) {
619 return '';
620 }
621 var prefix = Namespaces.getPrefix(uri);
622 // TODO(jmesserly): the spec doesn't define "qualified name".
623 // I'm not sure if this is correct, but it should parse reasonably.
624 return prefix == null ? '' : '$prefix:';
625 }
596 626
627 Element clone(bool deep) {
628 var result = new Element._(localName, namespaceUri)
629 ..attributes = new LinkedHashMap.from(attributes);
630 return _clone(result, deep);
631 }
632
633 // http://dom.spec.whatwg.org/#dom-element-id
597 String get id { 634 String get id {
598 var result = attributes['id']; 635 var result = attributes['id'];
599 return result != null ? result : ''; 636 return result != null ? result : '';
600 } 637 }
601 638
602 set id(String value) { 639 set id(String value) {
603 if (value == null) { 640 attributes['id'] = '$value';
604 attributes.remove('id');
605 } else {
606 attributes['id'] = value;
607 }
608 } 641 }
642
643 // http://dom.spec.whatwg.org/#dom-element-classname
644 String get className {
645 var result = attributes['class'];
646 return result != null ? result : '';
647 }
648
649 set className(String value) {
650 attributes['class'] = '$value';
651 }
652
653 /**
654 * The set of CSS classes applied to this element.
655 *
656 * This set makes it easy to add, remove or toggle the classes applied to
657 * this element.
658 *
659 * element.classes.add('selected');
660 * element.classes.toggle('isOnline');
661 * element.classes.remove('selected');
662 */
663 CssClassSet get classes => new ElementCssClassSet(this);
609 } 664 }
610 665
611 class Comment extends Node { 666 class Comment extends Node {
612 String data; 667 String data;
613 668
614 Comment(this.data) : super._(); 669 Comment(this.data) : super._();
615 670
616 int get nodeType => Node.COMMENT_NODE; 671 int get nodeType => Node.COMMENT_NODE;
617 672
618 String toString() => "<!-- $data -->"; 673 String toString() => "<!-- $data -->";
619 674
620 void _addOuterHtml(StringBuffer str) { 675 void _addOuterHtml(StringBuffer str) {
621 str.write("<!--$data-->"); 676 str.write("<!--$data-->");
622 } 677 }
623 678
624 Comment clone() => new Comment(data); 679 Comment clone(bool deep) => new Comment(data);
625 680
626 String get text => data; 681 String get text => data;
627 set text(String value) { 682 set text(String value) {
628 this.data = value; 683 this.data = value;
629 } 684 }
630 } 685 }
631 686
632 687
633 // TODO(jmesserly): fix this to extend one of the corelib classes if possible. 688 // TODO(jmesserly): fix this to extend one of the corelib classes if possible.
634 // (The requirement to remove the node from the old node list makes it tricky.) 689 // (The requirement to remove the node from the old node list makes it tricky.)
635 // TODO(jmesserly): is there any way to share code with the _NodeListImpl? 690 // TODO(jmesserly): is there any way to share code with the _NodeListImpl?
636 class NodeList extends ListProxy<Node> { 691 class NodeList extends ListProxy<Node> {
637 // Note: this is conceptually final, but because of circular reference 692 // Note: this is conceptually final, but because of circular reference
638 // between Node and NodeList we initialize it after construction. 693 // between Node and NodeList we initialize it after construction.
639 Node _parent; 694 Node _parent;
640 695
641 NodeList._(); 696 NodeList._();
642 697
643 Node get first => this[0]; 698 Node get first => this[0];
644 699
645 Node _setParent(Node node) { 700 Node _setParent(Node node) {
646 // Note: we need to remove the node from its previous parent node, if any, 701 // Note: we need to remove the node from its previous parent node, if any,
647 // before updating its parent pointer to point at our parent. 702 // before updating its parent pointer to point at our parent.
648 node.remove(); 703 node.remove();
649 node.parent = _parent; 704 node.parentNode = _parent;
650 return node; 705 return node;
651 } 706 }
652 707
653 void add(Node value) { 708 void add(Node value) {
654 if (value is DocumentFragment) { 709 if (value is DocumentFragment) {
655 addAll(value.nodes); 710 addAll(value.nodes);
656 } else { 711 } else {
657 super.add(_setParent(value)); 712 super.add(_setParent(value));
658 } 713 }
659 } 714 }
(...skipping 14 matching lines...) Expand all
674 } 729 }
675 730
676 void insert(int index, Node value) { 731 void insert(int index, Node value) {
677 if (value is DocumentFragment) { 732 if (value is DocumentFragment) {
678 insertAll(index, value.nodes); 733 insertAll(index, value.nodes);
679 } else { 734 } else {
680 super.insert(index, _setParent(value)); 735 super.insert(index, _setParent(value));
681 } 736 }
682 } 737 }
683 738
684 Node removeLast() => super.removeLast()..parent = null; 739 Node removeLast() => super.removeLast()..parentNode = null;
685 740
686 Node removeAt(int i) => super.removeAt(i)..parent = null; 741 Node removeAt(int i) => super.removeAt(i)..parentNode = null;
687 742
688 void clear() { 743 void clear() {
689 for (var node in this) node.parent = null; 744 for (var node in this) node.parentNode = null;
690 super.clear(); 745 super.clear();
691 } 746 }
692 747
693 void operator []=(int index, Node value) { 748 void operator []=(int index, Node value) {
694 if (value is DocumentFragment) { 749 if (value is DocumentFragment) {
695 removeAt(index); 750 removeAt(index);
696 insertAll(index, value.nodes); 751 insertAll(index, value.nodes);
697 } else { 752 } else {
698 this[index].parent = null; 753 this[index].parentNode = null;
699 super[index] = _setParent(value); 754 super[index] = _setParent(value);
700 } 755 }
701 } 756 }
702 757
703 // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see 758 // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see
704 // http://code.google.com/p/dart/issues/detail?id=5371 759 // http://code.google.com/p/dart/issues/detail?id=5371
705 void setRange(int start, int rangeLength, List<Node> from, 760 void setRange(int start, int rangeLength, List<Node> from,
706 [int startFrom = 0]) { 761 [int startFrom = 0]) {
707 if (from is NodeList) { 762 if (from is NodeList) {
708 // Note: this is presumed to make a copy 763 // Note: this is presumed to make a copy
709 from = from.sublist(startFrom, startFrom + rangeLength); 764 from = from.sublist(startFrom, startFrom + rangeLength);
710 } 765 }
711 // Note: see comment in [addAll]. We need to be careful about the order of 766 // Note: see comment in [addAll]. We need to be careful about the order of
712 // operations if [from] is also a NodeList. 767 // operations if [from] is also a NodeList.
713 for (int i = rangeLength - 1; i >= 0; i--) { 768 for (int i = rangeLength - 1; i >= 0; i--) {
714 this[start + i] = from[startFrom + i]; 769 this[start + i] = from[startFrom + i];
715 } 770 }
716 } 771 }
717 772
718 void replaceRange(int start, int end, Iterable<Node> newContents) { 773 void replaceRange(int start, int end, Iterable<Node> newContents) {
719 removeRange(start, end); 774 removeRange(start, end);
720 insertAll(start, newContents); 775 insertAll(start, newContents);
721 } 776 }
722 777
723 void removeRange(int start, int rangeLength) { 778 void removeRange(int start, int rangeLength) {
724 for (int i = start; i < rangeLength; i++) this[i].parent = null; 779 for (int i = start; i < rangeLength; i++) this[i].parentNode = null;
725 super.removeRange(start, rangeLength); 780 super.removeRange(start, rangeLength);
726 } 781 }
727 782
728 void removeWhere(bool test(Element e)) { 783 void removeWhere(bool test(Element e)) {
729 for (var node in where(test)) { 784 for (var node in where(test)) {
730 node.parent = null; 785 node.parentNode = null;
731 } 786 }
732 super.removeWhere(test); 787 super.removeWhere(test);
733 } 788 }
734 789
735 void retainWhere(bool test(Element e)) { 790 void retainWhere(bool test(Element e)) {
736 for (var node in where((n) => !test(n))) { 791 for (var node in where((n) => !test(n))) {
737 node.parent = null; 792 node.parentNode = null;
738 } 793 }
739 super.retainWhere(test); 794 super.retainWhere(test);
740 } 795 }
741 796
742 void insertAll(int index, Iterable<Node> collection) { 797 void insertAll(int index, Iterable<Node> collection) {
743 // Note: we need to be careful how we copy nodes. See note in addAll. 798 // Note: we need to be careful how we copy nodes. See note in addAll.
744 var list = _flattenDocFragments(collection); 799 var list = _flattenDocFragments(collection);
745 for (var node in list.reversed) _setParent(node); 800 for (var node in list.reversed) _setParent(node);
746 super.insertAll(index, list); 801 super.insertAll(index, list);
747 } 802 }
(...skipping 208 matching lines...) Expand 10 before | Expand all | Expand 10 after
956 1011
957 class _ConcatTextVisitor extends TreeVisitor { 1012 class _ConcatTextVisitor extends TreeVisitor {
958 final _str = new StringBuffer(); 1013 final _str = new StringBuffer();
959 1014
960 String toString() => _str.toString(); 1015 String toString() => _str.toString();
961 1016
962 visitText(Text node) { 1017 visitText(Text node) {
963 _str.write(node.data); 1018 _str.write(node.data);
964 } 1019 }
965 } 1020 }
OLDNEW
« no previous file with comments | « pkg/pkg.status ('k') | pkg/third_party/html5lib/lib/dom_parsing.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698