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

Side by Side Diff: html/lib/dom.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 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
« no previous file with comments | « glob/pubspec.yaml ('k') | html/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
(Empty)
1 /// A simple tree API that results from parsing html. Intended to be compatible
2 /// with dart:html, but it is missing many types and APIs.
3 library dom;
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
10 import 'dart:collection';
11 import 'package:source_span/source_span.dart';
12
13 import 'src/constants.dart';
14 import 'src/css_class_set.dart';
15 import 'src/list_proxy.dart';
16 import 'src/query_selector.dart' as query;
17 import 'src/token.dart';
18 import 'src/tokenizer.dart';
19 import 'dom_parsing.dart';
20 import 'parser.dart';
21
22 export 'src/css_class_set.dart' show CssClassSet;
23
24 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes
25 // that exposes namespace info.
26 class AttributeName implements Comparable {
27 /// The namespace prefix, e.g. `xlink`.
28 final String prefix;
29
30 /// The attribute name, e.g. `title`.
31 final String name;
32
33 /// The namespace url, e.g. `http://www.w3.org/1999/xlink`
34 final String namespace;
35
36 const AttributeName(this.prefix, this.name, this.namespace);
37
38 String toString() {
39 // Implement:
40 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments
41 // If we get here we know we are xml, xmlns, or xlink, because of
42 // [HtmlParser.adjustForeignAttriubtes] is the only place we create
43 // an AttributeName.
44 return prefix != null ? '$prefix:$name' : name;
45 }
46
47 int get hashCode {
48 int h = prefix.hashCode;
49 h = 37 * (h & 0x1FFFFF) + name.hashCode;
50 h = 37 * (h & 0x1FFFFF) + namespace.hashCode;
51 return h & 0x3FFFFFFF;
52 }
53
54 int compareTo(other) {
55 // Not sure about this sort order
56 if (other is! AttributeName) return 1;
57 int cmp = (prefix != null ? prefix : "")
58 .compareTo((other.prefix != null ? other.prefix : ""));
59 if (cmp != 0) return cmp;
60 cmp = name.compareTo(other.name);
61 if (cmp != 0) return cmp;
62 return namespace.compareTo(other.namespace);
63 }
64
65 bool operator ==(x) {
66 if (x is! AttributeName) return false;
67 return prefix == x.prefix && name == x.name && namespace == x.namespace;
68 }
69 }
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) => query.querySelector(this, selector);
84
85 /// Returns all descendant nodes matching the given selectors, using a
86 /// preorder traversal.
87 ///
88 /// NOTE: Not all selectors from
89 /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/)
90 /// are implemented. For example, nth-child does not implement An+B syntax
91 /// and *-of-type is not implemented. If a selector is not implemented this
92 /// method will throw [UniplmentedError].
93 List<Element> querySelectorAll(String selector) =>
94 query.querySelectorAll(this, selector);
95 }
96
97 // http://dom.spec.whatwg.org/#interface-nonelementparentnode
98 abstract class _NonElementParentNode implements _ParentNode {
99 // TODO(jmesserly): could be faster, should throw on invalid id.
100 Element getElementById(String id) => querySelector('#$id');
101 }
102
103 // This doesn't exist as an interface in the spec, but it's useful to merge
104 // common methods from these:
105 // http://dom.spec.whatwg.org/#interface-document
106 // http://dom.spec.whatwg.org/#element
107 abstract class _ElementAndDocument implements _ParentNode {
108 // TODO(jmesserly): could be faster, should throw on invalid tag/class names.
109
110 List<Element> getElementsByTagName(String localName) =>
111 querySelectorAll(localName);
112
113 List<Element> getElementsByClassName(String classNames) => querySelectorAll(
114 classNames.splitMapJoin(' ',
115 onNonMatch: (m) => m.isNotEmpty ? '.$m' : m, onMatch: (m) => ''));
116 }
117
118 /// Really basic implementation of a DOM-core like Node.
119 abstract class Node {
120 static const int ATTRIBUTE_NODE = 2;
121 static const int CDATA_SECTION_NODE = 4;
122 static const int COMMENT_NODE = 8;
123 static const int DOCUMENT_FRAGMENT_NODE = 11;
124 static const int DOCUMENT_NODE = 9;
125 static const int DOCUMENT_TYPE_NODE = 10;
126 static const int ELEMENT_NODE = 1;
127 static const int ENTITY_NODE = 6;
128 static const int ENTITY_REFERENCE_NODE = 5;
129 static const int NOTATION_NODE = 12;
130 static const int PROCESSING_INSTRUCTION_NODE = 7;
131 static const int TEXT_NODE = 3;
132
133 /// The parent of the current node (or null for the document node).
134 Node parentNode;
135
136 /// The parent element of this node.
137 ///
138 /// Returns null if this node either does not have a parent or its parent is
139 /// not an element.
140 Element get parent => parentNode is Element ? parentNode : null;
141
142 // TODO(jmesserly): should move to Element.
143 /// A map holding name, value pairs for attributes of the node.
144 ///
145 /// Note that attribute order needs to be stable for serialization, so we use
146 /// a LinkedHashMap. Each key is a [String] or [AttributeName].
147 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap();
148
149 /// A list of child nodes of the current node. This must
150 /// include all elements but not necessarily other node types.
151 final NodeList nodes = new NodeList._();
152
153 List<Element> _elements;
154
155 // TODO(jmesserly): consider using an Expando for this, and put it in
156 // dom_parsing. Need to check the performance affect.
157 /// The source span of this node, if it was created by the [HtmlParser].
158 FileSpan sourceSpan;
159
160 /// The attribute spans if requested. Otherwise null.
161 LinkedHashMap<dynamic, FileSpan> _attributeSpans;
162 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans;
163
164 Node._() {
165 nodes._parent = this;
166 }
167
168 /// If [sourceSpan] is available, this contains the spans of each attribute.
169 /// The span of an attribute is the entire attribute, including the name and
170 /// quotes (if any). For example, the span of "attr" in `<a attr="value">`
171 /// would be the text `attr="value"`.
172 LinkedHashMap<dynamic, FileSpan> get attributeSpans {
173 _ensureAttributeSpans();
174 return _attributeSpans;
175 }
176
177 /// If [sourceSpan] is available, this contains the spans of each attribute's
178 /// value. Unlike [attributeSpans], this span will inlcude only the value.
179 /// For example, the value span of "attr" in `<a attr="value">` would be the
180 /// text `value`.
181 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans {
182 _ensureAttributeSpans();
183 return _attributeValueSpans;
184 }
185
186 List<Element> get children {
187 if (_elements == null) {
188 _elements = new FilteredElementList(this);
189 }
190 return _elements;
191 }
192
193 /// Returns a copy of this node.
194 ///
195 /// If [deep] is `true`, then all of this node's children and decendents are
196 /// copied as well. If [deep] is `false`, then only this node is copied.
197 Node clone(bool deep);
198
199 int get nodeType;
200
201 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface
202 String get _outerHtml {
203 var str = new StringBuffer();
204 _addOuterHtml(str);
205 return str.toString();
206 }
207
208 String get _innerHtml {
209 var str = new StringBuffer();
210 _addInnerHtml(str);
211 return str.toString();
212 }
213
214 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent
215 String get text => null;
216 set text(String value) {}
217
218 void append(Node node) => nodes.add(node);
219
220 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null;
221
222 void _addOuterHtml(StringBuffer str);
223
224 void _addInnerHtml(StringBuffer str) {
225 for (Node child in nodes) child._addOuterHtml(str);
226 }
227
228 Node remove() {
229 // TODO(jmesserly): is parent == null an error?
230 if (parentNode != null) {
231 parentNode.nodes.remove(this);
232 }
233 return this;
234 }
235
236 /// Insert [node] as a child of the current node, before [refNode] in the
237 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode]
238 /// is not a child of the current node. If refNode is null, this adds to the
239 /// end of the list.
240 void insertBefore(Node node, Node refNode) {
241 if (refNode == null) {
242 nodes.add(node);
243 } else {
244 nodes.insert(nodes.indexOf(refNode), node);
245 }
246 }
247
248 /// Replaces this node with another node.
249 Node replaceWith(Node otherNode) {
250 if (parentNode == null) {
251 throw new UnsupportedError('Node must have a parent to replace it.');
252 }
253 parentNode.nodes[parentNode.nodes.indexOf(this)] = otherNode;
254 return this;
255 }
256
257 // TODO(jmesserly): should this be a property or remove?
258 /// Return true if the node has children or text.
259 bool hasContent() => nodes.length > 0;
260
261 /// Move all the children of the current node to [newParent].
262 /// This is needed so that trees that don't store text as nodes move the
263 /// text in the correct way.
264 void reparentChildren(Node newParent) {
265 newParent.nodes.addAll(nodes);
266 nodes.clear();
267 }
268
269 bool hasChildNodes() => !nodes.isEmpty;
270
271 bool contains(Node node) => nodes.contains(node);
272
273 /// Initialize [attributeSpans] using [sourceSpan].
274 void _ensureAttributeSpans() {
275 if (_attributeSpans != null) return;
276
277 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>();
278 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>();
279
280 if (sourceSpan == null) return;
281
282 var tokenizer = new HtmlTokenizer(sourceSpan.text,
283 generateSpans: true, attributeSpans: true);
284
285 tokenizer.moveNext();
286 var token = tokenizer.current as StartTagToken;
287
288 if (token.attributeSpans == null) return; // no attributes
289
290 for (var attr in token.attributeSpans) {
291 var offset = sourceSpan.start.offset;
292 _attributeSpans[attr.name] =
293 sourceSpan.file.span(offset + attr.start, offset + attr.end);
294 if (attr.startValue != null) {
295 _attributeValueSpans[attr.name] = sourceSpan.file.span(
296 offset + attr.startValue, offset + attr.endValue);
297 }
298 }
299 }
300
301 _clone(Node shallowClone, bool deep) {
302 if (deep) {
303 for (var child in nodes) {
304 shallowClone.append(child.clone(true));
305 }
306 }
307 return shallowClone;
308 }
309 }
310
311 class Document extends Node
312 with _ParentNode, _NonElementParentNode, _ElementAndDocument {
313 Document() : super._();
314 factory Document.html(String html) => parse(html);
315
316 int get nodeType => Node.DOCUMENT_NODE;
317
318 // TODO(jmesserly): optmize this if needed
319 Element get documentElement => querySelector('html');
320 Element get head => documentElement.querySelector('head');
321 Element get body => documentElement.querySelector('body');
322
323 /// Returns a fragment of HTML or XML that represents the element and its
324 /// contents.
325 // TODO(jmesserly): this API is not specified in:
326 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead
327 // only Element has outerHtml. However it is quite useful. Should we move it
328 // to dom_parsing, where we keep other custom APIs?
329 String get outerHtml => _outerHtml;
330
331 String toString() => "#document";
332
333 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
334
335 Document clone(bool deep) => _clone(new Document(), deep);
336
337 Element createElement(String tag) => new Element.tag(tag);
338
339 // TODO(jmesserly): this is only a partial implementation of:
340 // http://dom.spec.whatwg.org/#dom-document-createelementns
341 Element createElementNS(String namespaceUri, String tag) {
342 if (namespaceUri == '') namespaceUri = null;
343 return new Element._(tag, namespaceUri);
344 }
345
346 DocumentFragment createDocumentFragment() => new DocumentFragment();
347 }
348
349 class DocumentFragment extends Node with _ParentNode, _NonElementParentNode {
350 DocumentFragment() : super._();
351 factory DocumentFragment.html(String html) => parseFragment(html);
352
353 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE;
354
355 /// Returns a fragment of HTML or XML that represents the element and its
356 /// contents.
357 // TODO(jmesserly): this API is not specified in:
358 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead
359 // only Element has outerHtml. However it is quite useful. Should we move it
360 // to dom_parsing, where we keep other custom APIs?
361 String get outerHtml => _outerHtml;
362
363 String toString() => "#document-fragment";
364
365 DocumentFragment clone(bool deep) => _clone(new DocumentFragment(), deep);
366
367 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
368
369 String get text => _getText(this);
370 set text(String value) => _setText(this, value);
371 }
372
373 class DocumentType extends Node {
374 final String name;
375 final String publicId;
376 final String systemId;
377
378 DocumentType(String name, this.publicId, this.systemId)
379 // Note: once Node.tagName is removed, don't pass "name" to super
380 : name = name,
381 super._();
382
383 int get nodeType => Node.DOCUMENT_TYPE_NODE;
384
385 String toString() {
386 if (publicId != null || systemId != null) {
387 // TODO(jmesserly): the html5 serialization spec does not add these. But
388 // it seems useful, and the parser can handle it, so for now keeping it.
389 var pid = publicId != null ? publicId : '';
390 var sid = systemId != null ? systemId : '';
391 return '<!DOCTYPE $name "$pid" "$sid">';
392 } else {
393 return '<!DOCTYPE $name>';
394 }
395 }
396
397 void _addOuterHtml(StringBuffer str) {
398 str.write(toString());
399 }
400
401 DocumentType clone(bool deep) => new DocumentType(name, publicId, systemId);
402 }
403
404 class Text extends Node {
405 /// The text node's data, stored as either a String or StringBuffer.
406 /// We support storing a StringBuffer here to support fast [appendData].
407 /// It will flatten back to a String on read.
408 var _data;
409
410 Text(String data) : _data = data != null ? data : '', super._();
411
412 int get nodeType => Node.TEXT_NODE;
413
414 String get data => _data = _data.toString();
415 set data(String value) {
416 _data = value != null ? value : '';
417 }
418
419 String toString() => '"$data"';
420
421 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this);
422
423 Text clone(bool deep) => new Text(data);
424
425 void appendData(String data) {
426 if (_data is! StringBuffer) _data = new StringBuffer(_data);
427 StringBuffer sb = _data;
428 sb.write(data);
429 }
430
431 String get text => data;
432 set text(String value) {
433 data = value;
434 }
435 }
436
437 // TODO(jmesserly): Elements should have a pointer back to their document
438 class Element extends Node with _ParentNode, _ElementAndDocument {
439 final String namespaceUri;
440
441 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name)
442 /// of this element.
443 final String localName;
444
445 // TODO(jmesserly): consider using an Expando for this, and put it in
446 // dom_parsing. Need to check the performance affect.
447 /// The source span of the end tag this element, if it was created by the
448 /// [HtmlParser]. May be `null` if does not have an implicit end tag.
449 FileSpan endSourceSpan;
450
451 Element._(this.localName, [this.namespaceUri]) : super._();
452
453 Element.tag(this.localName)
454 : namespaceUri = Namespaces.html,
455 super._();
456
457 static final _START_TAG_REGEXP = new RegExp('<(\\w+)');
458
459 static final _CUSTOM_PARENT_TAG_MAP = const {
460 'body': 'html',
461 'head': 'html',
462 'caption': 'table',
463 'td': 'tr',
464 'colgroup': 'table',
465 'col': 'colgroup',
466 'tr': 'tbody',
467 'tbody': 'table',
468 'tfoot': 'table',
469 'thead': 'table',
470 'track': 'audio',
471 };
472
473 // TODO(jmesserly): this is from dart:html _ElementFactoryProvider...
474 // TODO(jmesserly): have a look at fixing some things in dart:html, in
475 // particular: is the parent tag map complete? Is it faster without regexp?
476 // TODO(jmesserly): for our version we can do something smarter in the parser.
477 // All we really need is to set the correct parse state.
478 factory Element.html(String html) {
479
480 // TODO(jacobr): this method can be made more robust and performant.
481 // 1) Cache the dummy parent elements required to use innerHTML rather than
482 // creating them every call.
483 // 2) Verify that the html does not contain leading or trailing text nodes.
484 // 3) Verify that the html does not contain both <head> and <body> tags.
485 // 4) Detatch the created element from its dummy parent.
486 String parentTag = 'div';
487 String tag;
488 final match = _START_TAG_REGEXP.firstMatch(html);
489 if (match != null) {
490 tag = match.group(1).toLowerCase();
491 if (_CUSTOM_PARENT_TAG_MAP.containsKey(tag)) {
492 parentTag = _CUSTOM_PARENT_TAG_MAP[tag];
493 }
494 }
495
496 var fragment = parseFragment(html, container: parentTag);
497 Element element;
498 if (fragment.children.length == 1) {
499 element = fragment.children[0];
500 } else if (parentTag == 'html' && fragment.children.length == 2) {
501 // You'll always get a head and a body when starting from html.
502 element = fragment.children[tag == 'head' ? 0 : 1];
503 } else {
504 throw new ArgumentError('HTML had ${fragment.children.length} '
505 'top level elements but 1 expected');
506 }
507 element.remove();
508 return element;
509 }
510
511 int get nodeType => Node.ELEMENT_NODE;
512
513 // TODO(jmesserly): we can make this faster
514 Element get previousElementSibling {
515 if (parentNode == null) return null;
516 var siblings = parentNode.nodes;
517 for (int i = siblings.indexOf(this) - 1; i >= 0; i--) {
518 var s = siblings[i];
519 if (s is Element) return s;
520 }
521 return null;
522 }
523
524 Element get nextElementSibling {
525 if (parentNode == null) return null;
526 var siblings = parentNode.nodes;
527 for (int i = siblings.indexOf(this) + 1; i < siblings.length; i++) {
528 var s = siblings[i];
529 if (s is Element) return s;
530 }
531 return null;
532 }
533
534 String toString() {
535 var prefix = Namespaces.getPrefix(namespaceUri);
536 return "<${prefix == null ? '' : '$prefix '}$localName>";
537 }
538
539 String get text => _getText(this);
540 set text(String value) => _setText(this, value);
541
542 /// Returns a fragment of HTML or XML that represents the element and its
543 /// contents.
544 String get outerHtml => _outerHtml;
545
546 /// Returns a fragment of HTML or XML that represents the element's contents.
547 /// Can be set, to replace the contents of the element with nodes parsed from
548 /// the given string.
549 String get innerHtml => _innerHtml;
550 // TODO(jmesserly): deprecate in favor of:
551 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id _setInnerHtml>
552 set innerHtml(String value) {
553 nodes.clear();
554 // TODO(jmesserly): should be able to get the same effect by adding the
555 // fragment directly.
556 nodes.addAll(parseFragment(value, container: localName).nodes);
557 }
558
559 void _addOuterHtml(StringBuffer str) {
560 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments
561 // Element is the most complicated one.
562 str.write('<');
563 str.write(_getSerializationPrefix(namespaceUri));
564 str.write(localName);
565
566 if (attributes.length > 0) {
567 attributes.forEach((key, v) {
568 // Note: AttributeName.toString handles serialization of attribute
569 // namespace, if needed.
570 str.write(' ');
571 str.write(key);
572 str.write('="');
573 str.write(htmlSerializeEscape(v, attributeMode: true));
574 str.write('"');
575 });
576 }
577
578 str.write('>');
579
580 if (nodes.length > 0) {
581 if (localName == 'pre' ||
582 localName == 'textarea' ||
583 localName == 'listing') {
584 final first = nodes[0];
585 if (first is Text && first.data.startsWith('\n')) {
586 // These nodes will remove a leading \n at parse time, so if we still
587 // have one, it means we started with two. Add it back.
588 str.write('\n');
589 }
590 }
591
592 _addInnerHtml(str);
593 }
594
595 // void elements must not have an end tag
596 // http://dev.w3.org/html5/markup/syntax.html#void-elements
597 if (!isVoidElement(localName)) str.write('</$localName>');
598 }
599
600 static String _getSerializationPrefix(String uri) {
601 if (uri == null ||
602 uri == Namespaces.html ||
603 uri == Namespaces.mathml ||
604 uri == Namespaces.svg) {
605 return '';
606 }
607 var prefix = Namespaces.getPrefix(uri);
608 // TODO(jmesserly): the spec doesn't define "qualified name".
609 // I'm not sure if this is correct, but it should parse reasonably.
610 return prefix == null ? '' : '$prefix:';
611 }
612
613 Element clone(bool deep) {
614 var result = new Element._(localName, namespaceUri)
615 ..attributes = new LinkedHashMap.from(attributes);
616 return _clone(result, deep);
617 }
618
619 // http://dom.spec.whatwg.org/#dom-element-id
620 String get id {
621 var result = attributes['id'];
622 return result != null ? result : '';
623 }
624
625 set id(String value) {
626 attributes['id'] = '$value';
627 }
628
629 // http://dom.spec.whatwg.org/#dom-element-classname
630 String get className {
631 var result = attributes['class'];
632 return result != null ? result : '';
633 }
634
635 set className(String value) {
636 attributes['class'] = '$value';
637 }
638
639 /**
640 * The set of CSS classes applied to this element.
641 *
642 * This set makes it easy to add, remove or toggle the classes applied to
643 * this element.
644 *
645 * element.classes.add('selected');
646 * element.classes.toggle('isOnline');
647 * element.classes.remove('selected');
648 */
649 CssClassSet get classes => new ElementCssClassSet(this);
650 }
651
652 class Comment extends Node {
653 String data;
654
655 Comment(this.data) : super._();
656
657 int get nodeType => Node.COMMENT_NODE;
658
659 String toString() => "<!-- $data -->";
660
661 void _addOuterHtml(StringBuffer str) {
662 str.write("<!--$data-->");
663 }
664
665 Comment clone(bool deep) => new Comment(data);
666
667 String get text => data;
668 set text(String value) {
669 this.data = value;
670 }
671 }
672
673 // TODO(jmesserly): fix this to extend one of the corelib classes if possible.
674 // (The requirement to remove the node from the old node list makes it tricky.)
675 // TODO(jmesserly): is there any way to share code with the _NodeListImpl?
676 class NodeList extends ListProxy<Node> {
677 // Note: this is conceptually final, but because of circular reference
678 // between Node and NodeList we initialize it after construction.
679 Node _parent;
680
681 NodeList._();
682
683 Node get first => this[0];
684
685 Node _setParent(Node node) {
686 // Note: we need to remove the node from its previous parent node, if any,
687 // before updating its parent pointer to point at our parent.
688 node.remove();
689 node.parentNode = _parent;
690 return node;
691 }
692
693 void add(Node value) {
694 if (value is DocumentFragment) {
695 addAll(value.nodes);
696 } else {
697 super.add(_setParent(value));
698 }
699 }
700
701 void addLast(Node value) => add(value);
702
703 void addAll(Iterable<Node> collection) {
704 // Note: we need to be careful if collection is another NodeList.
705 // In particular:
706 // 1. we need to copy the items before updating their parent pointers,
707 // _flattenDocFragments does a copy internally.
708 // 2. we should update parent pointers in reverse order. That way they
709 // are removed from the original NodeList (if any) from the end, which
710 // is faster.
711 var list = _flattenDocFragments(collection);
712 for (var node in list.reversed) _setParent(node);
713 super.addAll(list);
714 }
715
716 void insert(int index, Node value) {
717 if (value is DocumentFragment) {
718 insertAll(index, value.nodes);
719 } else {
720 super.insert(index, _setParent(value));
721 }
722 }
723
724 Node removeLast() => super.removeLast()..parentNode = null;
725
726 Node removeAt(int i) => super.removeAt(i)..parentNode = null;
727
728 void clear() {
729 for (var node in this) node.parentNode = null;
730 super.clear();
731 }
732
733 void operator []=(int index, Node value) {
734 if (value is DocumentFragment) {
735 removeAt(index);
736 insertAll(index, value.nodes);
737 } else {
738 this[index].parentNode = null;
739 super[index] = _setParent(value);
740 }
741 }
742
743 // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see
744 // http://code.google.com/p/dart/issues/detail?id=5371
745 void setRange(int start, int rangeLength, List<Node> from,
746 [int startFrom = 0]) {
747 if (from is NodeList) {
748 // Note: this is presumed to make a copy
749 from = from.sublist(startFrom, startFrom + rangeLength);
750 }
751 // Note: see comment in [addAll]. We need to be careful about the order of
752 // operations if [from] is also a NodeList.
753 for (int i = rangeLength - 1; i >= 0; i--) {
754 this[start + i] = from[startFrom + i];
755 }
756 }
757
758 void replaceRange(int start, int end, Iterable<Node> newContents) {
759 removeRange(start, end);
760 insertAll(start, newContents);
761 }
762
763 void removeRange(int start, int rangeLength) {
764 for (int i = start; i < rangeLength; i++) this[i].parentNode = null;
765 super.removeRange(start, rangeLength);
766 }
767
768 void removeWhere(bool test(Element e)) {
769 for (var node in where(test)) {
770 node.parentNode = null;
771 }
772 super.removeWhere(test);
773 }
774
775 void retainWhere(bool test(Element e)) {
776 for (var node in where((n) => !test(n))) {
777 node.parentNode = null;
778 }
779 super.retainWhere(test);
780 }
781
782 void insertAll(int index, Iterable<Node> collection) {
783 // Note: we need to be careful how we copy nodes. See note in addAll.
784 var list = _flattenDocFragments(collection);
785 for (var node in list.reversed) _setParent(node);
786 super.insertAll(index, list);
787 }
788
789 _flattenDocFragments(Iterable<Node> collection) {
790 // Note: this function serves two purposes:
791 // * it flattens document fragments
792 // * it creates a copy of [collections] when `collection is NodeList`.
793 var result = [];
794 for (var node in collection) {
795 if (node is DocumentFragment) {
796 result.addAll(node.nodes);
797 } else {
798 result.add(node);
799 }
800 }
801 return result;
802 }
803 }
804
805 /// An indexable collection of a node's descendants in the document tree,
806 /// filtered so that only elements are in the collection.
807 // TODO(jmesserly): this was copied from dart:html
808 // TODO(jmesserly): "implements List<Element>" is a workaround for analyzer bug.
809 class FilteredElementList extends IterableBase<Element> with ListMixin<Element>
810 implements List<Element> {
811 final Node _node;
812 final List<Node> _childNodes;
813
814 /// Creates a collection of the elements that descend from a node.
815 ///
816 /// Example usage:
817 ///
818 /// var filteredElements = new FilteredElementList(query("#container"));
819 /// // filteredElements is [a, b, c].
820 FilteredElementList(Node node)
821 : _childNodes = node.nodes,
822 _node = node;
823
824 // We can't memoize this, since it's possible that children will be messed
825 // with externally to this class.
826 //
827 // TODO(nweiz): we don't always need to create a new list. For example
828 // forEach, every, any, ... could directly work on the _childNodes.
829 List<Element> get _filtered =>
830 new List<Element>.from(_childNodes.where((n) => n is Element));
831
832 void forEach(void f(Element element)) {
833 _filtered.forEach(f);
834 }
835
836 void operator []=(int index, Element value) {
837 this[index].replaceWith(value);
838 }
839
840 void set length(int newLength) {
841 final len = this.length;
842 if (newLength >= len) {
843 return;
844 } else if (newLength < 0) {
845 throw new ArgumentError("Invalid list length");
846 }
847
848 removeRange(newLength, len);
849 }
850
851 String join([String separator = ""]) => _filtered.join(separator);
852
853 void add(Element value) {
854 _childNodes.add(value);
855 }
856
857 void addAll(Iterable<Element> iterable) {
858 for (Element element in iterable) {
859 add(element);
860 }
861 }
862
863 bool contains(Element element) {
864 return element is Element && _childNodes.contains(element);
865 }
866
867 Iterable<Element> get reversed => _filtered.reversed;
868
869 void sort([int compare(Element a, Element b)]) {
870 throw new UnsupportedError('TODO(jacobr): should we impl?');
871 }
872
873 void setRange(int start, int end, Iterable<Element> iterable,
874 [int skipCount = 0]) {
875 throw new UnimplementedError();
876 }
877
878 void fillRange(int start, int end, [Element fillValue]) {
879 throw new UnimplementedError();
880 }
881
882 void replaceRange(int start, int end, Iterable<Element> iterable) {
883 throw new UnimplementedError();
884 }
885
886 void removeRange(int start, int end) {
887 _filtered.sublist(start, end).forEach((el) => el.remove());
888 }
889
890 void clear() {
891 // Currently, ElementList#clear clears even non-element nodes, so we follow
892 // that behavior.
893 _childNodes.clear();
894 }
895
896 Element removeLast() {
897 final result = this.last;
898 if (result != null) {
899 result.remove();
900 }
901 return result;
902 }
903
904 Iterable map(f(Element element)) => _filtered.map(f);
905 Iterable<Element> where(bool f(Element element)) => _filtered.where(f);
906 Iterable expand(Iterable f(Element element)) => _filtered.expand(f);
907
908 void insert(int index, Element value) {
909 _childNodes.insert(index, value);
910 }
911
912 void insertAll(int index, Iterable<Element> iterable) {
913 _childNodes.insertAll(index, iterable);
914 }
915
916 Element removeAt(int index) {
917 final result = this[index];
918 result.remove();
919 return result;
920 }
921
922 bool remove(Object element) {
923 if (element is! Element) return false;
924 for (int i = 0; i < length; i++) {
925 Element indexElement = this[i];
926 if (identical(indexElement, element)) {
927 indexElement.remove();
928 return true;
929 }
930 }
931 return false;
932 }
933
934 Element reduce(Element combine(Element value, Element element)) {
935 return _filtered.reduce(combine);
936 }
937
938 dynamic fold(dynamic initialValue,
939 dynamic combine(dynamic previousValue, Element element)) {
940 return _filtered.fold(initialValue, combine);
941 }
942
943 bool every(bool f(Element element)) => _filtered.every(f);
944 bool any(bool f(Element element)) => _filtered.any(f);
945 List<Element> toList({bool growable: true}) =>
946 new List<Element>.from(this, growable: growable);
947 Set<Element> toSet() => new Set<Element>.from(this);
948 Element firstWhere(bool test(Element value), {Element orElse()}) {
949 return _filtered.firstWhere(test, orElse: orElse);
950 }
951
952 Element lastWhere(bool test(Element value), {Element orElse()}) {
953 return _filtered.lastWhere(test, orElse: orElse);
954 }
955
956 Element singleWhere(bool test(Element value)) {
957 return _filtered.singleWhere(test);
958 }
959
960 Element elementAt(int index) {
961 return this[index];
962 }
963
964 bool get isEmpty => _filtered.isEmpty;
965 int get length => _filtered.length;
966 Element operator [](int index) => _filtered[index];
967 Iterator<Element> get iterator => _filtered.iterator;
968 List<Element> sublist(int start, [int end]) => _filtered.sublist(start, end);
969 Iterable<Element> getRange(int start, int end) =>
970 _filtered.getRange(start, end);
971 int indexOf(Element element, [int start = 0]) =>
972 _filtered.indexOf(element, start);
973
974 int lastIndexOf(Element element, [int start = null]) {
975 if (start == null) start = length - 1;
976 return _filtered.lastIndexOf(element, start);
977 }
978
979 Element get first => _filtered.first;
980
981 Element get last => _filtered.last;
982
983 Element get single => _filtered.single;
984 }
985
986 // http://dom.spec.whatwg.org/#dom-node-textcontent
987 // For Element and DocumentFragment
988 String _getText(Node node) =>
989 (new _ConcatTextVisitor()..visit(node)).toString();
990
991 void _setText(Node node, String value) {
992 node.nodes.clear();
993 node.append(new Text(value));
994 }
995
996 class _ConcatTextVisitor extends TreeVisitor {
997 final _str = new StringBuffer();
998
999 String toString() => _str.toString();
1000
1001 visitText(Text node) {
1002 _str.write(node.data);
1003 }
1004 }
OLDNEW
« no previous file with comments | « glob/pubspec.yaml ('k') | html/lib/dom_parsing.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698