OLD | NEW |
1 /** | 1 /** |
2 * A simple tree API that results from parsing html. Intended to be compatible | 2 * A simple tree API that results from parsing html. Intended to be compatible |
3 * with dart:html, but right now it resembles the classic JS DOM. | 3 * with dart:html, but right now it resembles the classic JS DOM. |
4 */ | 4 */ |
5 library dom; | 5 library dom; |
6 | 6 |
7 import 'dart:collection'; | 7 import 'dart:collection'; |
8 import 'package:source_maps/span.dart' show FileSpan; | 8 import 'package:source_maps/span.dart' show FileSpan; |
9 | 9 |
10 import 'src/constants.dart'; | 10 import 'src/constants.dart'; |
(...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
145 | 145 |
146 // TODO(jmesserly): needs to support deep clone. | 146 // TODO(jmesserly): needs to support deep clone. |
147 /** | 147 /** |
148 * Return a shallow copy of the current node i.e. a node with the same | 148 * Return a shallow copy of the current node i.e. a node with the same |
149 * name and attributes but with no parent or child nodes. | 149 * name and attributes but with no parent or child nodes. |
150 */ | 150 */ |
151 Node clone(); | 151 Node clone(); |
152 | 152 |
153 String get namespace => null; | 153 String get namespace => null; |
154 | 154 |
155 // TODO(jmesserly): do we need this here? | |
156 /** The value of the current node (applies to text nodes and comments). */ | |
157 String get value => null; | |
158 | |
159 // TODO(jmesserly): this is a workaround for http://dartbug.com/4754 | |
160 int get $dom_nodeType => nodeType; | |
161 | |
162 int get nodeType; | 155 int get nodeType; |
163 | 156 |
164 String get outerHtml { | 157 String get outerHtml { |
165 var str = new StringBuffer(); | 158 var str = new StringBuffer(); |
166 _addOuterHtml(str); | 159 _addOuterHtml(str); |
167 return str.toString(); | 160 return str.toString(); |
168 } | 161 } |
169 | 162 |
170 String get innerHtml { | 163 String get innerHtml { |
171 var str = new StringBuffer(); | 164 var str = new StringBuffer(); |
172 _addInnerHtml(str); | 165 _addInnerHtml(str); |
173 return str.toString(); | 166 return str.toString(); |
174 } | 167 } |
175 | 168 |
176 set innerHtml(String value) { | 169 set innerHtml(String value) { |
177 nodes.clear(); | 170 nodes.clear(); |
178 // TODO(jmesserly): should be able to get the same effect by adding the | 171 // TODO(jmesserly): should be able to get the same effect by adding the |
179 // fragment directly. | 172 // fragment directly. |
180 nodes.addAll(parseFragment(value, container: tagName).nodes); | 173 nodes.addAll(parseFragment(value, container: tagName).nodes); |
181 } | 174 } |
182 | 175 |
| 176 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent |
| 177 String get text => null; |
| 178 set text(String value) {} |
| 179 |
| 180 void append(Node node) => nodes.add(node); |
| 181 |
183 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; | 182 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; |
184 | 183 |
185 void _addOuterHtml(StringBuffer str); | 184 void _addOuterHtml(StringBuffer str); |
186 | 185 |
187 void _addInnerHtml(StringBuffer str) { | 186 void _addInnerHtml(StringBuffer str) { |
188 for (Node child in nodes) child._addOuterHtml(str); | 187 for (Node child in nodes) child._addOuterHtml(str); |
189 } | 188 } |
190 | 189 |
191 String toString() => tagName; | 190 String toString() => tagName; |
192 | 191 |
(...skipping 196 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
389 | 388 |
390 class DocumentFragment extends Document { | 389 class DocumentFragment extends Document { |
391 DocumentFragment(); | 390 DocumentFragment(); |
392 factory DocumentFragment.html(String html) => parseFragment(html); | 391 factory DocumentFragment.html(String html) => parseFragment(html); |
393 | 392 |
394 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; | 393 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; |
395 | 394 |
396 String toString() => "#document-fragment"; | 395 String toString() => "#document-fragment"; |
397 | 396 |
398 DocumentFragment clone() => new DocumentFragment(); | 397 DocumentFragment clone() => new DocumentFragment(); |
| 398 |
| 399 String get text => _getText(this); |
| 400 set text(String value) => _setText(this, value); |
399 } | 401 } |
400 | 402 |
401 class DocumentType extends Node { | 403 class DocumentType extends Node { |
402 final String publicId; | 404 final String publicId; |
403 final String systemId; | 405 final String systemId; |
404 | 406 |
405 DocumentType(String name, this.publicId, this.systemId) : super(name); | 407 DocumentType(String name, this.publicId, this.systemId) : super(name); |
406 | 408 |
407 int get nodeType => Node.DOCUMENT_TYPE_NODE; | 409 int get nodeType => Node.DOCUMENT_TYPE_NODE; |
408 | 410 |
(...skipping 11 matching lines...) Expand all Loading... |
420 | 422 |
421 | 423 |
422 void _addOuterHtml(StringBuffer str) { | 424 void _addOuterHtml(StringBuffer str) { |
423 str.write(toString()); | 425 str.write(toString()); |
424 } | 426 } |
425 | 427 |
426 DocumentType clone() => new DocumentType(tagName, publicId, systemId); | 428 DocumentType clone() => new DocumentType(tagName, publicId, systemId); |
427 } | 429 } |
428 | 430 |
429 class Text extends Node { | 431 class Text extends Node { |
430 // TODO(jmesserly): this should be text? | 432 String data; |
431 String value; | |
432 | 433 |
433 Text(this.value) : super(null); | 434 Text(this.data) : super(null); |
434 | 435 |
435 int get nodeType => Node.TEXT_NODE; | 436 int get nodeType => Node.TEXT_NODE; |
436 | 437 |
437 String toString() => '"$value"'; | 438 String toString() => '"$data"'; |
438 | 439 |
439 void _addOuterHtml(StringBuffer str) { | 440 void _addOuterHtml(StringBuffer str) { |
440 // Don't escape text for certain elements, notably <script>. | 441 // Don't escape text for certain elements, notably <script>. |
441 if (rcdataElements.contains(parent.tagName) || | 442 if (rcdataElements.contains(parent.tagName) || |
442 parent.tagName == 'plaintext') { | 443 parent.tagName == 'plaintext') { |
443 str.write(value); | 444 str.write(data); |
444 } else { | 445 } else { |
445 str.write(htmlSerializeEscape(value)); | 446 str.write(htmlSerializeEscape(data)); |
446 } | 447 } |
447 } | 448 } |
448 | 449 |
449 Text clone() => new Text(value); | 450 Text clone() => new Text(data); |
| 451 |
| 452 String get text => data; |
| 453 set text(String value) { data = value; } |
450 } | 454 } |
451 | 455 |
452 class Element extends Node { | 456 class Element extends Node { |
453 final String namespace; | 457 final String namespace; |
454 | 458 |
455 // TODO(jmesserly): deprecate in favor of Element.tag? Or rename? | 459 // TODO(jmesserly): deprecate in favor of Element.tag? Or rename? |
456 Element(String name, [this.namespace]) : super(name); | 460 Element(String name, [this.namespace]) : super(name); |
457 | 461 |
458 Element.tag(String name) : namespace = null, super(name); | 462 Element.tag(String name) : namespace = null, super(name); |
459 | 463 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
511 return element; | 515 return element; |
512 } | 516 } |
513 | 517 |
514 int get nodeType => Node.ELEMENT_NODE; | 518 int get nodeType => Node.ELEMENT_NODE; |
515 | 519 |
516 String toString() { | 520 String toString() { |
517 if (namespace == null) return "<$tagName>"; | 521 if (namespace == null) return "<$tagName>"; |
518 return "<${Namespaces.getPrefix(namespace)} $tagName>"; | 522 return "<${Namespaces.getPrefix(namespace)} $tagName>"; |
519 } | 523 } |
520 | 524 |
| 525 String get text => _getText(this); |
| 526 set text(String value) => _setText(this, value); |
| 527 |
521 void _addOuterHtml(StringBuffer str) { | 528 void _addOuterHtml(StringBuffer str) { |
522 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#
serializing-html-fragments | 529 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#
serializing-html-fragments |
523 // Element is the most complicated one. | 530 // Element is the most complicated one. |
524 if (namespace == null || | 531 if (namespace == null || |
525 namespace == Namespaces.html || | 532 namespace == Namespaces.html || |
526 namespace == Namespaces.mathml || | 533 namespace == Namespaces.mathml || |
527 namespace == Namespaces.svg) { | 534 namespace == Namespaces.svg) { |
528 str.write('<$tagName'); | 535 str.write('<$tagName'); |
529 } else { | 536 } else { |
530 // TODO(jmesserly): the spec doesn't define "qualified name". | 537 // TODO(jmesserly): the spec doesn't define "qualified name". |
531 // I'm not sure if this is correct, but it should parse reasonably. | 538 // I'm not sure if this is correct, but it should parse reasonably. |
532 str.write('<${Namespaces.getPrefix(namespace)}:$tagName'); | 539 str.write('<${Namespaces.getPrefix(namespace)}:$tagName'); |
533 } | 540 } |
534 | 541 |
535 if (attributes.length > 0) { | 542 if (attributes.length > 0) { |
536 attributes.forEach((key, v) { | 543 attributes.forEach((key, v) { |
537 // Note: AttributeName.toString handles serialization of attribute | 544 // Note: AttributeName.toString handles serialization of attribute |
538 // namespace, if needed. | 545 // namespace, if needed. |
539 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); | 546 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); |
540 }); | 547 }); |
541 } | 548 } |
542 | 549 |
543 str.write('>'); | 550 str.write('>'); |
544 | 551 |
545 if (nodes.length > 0) { | 552 if (nodes.length > 0) { |
546 if (tagName == 'pre' || tagName == 'textarea' || tagName == 'listing') { | 553 if (tagName == 'pre' || tagName == 'textarea' || tagName == 'listing') { |
547 if (nodes[0] is Text && nodes[0].value.startsWith('\n')) { | 554 final first = nodes[0]; |
| 555 if (first is Text && first.data.startsWith('\n')) { |
548 // These nodes will remove a leading \n at parse time, so if we still | 556 // These nodes will remove a leading \n at parse time, so if we still |
549 // have one, it means we started with two. Add it back. | 557 // have one, it means we started with two. Add it back. |
550 str.write('\n'); | 558 str.write('\n'); |
551 } | 559 } |
552 } | 560 } |
553 | 561 |
554 _addInnerHtml(str); | 562 _addInnerHtml(str); |
555 } | 563 } |
556 | 564 |
557 // void elements must not have an end tag | 565 // void elements must not have an end tag |
(...skipping 12 matching lines...) Expand all Loading... |
570 set id(String value) { | 578 set id(String value) { |
571 if (value == null) { | 579 if (value == null) { |
572 attributes.remove('id'); | 580 attributes.remove('id'); |
573 } else { | 581 } else { |
574 attributes['id'] = value; | 582 attributes['id'] = value; |
575 } | 583 } |
576 } | 584 } |
577 } | 585 } |
578 | 586 |
579 class Comment extends Node { | 587 class Comment extends Node { |
580 final String data; | 588 String data; |
581 | 589 |
582 Comment(this.data) : super(null); | 590 Comment(this.data) : super(null); |
583 | 591 |
584 int get nodeType => Node.COMMENT_NODE; | 592 int get nodeType => Node.COMMENT_NODE; |
585 | 593 |
586 String toString() => "<!-- $data -->"; | 594 String toString() => "<!-- $data -->"; |
587 | 595 |
588 void _addOuterHtml(StringBuffer str) { | 596 void _addOuterHtml(StringBuffer str) { |
589 str.write("<!--$data-->"); | 597 str.write("<!--$data-->"); |
590 } | 598 } |
591 | 599 |
592 Comment clone() => new Comment(data); | 600 Comment clone() => new Comment(data); |
| 601 |
| 602 String get text => data; |
| 603 set text(String value) { |
| 604 this.data = value; |
| 605 } |
593 } | 606 } |
594 | 607 |
595 | 608 |
596 // TODO(jmesserly): fix this to extend one of the corelib classes if possible. | 609 // TODO(jmesserly): fix this to extend one of the corelib classes if possible. |
597 // (The requirement to remove the node from the old node list makes it tricky.) | 610 // (The requirement to remove the node from the old node list makes it tricky.) |
598 // TODO(jmesserly): is there any way to share code with the _NodeListImpl? | 611 // TODO(jmesserly): is there any way to share code with the _NodeListImpl? |
599 class NodeList extends ListProxy<Node> { | 612 class NodeList extends ListProxy<Node> { |
600 // Note: this is conceptually final, but because of circular reference | 613 // Note: this is conceptually final, but because of circular reference |
601 // between Node and NodeList we initialize it after construction. | 614 // between Node and NodeList we initialize it after construction. |
602 Node _parent; | 615 Node _parent; |
(...skipping 271 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
874 if (start == null) start = length - 1; | 887 if (start == null) start = length - 1; |
875 return _filtered.lastIndexOf(element, start); | 888 return _filtered.lastIndexOf(element, start); |
876 } | 889 } |
877 | 890 |
878 Element get first => _filtered.first; | 891 Element get first => _filtered.first; |
879 | 892 |
880 Element get last => _filtered.last; | 893 Element get last => _filtered.last; |
881 | 894 |
882 Element get single => _filtered.single; | 895 Element get single => _filtered.single; |
883 } | 896 } |
| 897 |
| 898 // http://dom.spec.whatwg.org/#dom-node-textcontent |
| 899 // For Element and DocumentFragment |
| 900 String _getText(Node node) => |
| 901 (new _ConcatTextVisitor()..visit(node)).toString(); |
| 902 |
| 903 void _setText(Node node, String value) { |
| 904 node.nodes.clear(); |
| 905 node.append(new Text(value)); |
| 906 } |
| 907 |
| 908 class _ConcatTextVisitor extends TreeVisitor { |
| 909 final _str = new StringBuffer(); |
| 910 |
| 911 String toString() => _str.toString(); |
| 912 |
| 913 visitText(Text node) { |
| 914 _str.write(node.data); |
| 915 } |
| 916 } |
OLD | NEW |