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

Side by Side Diff: observatory_pub_packages/html5lib/dom.dart

Issue 816693004: Add observatory_pub_packages snapshot to third_party (Closed) Base URL: http://dart.googlecode.com/svn/third_party/
Patch Set: Created 6 years 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
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 : "").compareTo(
58 (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) =>
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
120 /// Really basic implementation of a DOM-core like Node.
121 abstract class Node {
122 static const int ATTRIBUTE_NODE = 2;
123 static const int CDATA_SECTION_NODE = 4;
124 static const int COMMENT_NODE = 8;
125 static const int DOCUMENT_FRAGMENT_NODE = 11;
126 static const int DOCUMENT_NODE = 9;
127 static const int DOCUMENT_TYPE_NODE = 10;
128 static const int ELEMENT_NODE = 1;
129 static const int ENTITY_NODE = 6;
130 static const int ENTITY_REFERENCE_NODE = 5;
131 static const int NOTATION_NODE = 12;
132 static const int PROCESSING_INSTRUCTION_NODE = 7;
133 static const int TEXT_NODE = 3;
134
135 /// The parent of the current node (or null for the document node).
136 Node parentNode;
137
138 /// The parent element of this node.
139 ///
140 /// Returns null if this node either does not have a parent or its parent is
141 /// not an element.
142 Element get parent => parentNode is Element ? parentNode : null;
143
144 // TODO(jmesserly): should move to Element.
145 /// A map holding name, value pairs for attributes of the node.
146 ///
147 /// Note that attribute order needs to be stable for serialization, so we use
148 /// a LinkedHashMap. Each key is a [String] or [AttributeName].
149 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap();
150
151 /// A list of child nodes of the current node. This must
152 /// include all elements but not necessarily other node types.
153 final NodeList nodes = new NodeList._();
154
155 List<Element> _elements;
156
157 // TODO(jmesserly): consider using an Expando for this, and put it in
158 // dom_parsing. Need to check the performance affect.
159 /// The source span of this node, if it was created by the [HtmlParser].
160 FileSpan sourceSpan;
161
162 /// The attribute spans if requested. Otherwise null.
163 LinkedHashMap<dynamic, FileSpan> _attributeSpans;
164 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans;
165
166 Node._() {
167 nodes._parent = this;
168 }
169
170 /// If [sourceSpan] is available, this contains the spans of each attribute.
171 /// The span of an attribute is the entire attribute, including the name and
172 /// quotes (if any). For example, the span of "attr" in `<a attr="value">`
173 /// would be the text `attr="value"`.
174 LinkedHashMap<dynamic, FileSpan> get attributeSpans {
175 _ensureAttributeSpans();
176 return _attributeSpans;
177 }
178
179 /// If [sourceSpan] is available, this contains the spans of each attribute's
180 /// value. Unlike [attributeSpans], this span will inlcude only the value.
181 /// For example, the value span of "attr" in `<a attr="value">` would be the
182 /// text `value`.
183 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans {
184 _ensureAttributeSpans();
185 return _attributeValueSpans;
186 }
187
188 List<Element> get children {
189 if (_elements == null) {
190 _elements = new FilteredElementList(this);
191 }
192 return _elements;
193 }
194
195 /// Returns a copy of this node.
196 ///
197 /// If [deep] is `true`, then all of this node's children and decendents are
198 /// copied as well. If [deep] is `false`, then only this node is copied.
199 Node clone(bool deep);
200
201 int get nodeType;
202
203 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface
204 String get _outerHtml {
205 var str = new StringBuffer();
206 _addOuterHtml(str);
207 return str.toString();
208 }
209
210 String get _innerHtml {
211 var str = new StringBuffer();
212 _addInnerHtml(str);
213 return str.toString();
214 }
215
216 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent
217 String get text => null;
218 set text(String value) {}
219
220 void append(Node node) => nodes.add(node);
221
222 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null;
223
224 void _addOuterHtml(StringBuffer str);
225
226 void _addInnerHtml(StringBuffer str) {
227 for (Node child in nodes) child._addOuterHtml(str);
228 }
229
230 Node remove() {
231 // TODO(jmesserly): is parent == null an error?
232 if (parentNode != null) {
233 parentNode.nodes.remove(this);
234 }
235 return this;
236 }
237
238 /// Insert [node] as a child of the current node, before [refNode] in the
239 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode]
240 /// is not a child of the current node. If refNode is null, this adds to the
241 /// end of the list.
242 void insertBefore(Node node, Node refNode) {
243 if (refNode == null) {
244 nodes.add(node);
245 } else {
246 nodes.insert(nodes.indexOf(refNode), node);
247 }
248 }
249
250 /// Replaces this node with another node.
251 Node replaceWith(Node otherNode) {
252 if (parentNode == null) {
253 throw new UnsupportedError('Node must have a parent to replace it.');
254 }
255 parentNode.nodes[parentNode.nodes.indexOf(this)] = otherNode;
256 return this;
257 }
258
259 // TODO(jmesserly): should this be a property or remove?
260 /// Return true if the node has children or text.
261 bool hasContent() => nodes.length > 0;
262
263 /// Move all the children of the current node to [newParent].
264 /// This is needed so that trees that don't store text as nodes move the
265 /// text in the correct way.
266 void reparentChildren(Node newParent) {
267 newParent.nodes.addAll(nodes);
268 nodes.clear();
269 }
270
271 bool hasChildNodes() => !nodes.isEmpty;
272
273 bool contains(Node node) => nodes.contains(node);
274
275 /// Checks if this is a type selector.
276 /// See <http://www.w3.org/TR/CSS2/grammar.html>.
277 /// Note: this doesn't support '*', the universal selector, non-ascii chars or
278 /// escape chars.
279 bool _isTypeSelector(String selector) {
280 // Parser:
281
282 // element_name
283 // : IDENT | '*'
284 // ;
285
286 // Lexer:
287
288 // nmstart [_a-z]|{nonascii}|{escape}
289 // nmchar [_a-z0-9-]|{nonascii}|{escape}
290 // ident -?{nmstart}{nmchar}*
291 // nonascii [\240-\377]
292 // unicode \\{h}{1,6}(\r\n|[ \t\r\n\f])?
293 // escape {unicode}|\\[^\r\n\f0-9a-f]
294
295 // As mentioned above, no nonascii or escape support yet.
296 int len = selector.length;
297 if (len == 0) return false;
298
299 int i = 0;
300 const int DASH = 45;
301 if (selector.codeUnitAt(i) == DASH) i++;
302
303 if (i >= len || !isLetter(selector[i])) return false;
304 i++;
305
306 for (; i < len; i++) {
307 if (!isLetterOrDigit(selector[i]) && selector.codeUnitAt(i) != DASH) {
308 return false;
309 }
310 }
311
312 return true;
313 }
314
315 /// Initialize [attributeSpans] using [sourceSpan].
316 void _ensureAttributeSpans() {
317 if (_attributeSpans != null) return;
318
319 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>();
320 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>();
321
322 if (sourceSpan == null) return;
323
324 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true,
325 attributeSpans: true);
326
327 tokenizer.moveNext();
328 var token = tokenizer.current as StartTagToken;
329
330 if (token.attributeSpans == null) return; // no attributes
331
332 for (var attr in token.attributeSpans) {
333 var offset = sourceSpan.start.offset;
334 _attributeSpans[attr.name] = sourceSpan.file.span(
335 offset + attr.start, offset + attr.end);
336 if (attr.startValue != null) {
337 _attributeValueSpans[attr.name] = sourceSpan.file.span(
338 offset + attr.startValue, offset + attr.endValue);
339 }
340 }
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 }
351 }
352
353 class Document extends Node
354 with _ParentNode, _NonElementParentNode, _ElementAndDocument {
355
356 Document() : super._();
357 factory Document.html(String html) => parse(html);
358
359 int get nodeType => Node.DOCUMENT_NODE;
360
361 // TODO(jmesserly): optmize this if needed
362 Element get documentElement => querySelector('html');
363 Element get head => documentElement.querySelector('head');
364 Element get body => documentElement.querySelector('body');
365
366 /// Returns a fragment of HTML or XML that represents the element and its
367 /// contents.
368 // TODO(jmesserly): this API is not specified in:
369 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead
370 // only Element has outerHtml. However it is quite useful. Should we move it
371 // to dom_parsing, where we keep other custom APIs?
372 String get outerHtml => _outerHtml;
373
374 String toString() => "#document";
375
376 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
377
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();
390 }
391
392 class DocumentFragment extends Node
393 with _ParentNode, _NonElementParentNode {
394
395 DocumentFragment() : super._();
396 factory DocumentFragment.html(String html) => parseFragment(html);
397
398 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE;
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
408 String toString() => "#document-fragment";
409
410 DocumentFragment clone(bool deep) => _clone(new DocumentFragment(), deep);
411
412 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str);
413
414 String get text => _getText(this);
415 set text(String value) => _setText(this, value);
416 }
417
418 class DocumentType extends Node {
419 final String name;
420 final String publicId;
421 final String systemId;
422
423 DocumentType(String name, this.publicId, this.systemId)
424 // Note: once Node.tagName is removed, don't pass "name" to super
425 : name = name, super._();
426
427 int get nodeType => Node.DOCUMENT_TYPE_NODE;
428
429 String toString() {
430 if (publicId != null || systemId != null) {
431 // TODO(jmesserly): the html5 serialization spec does not add these. But
432 // it seems useful, and the parser can handle it, so for now keeping it.
433 var pid = publicId != null ? publicId : '';
434 var sid = systemId != null ? systemId : '';
435 return '<!DOCTYPE $name "$pid" "$sid">';
436 } else {
437 return '<!DOCTYPE $name>';
438 }
439 }
440
441
442 void _addOuterHtml(StringBuffer str) {
443 str.write(toString());
444 }
445
446 DocumentType clone(bool deep) => new DocumentType(name, publicId, systemId);
447 }
448
449 class Text extends Node {
450 String data;
451
452 Text(this.data) : super._();
453
454 int get nodeType => Node.TEXT_NODE;
455
456 String toString() => '"$data"';
457
458 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this);
459
460 Text clone(bool deep) => new Text(data);
461
462 String get text => data;
463 set text(String value) { data = value; }
464 }
465
466 // TODO(jmesserly): Elements should have a pointer back to their document
467 class Element extends Node with _ParentNode, _ElementAndDocument {
468 final String namespaceUri;
469
470 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name)
471 /// of this element.
472 final String localName;
473
474 Element._(this.localName, [this.namespaceUri]) : super._();
475
476 Element.tag(this.localName) : namespaceUri = Namespaces.html, super._();
477
478 static final _START_TAG_REGEXP = new RegExp('<(\\w+)');
479
480 static final _CUSTOM_PARENT_TAG_MAP = const {
481 'body': 'html',
482 'head': 'html',
483 'caption': 'table',
484 'td': 'tr',
485 'colgroup': 'table',
486 'col': 'colgroup',
487 'tr': 'tbody',
488 'tbody': 'table',
489 'tfoot': 'table',
490 'thead': 'table',
491 'track': 'audio',
492 };
493
494 // TODO(jmesserly): this is from dart:html _ElementFactoryProvider...
495 // TODO(jmesserly): have a look at fixing some things in dart:html, in
496 // particular: is the parent tag map complete? Is it faster without regexp?
497 // TODO(jmesserly): for our version we can do something smarter in the parser.
498 // All we really need is to set the correct parse state.
499 factory Element.html(String html) {
500
501 // TODO(jacobr): this method can be made more robust and performant.
502 // 1) Cache the dummy parent elements required to use innerHTML rather than
503 // creating them every call.
504 // 2) Verify that the html does not contain leading or trailing text nodes.
505 // 3) Verify that the html does not contain both <head> and <body> tags.
506 // 4) Detatch the created element from its dummy parent.
507 String parentTag = 'div';
508 String tag;
509 final match = _START_TAG_REGEXP.firstMatch(html);
510 if (match != null) {
511 tag = match.group(1).toLowerCase();
512 if (_CUSTOM_PARENT_TAG_MAP.containsKey(tag)) {
513 parentTag = _CUSTOM_PARENT_TAG_MAP[tag];
514 }
515 }
516
517 var fragment = parseFragment(html, container: parentTag);
518 Element element;
519 if (fragment.children.length == 1) {
520 element = fragment.children[0];
521 } else if (parentTag == 'html' && fragment.children.length == 2) {
522 // You'll always get a head and a body when starting from html.
523 element = fragment.children[tag == 'head' ? 0 : 1];
524 } else {
525 throw new ArgumentError('HTML had ${fragment.children.length} '
526 'top level elements but 1 expected');
527 }
528 element.remove();
529 return element;
530 }
531
532 int get nodeType => Node.ELEMENT_NODE;
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
555 String toString() {
556 var prefix = Namespaces.getPrefix(namespaceUri);
557 return "<${prefix == null ? '' : '$prefix '}$localName>";
558 }
559
560 String get text => _getText(this);
561 set text(String value) => _setText(this, value);
562
563 /// Returns a fragment of HTML or XML that represents the element and its
564 /// contents.
565 String get outerHtml => _outerHtml;
566
567 /// Returns a fragment of HTML or XML that represents the element's contents.
568 /// Can be set, to replace the contents of the element with nodes parsed from
569 /// the given string.
570 String get innerHtml => _innerHtml;
571 // TODO(jmesserly): deprecate in favor of:
572 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id _setInnerHtml>
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 }
579
580 void _addOuterHtml(StringBuffer str) {
581 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments
582 // Element is the most complicated one.
583 str.write('<${_getSerializationPrefix(namespaceUri)}$localName');
584
585 if (attributes.length > 0) {
586 attributes.forEach((key, v) {
587 // Note: AttributeName.toString handles serialization of attribute
588 // namespace, if needed.
589 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"');
590 });
591 }
592
593 str.write('>');
594
595 if (nodes.length > 0) {
596 if (localName == 'pre' || localName == 'textarea' ||
597 localName == 'listing') {
598 final first = nodes[0];
599 if (first is Text && first.data.startsWith('\n')) {
600 // These nodes will remove a leading \n at parse time, so if we still
601 // have one, it means we started with two. Add it back.
602 str.write('\n');
603 }
604 }
605
606 _addInnerHtml(str);
607 }
608
609 // void elements must not have an end tag
610 // http://dev.w3.org/html5/markup/syntax.html#void-elements
611 if (!isVoidElement(localName)) str.write('</$localName>');
612 }
613
614 static String _getSerializationPrefix(String uri) {
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 }
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
634 String get id {
635 var result = attributes['id'];
636 return result != null ? result : '';
637 }
638
639 set id(String value) {
640 attributes['id'] = '$value';
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);
664 }
665
666 class Comment extends Node {
667 String data;
668
669 Comment(this.data) : super._();
670
671 int get nodeType => Node.COMMENT_NODE;
672
673 String toString() => "<!-- $data -->";
674
675 void _addOuterHtml(StringBuffer str) {
676 str.write("<!--$data-->");
677 }
678
679 Comment clone(bool deep) => new Comment(data);
680
681 String get text => data;
682 set text(String value) {
683 this.data = value;
684 }
685 }
686
687
688 // TODO(jmesserly): fix this to extend one of the corelib classes if possible.
689 // (The requirement to remove the node from the old node list makes it tricky.)
690 // TODO(jmesserly): is there any way to share code with the _NodeListImpl?
691 class NodeList extends ListProxy<Node> {
692 // Note: this is conceptually final, but because of circular reference
693 // between Node and NodeList we initialize it after construction.
694 Node _parent;
695
696 NodeList._();
697
698 Node get first => this[0];
699
700 Node _setParent(Node node) {
701 // Note: we need to remove the node from its previous parent node, if any,
702 // before updating its parent pointer to point at our parent.
703 node.remove();
704 node.parentNode = _parent;
705 return node;
706 }
707
708 void add(Node value) {
709 if (value is DocumentFragment) {
710 addAll(value.nodes);
711 } else {
712 super.add(_setParent(value));
713 }
714 }
715
716 void addLast(Node value) => add(value);
717
718 void addAll(Iterable<Node> collection) {
719 // Note: we need to be careful if collection is another NodeList.
720 // In particular:
721 // 1. we need to copy the items before updating their parent pointers,
722 // _flattenDocFragments does a copy internally.
723 // 2. we should update parent pointers in reverse order. That way they
724 // are removed from the original NodeList (if any) from the end, which
725 // is faster.
726 var list = _flattenDocFragments(collection);
727 for (var node in list.reversed) _setParent(node);
728 super.addAll(list);
729 }
730
731 void insert(int index, Node value) {
732 if (value is DocumentFragment) {
733 insertAll(index, value.nodes);
734 } else {
735 super.insert(index, _setParent(value));
736 }
737 }
738
739 Node removeLast() => super.removeLast()..parentNode = null;
740
741 Node removeAt(int i) => super.removeAt(i)..parentNode = null;
742
743 void clear() {
744 for (var node in this) node.parentNode = null;
745 super.clear();
746 }
747
748 void operator []=(int index, Node value) {
749 if (value is DocumentFragment) {
750 removeAt(index);
751 insertAll(index, value.nodes);
752 } else {
753 this[index].parentNode = null;
754 super[index] = _setParent(value);
755 }
756 }
757
758 // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see
759 // http://code.google.com/p/dart/issues/detail?id=5371
760 void setRange(int start, int rangeLength, List<Node> from,
761 [int startFrom = 0]) {
762 if (from is NodeList) {
763 // Note: this is presumed to make a copy
764 from = from.sublist(startFrom, startFrom + rangeLength);
765 }
766 // Note: see comment in [addAll]. We need to be careful about the order of
767 // operations if [from] is also a NodeList.
768 for (int i = rangeLength - 1; i >= 0; i--) {
769 this[start + i] = from[startFrom + i];
770 }
771 }
772
773 void replaceRange(int start, int end, Iterable<Node> newContents) {
774 removeRange(start, end);
775 insertAll(start, newContents);
776 }
777
778 void removeRange(int start, int rangeLength) {
779 for (int i = start; i < rangeLength; i++) this[i].parentNode = null;
780 super.removeRange(start, rangeLength);
781 }
782
783 void removeWhere(bool test(Element e)) {
784 for (var node in where(test)) {
785 node.parentNode = null;
786 }
787 super.removeWhere(test);
788 }
789
790 void retainWhere(bool test(Element e)) {
791 for (var node in where((n) => !test(n))) {
792 node.parentNode = null;
793 }
794 super.retainWhere(test);
795 }
796
797 void insertAll(int index, Iterable<Node> collection) {
798 // Note: we need to be careful how we copy nodes. See note in addAll.
799 var list = _flattenDocFragments(collection);
800 for (var node in list.reversed) _setParent(node);
801 super.insertAll(index, list);
802 }
803
804 _flattenDocFragments(Iterable<Node> collection) {
805 // Note: this function serves two purposes:
806 // * it flattens document fragments
807 // * it creates a copy of [collections] when `collection is NodeList`.
808 var result = [];
809 for (var node in collection) {
810 if (node is DocumentFragment) {
811 result.addAll(node.nodes);
812 } else {
813 result.add(node);
814 }
815 }
816 return result;
817 }
818 }
819
820
821 /// An indexable collection of a node's descendants in the document tree,
822 /// filtered so that only elements are in the collection.
823 // TODO(jmesserly): this was copied from dart:html
824 // TODO(jmesserly): "implements List<Element>" is a workaround for analyzer bug.
825 class FilteredElementList extends IterableBase<Element> with ListMixin<Element>
826 implements List<Element> {
827
828 final Node _node;
829 final List<Node> _childNodes;
830
831 /// Creates a collection of the elements that descend from a node.
832 ///
833 /// Example usage:
834 ///
835 /// var filteredElements = new FilteredElementList(query("#container"));
836 /// // filteredElements is [a, b, c].
837 FilteredElementList(Node node): _childNodes = node.nodes, _node = node;
838
839 // We can't memoize this, since it's possible that children will be messed
840 // with externally to this class.
841 //
842 // TODO(nweiz): we don't always need to create a new list. For example
843 // forEach, every, any, ... could directly work on the _childNodes.
844 List<Element> get _filtered =>
845 new List<Element>.from(_childNodes.where((n) => n is Element));
846
847 void forEach(void f(Element element)) {
848 _filtered.forEach(f);
849 }
850
851 void operator []=(int index, Element value) {
852 this[index].replaceWith(value);
853 }
854
855 void set length(int newLength) {
856 final len = this.length;
857 if (newLength >= len) {
858 return;
859 } else if (newLength < 0) {
860 throw new ArgumentError("Invalid list length");
861 }
862
863 removeRange(newLength, len);
864 }
865
866 String join([String separator = ""]) => _filtered.join(separator);
867
868 void add(Element value) {
869 _childNodes.add(value);
870 }
871
872 void addAll(Iterable<Element> iterable) {
873 for (Element element in iterable) {
874 add(element);
875 }
876 }
877
878 bool contains(Element element) {
879 return element is Element && _childNodes.contains(element);
880 }
881
882 Iterable<Element> get reversed => _filtered.reversed;
883
884 void sort([int compare(Element a, Element b)]) {
885 throw new UnsupportedError('TODO(jacobr): should we impl?');
886 }
887
888 void setRange(int start, int end, Iterable<Element> iterable,
889 [int skipCount = 0]) {
890 throw new UnimplementedError();
891 }
892
893 void fillRange(int start, int end, [Element fillValue]) {
894 throw new UnimplementedError();
895 }
896
897 void replaceRange(int start, int end, Iterable<Element> iterable) {
898 throw new UnimplementedError();
899 }
900
901 void removeRange(int start, int end) {
902 _filtered.sublist(start, end).forEach((el) => el.remove());
903 }
904
905 void clear() {
906 // Currently, ElementList#clear clears even non-element nodes, so we follow
907 // that behavior.
908 _childNodes.clear();
909 }
910
911 Element removeLast() {
912 final result = this.last;
913 if (result != null) {
914 result.remove();
915 }
916 return result;
917 }
918
919 Iterable map(f(Element element)) => _filtered.map(f);
920 Iterable<Element> where(bool f(Element element)) => _filtered.where(f);
921 Iterable expand(Iterable f(Element element)) => _filtered.expand(f);
922
923 void insert(int index, Element value) {
924 _childNodes.insert(index, value);
925 }
926
927 void insertAll(int index, Iterable<Element> iterable) {
928 _childNodes.insertAll(index, iterable);
929 }
930
931 Element removeAt(int index) {
932 final result = this[index];
933 result.remove();
934 return result;
935 }
936
937 bool remove(Object element) {
938 if (element is! Element) return false;
939 for (int i = 0; i < length; i++) {
940 Element indexElement = this[i];
941 if (identical(indexElement, element)) {
942 indexElement.remove();
943 return true;
944 }
945 }
946 return false;
947 }
948
949 Element reduce(Element combine(Element value, Element element)) {
950 return _filtered.reduce(combine);
951 }
952
953 dynamic fold(dynamic initialValue,
954 dynamic combine(dynamic previousValue, Element element)) {
955 return _filtered.fold(initialValue, combine);
956 }
957
958 bool every(bool f(Element element)) => _filtered.every(f);
959 bool any(bool f(Element element)) => _filtered.any(f);
960 List<Element> toList({ bool growable: true }) =>
961 new List<Element>.from(this, growable: growable);
962 Set<Element> toSet() => new Set<Element>.from(this);
963 Element firstWhere(bool test(Element value), {Element orElse()}) {
964 return _filtered.firstWhere(test, orElse: orElse);
965 }
966
967 Element lastWhere(bool test(Element value), {Element orElse()}) {
968 return _filtered.lastWhere(test, orElse: orElse);
969 }
970
971 Element singleWhere(bool test(Element value)) {
972 return _filtered.singleWhere(test);
973 }
974
975 Element elementAt(int index) {
976 return this[index];
977 }
978
979 bool get isEmpty => _filtered.isEmpty;
980 int get length => _filtered.length;
981 Element operator [](int index) => _filtered[index];
982 Iterator<Element> get iterator => _filtered.iterator;
983 List<Element> sublist(int start, [int end]) =>
984 _filtered.sublist(start, end);
985 Iterable<Element> getRange(int start, int end) =>
986 _filtered.getRange(start, end);
987 int indexOf(Element element, [int start = 0]) =>
988 _filtered.indexOf(element, start);
989
990 int lastIndexOf(Element element, [int start = null]) {
991 if (start == null) start = length - 1;
992 return _filtered.lastIndexOf(element, start);
993 }
994
995 Element get first => _filtered.first;
996
997 Element get last => _filtered.last;
998
999 Element get single => _filtered.single;
1000 }
1001
1002 // http://dom.spec.whatwg.org/#dom-node-textcontent
1003 // For Element and DocumentFragment
1004 String _getText(Node node) =>
1005 (new _ConcatTextVisitor()..visit(node)).toString();
1006
1007 void _setText(Node node, String value) {
1008 node.nodes.clear();
1009 node.append(new Text(value));
1010 }
1011
1012 class _ConcatTextVisitor extends TreeVisitor {
1013 final _str = new StringBuffer();
1014
1015 String toString() => _str.toString();
1016
1017 visitText(Text node) {
1018 _str.write(node.data);
1019 }
1020 }
OLDNEW
« no previous file with comments | « observatory_pub_packages/csslib/visitor.dart ('k') | observatory_pub_packages/html5lib/dom_parsing.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698