| OLD | NEW |
| 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 right now it resembles the classic JS DOM. |
| 3 library dom; | 3 library dom; |
| 4 | 4 |
| 5 import 'dart:collection'; | 5 import 'dart:collection'; |
| 6 import 'package:source_maps/span.dart' show FileSpan; | 6 import 'package:source_maps/span.dart' show FileSpan; |
| 7 | 7 |
| 8 import 'src/constants.dart'; | 8 import 'src/constants.dart'; |
| 9 import 'src/list_proxy.dart'; | 9 import 'src/list_proxy.dart'; |
| 10 import 'src/token.dart'; | 10 import 'src/token.dart'; |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 68 static const int DOCUMENT_FRAGMENT_NODE = 11; | 68 static const int DOCUMENT_FRAGMENT_NODE = 11; |
| 69 static const int DOCUMENT_NODE = 9; | 69 static const int DOCUMENT_NODE = 9; |
| 70 static const int DOCUMENT_TYPE_NODE = 10; | 70 static const int DOCUMENT_TYPE_NODE = 10; |
| 71 static const int ELEMENT_NODE = 1; | 71 static const int ELEMENT_NODE = 1; |
| 72 static const int ENTITY_NODE = 6; | 72 static const int ENTITY_NODE = 6; |
| 73 static const int ENTITY_REFERENCE_NODE = 5; | 73 static const int ENTITY_REFERENCE_NODE = 5; |
| 74 static const int NOTATION_NODE = 12; | 74 static const int NOTATION_NODE = 12; |
| 75 static const int PROCESSING_INSTRUCTION_NODE = 7; | 75 static const int PROCESSING_INSTRUCTION_NODE = 7; |
| 76 static const int TEXT_NODE = 3; | 76 static const int TEXT_NODE = 3; |
| 77 | 77 |
| 78 // TODO(jmesserly): this should be on Element | 78 /// Note: For now we use it to implement the deprecated tagName property. |
| 79 /// The tag name associated with the node. | 79 final String _tagName; |
| 80 final String tagName; | 80 |
| 81 /// *Deprecated* use [Element.localName] instead. |
| 82 /// Note: after removal, this will be replaced by a correct version that |
| 83 /// returns uppercase [as specified](http://dom.spec.whatwg.org/#dom-element-t
agname). |
| 84 @deprecated String get tagName => _tagName; |
| 81 | 85 |
| 82 /// The parent of the current node (or null for the document node). | 86 /// The parent of the current node (or null for the document node). |
| 83 Node parent; | 87 Node parent; |
| 84 | 88 |
| 85 // TODO(jmesserly): should move to Element. | 89 // TODO(jmesserly): should move to Element. |
| 86 /// A map holding name, value pairs for attributes of the node. | 90 /// A map holding name, value pairs for attributes of the node. |
| 87 /// | 91 /// |
| 88 /// Note that attribute order needs to be stable for serialization, so we use | 92 /// Note that attribute order needs to be stable for serialization, so we use |
| 89 /// a LinkedHashMap. Each key is a [String] or [AttributeName]. | 93 /// a LinkedHashMap. Each key is a [String] or [AttributeName]. |
| 90 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); | 94 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); |
| 91 | 95 |
| 92 /// A list of child nodes of the current node. This must | 96 /// A list of child nodes of the current node. This must |
| 93 /// include all elements but not necessarily other node types. | 97 /// include all elements but not necessarily other node types. |
| 94 final NodeList nodes = new NodeList._(); | 98 final NodeList nodes = new NodeList._(); |
| 95 | 99 |
| 96 List<Element> _elements; | 100 List<Element> _elements; |
| 97 | 101 |
| 98 // TODO(jmesserly): consider using an Expando for this, and put it in | 102 // TODO(jmesserly): consider using an Expando for this, and put it in |
| 99 // dom_parsing. Need to check the performance affect. | 103 // dom_parsing. Need to check the performance affect. |
| 100 /// The source span of this node, if it was created by the [HtmlParser]. | 104 /// The source span of this node, if it was created by the [HtmlParser]. |
| 101 FileSpan sourceSpan; | 105 FileSpan sourceSpan; |
| 102 | 106 |
| 103 /// The attribute spans if requested. Otherwise null. | 107 /// The attribute spans if requested. Otherwise null. |
| 104 LinkedHashMap<dynamic, FileSpan> _attributeSpans; | 108 LinkedHashMap<dynamic, FileSpan> _attributeSpans; |
| 105 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; | 109 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; |
| 106 | 110 |
| 107 Node(this.tagName) { | 111 /// *Deprecated* use [new Element.tag] instead. |
| 112 @deprecated Node(String tagName) : this._(tagName); |
| 113 |
| 114 Node._([this._tagName]) { |
| 108 nodes._parent = this; | 115 nodes._parent = this; |
| 109 } | 116 } |
| 110 | 117 |
| 111 /// If [sourceSpan] is available, this contains the spans of each attribute. | 118 /// If [sourceSpan] is available, this contains the spans of each attribute. |
| 112 /// The span of an attribute is the entire attribute, including the name and | 119 /// The span of an attribute is the entire attribute, including the name and |
| 113 /// quotes (if any). For example, the span of "attr" in `<a attr="value">` | 120 /// quotes (if any). For example, the span of "attr" in `<a attr="value">` |
| 114 /// would be the text `attr="value"`. | 121 /// would be the text `attr="value"`. |
| 115 LinkedHashMap<dynamic, FileSpan> get attributeSpans { | 122 LinkedHashMap<dynamic, FileSpan> get attributeSpans { |
| 116 _ensureAttributeSpans(); | 123 _ensureAttributeSpans(); |
| 117 return _attributeSpans; | 124 return _attributeSpans; |
| (...skipping 13 matching lines...) Expand all Loading... |
| 131 _elements = new FilteredElementList(this); | 138 _elements = new FilteredElementList(this); |
| 132 } | 139 } |
| 133 return _elements; | 140 return _elements; |
| 134 } | 141 } |
| 135 | 142 |
| 136 // TODO(jmesserly): needs to support deep clone. | 143 // TODO(jmesserly): needs to support deep clone. |
| 137 /// Return a shallow copy of the current node i.e. a node with the same | 144 /// Return a shallow copy of the current node i.e. a node with the same |
| 138 /// name and attributes but with no parent or child nodes. | 145 /// name and attributes but with no parent or child nodes. |
| 139 Node clone(); | 146 Node clone(); |
| 140 | 147 |
| 141 String get namespace => null; | 148 /// *Deprecated* use [Element.namespaceUri] instead. |
| 149 @deprecated String get namespace => null; |
| 142 | 150 |
| 143 int get nodeType; | 151 int get nodeType; |
| 144 | 152 |
| 145 /// *Deprecated* use [text], [Text.data] or [Comment.data]. | 153 /// *Deprecated* use [text], [Text.data] or [Comment.data]. |
| 146 @deprecated String get value => null; | 154 @deprecated String get value => null; |
| 147 | 155 |
| 148 /// *Deprecated* use [nodeType]. | 156 /// *Deprecated* use [nodeType]. |
| 149 @deprecated int get $dom_nodeType => nodeType; | 157 @deprecated int get $dom_nodeType => nodeType; |
| 150 | 158 |
| 151 String get outerHtml { | 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 |
| 167 String get _outerHtml { |
| 152 var str = new StringBuffer(); | 168 var str = new StringBuffer(); |
| 153 _addOuterHtml(str); | 169 _addOuterHtml(str); |
| 154 return str.toString(); | 170 return str.toString(); |
| 155 } | 171 } |
| 156 | 172 |
| 157 String get innerHtml { | 173 String get _innerHtml { |
| 158 var str = new StringBuffer(); | 174 var str = new StringBuffer(); |
| 159 _addInnerHtml(str); | 175 _addInnerHtml(str); |
| 160 return str.toString(); | 176 return str.toString(); |
| 161 } | 177 } |
| 162 | 178 |
| 163 set innerHtml(String value) { | 179 set _innerHtml(String value) { |
| 164 nodes.clear(); | 180 nodes.clear(); |
| 165 // TODO(jmesserly): should be able to get the same effect by adding the | 181 // TODO(jmesserly): should be able to get the same effect by adding the |
| 166 // fragment directly. | 182 // fragment directly. |
| 167 nodes.addAll(parseFragment(value, container: tagName).nodes); | 183 nodes.addAll(parseFragment(value, container: _tagName).nodes); |
| 168 } | 184 } |
| 169 | 185 |
| 170 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent | 186 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent |
| 171 String get text => null; | 187 String get text => null; |
| 172 set text(String value) {} | 188 set text(String value) {} |
| 173 | 189 |
| 174 void append(Node node) => nodes.add(node); | 190 void append(Node node) => nodes.add(node); |
| 175 | 191 |
| 176 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; | 192 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; |
| 177 | 193 |
| 178 void _addOuterHtml(StringBuffer str); | 194 void _addOuterHtml(StringBuffer str); |
| 179 | 195 |
| 180 void _addInnerHtml(StringBuffer str) { | 196 void _addInnerHtml(StringBuffer str) { |
| 181 for (Node child in nodes) child._addOuterHtml(str); | 197 for (Node child in nodes) child._addOuterHtml(str); |
| 182 } | 198 } |
| 183 | 199 |
| 184 String toString() => tagName; | |
| 185 | |
| 186 Node remove() { | 200 Node remove() { |
| 187 // TODO(jmesserly): is parent == null an error? | 201 // TODO(jmesserly): is parent == null an error? |
| 188 if (parent != null) { | 202 if (parent != null) { |
| 189 parent.nodes.remove(this); | 203 parent.nodes.remove(this); |
| 190 } | 204 } |
| 191 return this; | 205 return this; |
| 192 } | 206 } |
| 193 | 207 |
| 194 /// Insert [node] as a child of the current node, before [refNode] in the | 208 /// Insert [node] as a child of the current node, before [refNode] in the |
| 195 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode] | 209 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode] |
| (...skipping 13 matching lines...) Expand all Loading... |
| 209 throw new UnsupportedError('Node must have a parent to replace it.'); | 223 throw new UnsupportedError('Node must have a parent to replace it.'); |
| 210 } | 224 } |
| 211 parent.nodes[parent.nodes.indexOf(this)] = otherNode; | 225 parent.nodes[parent.nodes.indexOf(this)] = otherNode; |
| 212 return this; | 226 return this; |
| 213 } | 227 } |
| 214 | 228 |
| 215 // TODO(jmesserly): should this be a property or remove? | 229 // TODO(jmesserly): should this be a property or remove? |
| 216 /// Return true if the node has children or text. | 230 /// Return true if the node has children or text. |
| 217 bool hasContent() => nodes.length > 0; | 231 bool hasContent() => nodes.length > 0; |
| 218 | 232 |
| 219 Pair<String, String> get nameTuple { | 233 /// *Deprecated* construct a pair using the namespaceUri and the name. |
| 220 var ns = namespace != null ? namespace : Namespaces.html; | 234 @deprecated Pair<String, String> get nameTuple => |
| 221 return new Pair(ns, tagName); | 235 this is Element ? getElementNameTuple(this) : null; |
| 222 } | |
| 223 | 236 |
| 224 /// Move all the children of the current node to [newParent]. | 237 /// Move all the children of the current node to [newParent]. |
| 225 /// This is needed so that trees that don't store text as nodes move the | 238 /// This is needed so that trees that don't store text as nodes move the |
| 226 /// text in the correct way. | 239 /// text in the correct way. |
| 227 void reparentChildren(Node newParent) { | 240 void reparentChildren(Node newParent) { |
| 228 newParent.nodes.addAll(nodes); | 241 newParent.nodes.addAll(nodes); |
| 229 nodes.clear(); | 242 nodes.clear(); |
| 230 } | 243 } |
| 231 | 244 |
| 232 /// *Deprecated* use [querySelector] instead. | 245 /// *Deprecated* use [querySelector] instead. |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 301 return false; | 314 return false; |
| 302 } | 315 } |
| 303 } | 316 } |
| 304 | 317 |
| 305 return true; | 318 return true; |
| 306 } | 319 } |
| 307 | 320 |
| 308 Element _queryType(String tag) { | 321 Element _queryType(String tag) { |
| 309 for (var node in nodes) { | 322 for (var node in nodes) { |
| 310 if (node is! Element) continue; | 323 if (node is! Element) continue; |
| 311 if (node.tagName == tag) return node; | 324 if (node.localName == tag) return node; |
| 312 var result = node._queryType(tag); | 325 var result = node._queryType(tag); |
| 313 if (result != null) return result; | 326 if (result != null) return result; |
| 314 } | 327 } |
| 315 return null; | 328 return null; |
| 316 } | 329 } |
| 317 | 330 |
| 318 void _queryAllType(String tag, List<Element> results) { | 331 void _queryAllType(String tag, List<Element> results) { |
| 319 for (var node in nodes) { | 332 for (var node in nodes) { |
| 320 if (node is! Element) continue; | 333 if (node is! Element) continue; |
| 321 if (node.tagName == tag) results.add(node); | 334 if (node.localName == tag) results.add(node); |
| 322 node._queryAllType(tag, results); | 335 node._queryAllType(tag, results); |
| 323 } | 336 } |
| 324 } | 337 } |
| 325 | 338 |
| 326 /// Initialize [attributeSpans] using [sourceSpan]. | 339 /// Initialize [attributeSpans] using [sourceSpan]. |
| 327 void _ensureAttributeSpans() { | 340 void _ensureAttributeSpans() { |
| 328 if (_attributeSpans != null) return; | 341 if (_attributeSpans != null) return; |
| 329 | 342 |
| 330 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); | 343 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); |
| 331 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); | 344 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); |
| (...skipping 14 matching lines...) Expand all Loading... |
| 346 offset + attr.start, offset + attr.end); | 359 offset + attr.start, offset + attr.end); |
| 347 if (attr.startValue != null) { | 360 if (attr.startValue != null) { |
| 348 _attributeValueSpans[attr.name] = sourceSpan.file.span( | 361 _attributeValueSpans[attr.name] = sourceSpan.file.span( |
| 349 offset + attr.startValue, offset + attr.endValue); | 362 offset + attr.startValue, offset + attr.endValue); |
| 350 } | 363 } |
| 351 } | 364 } |
| 352 } | 365 } |
| 353 } | 366 } |
| 354 | 367 |
| 355 class Document extends Node { | 368 class Document extends Node { |
| 356 Document() : super(null); | 369 Document() : super._(); |
| 357 factory Document.html(String html) => parse(html); | 370 factory Document.html(String html) => parse(html); |
| 358 | 371 |
| 359 int get nodeType => Node.DOCUMENT_NODE; | 372 int get nodeType => Node.DOCUMENT_NODE; |
| 360 | 373 |
| 361 // TODO(jmesserly): optmize this if needed | 374 // TODO(jmesserly): optmize this if needed |
| 362 Element get documentElement => querySelector('html'); | 375 Element get documentElement => querySelector('html'); |
| 363 Element get head => documentElement.querySelector('head'); | 376 Element get head => documentElement.querySelector('head'); |
| 364 Element get body => documentElement.querySelector('body'); | 377 Element get body => documentElement.querySelector('body'); |
| 365 | 378 |
| 379 /// Returns a fragment of HTML or XML that represents the element and its |
| 380 /// contents. |
| 381 // TODO(jmesserly): this API is not specified in: |
| 382 // <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 |
| 384 // to dom_parsing, where we keep other custom APIs? |
| 385 String get outerHtml => _outerHtml; |
| 386 |
| 366 String toString() => "#document"; | 387 String toString() => "#document"; |
| 367 | 388 |
| 368 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); | 389 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); |
| 369 | 390 |
| 370 Document clone() => new Document(); | 391 Document clone() => new Document(); |
| 371 } | 392 } |
| 372 | 393 |
| 373 class DocumentFragment extends Document { | 394 class DocumentFragment extends Document { |
| 374 DocumentFragment(); | 395 DocumentFragment(); |
| 375 factory DocumentFragment.html(String html) => parseFragment(html); | 396 factory DocumentFragment.html(String html) => parseFragment(html); |
| 376 | 397 |
| 377 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; | 398 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; |
| 378 | 399 |
| 379 String toString() => "#document-fragment"; | 400 String toString() => "#document-fragment"; |
| 380 | 401 |
| 381 DocumentFragment clone() => new DocumentFragment(); | 402 DocumentFragment clone() => new DocumentFragment(); |
| 382 | 403 |
| 383 String get text => _getText(this); | 404 String get text => _getText(this); |
| 384 set text(String value) => _setText(this, value); | 405 set text(String value) => _setText(this, value); |
| 385 } | 406 } |
| 386 | 407 |
| 387 class DocumentType extends Node { | 408 class DocumentType extends Node { |
| 409 final String name; |
| 388 final String publicId; | 410 final String publicId; |
| 389 final String systemId; | 411 final String systemId; |
| 390 | 412 |
| 391 DocumentType(String name, this.publicId, this.systemId) : super(name); | 413 DocumentType(String name, this.publicId, this.systemId) |
| 414 // Note: once Node.tagName is removed, don't pass "name" to super |
| 415 : name = name, super._(name); |
| 392 | 416 |
| 393 int get nodeType => Node.DOCUMENT_TYPE_NODE; | 417 int get nodeType => Node.DOCUMENT_TYPE_NODE; |
| 394 | 418 |
| 395 String toString() { | 419 String toString() { |
| 396 if (publicId != null || systemId != null) { | 420 if (publicId != null || systemId != null) { |
| 397 // TODO(jmesserly): the html5 serialization spec does not add these. But | 421 // TODO(jmesserly): the html5 serialization spec does not add these. But |
| 398 // it seems useful, and the parser can handle it, so for now keeping it. | 422 // it seems useful, and the parser can handle it, so for now keeping it. |
| 399 var pid = publicId != null ? publicId : ''; | 423 var pid = publicId != null ? publicId : ''; |
| 400 var sid = systemId != null ? systemId : ''; | 424 var sid = systemId != null ? systemId : ''; |
| 401 return '<!DOCTYPE $tagName "$pid" "$sid">'; | 425 return '<!DOCTYPE $name "$pid" "$sid">'; |
| 402 } else { | 426 } else { |
| 403 return '<!DOCTYPE $tagName>'; | 427 return '<!DOCTYPE $name>'; |
| 404 } | 428 } |
| 405 } | 429 } |
| 406 | 430 |
| 407 | 431 |
| 408 void _addOuterHtml(StringBuffer str) { | 432 void _addOuterHtml(StringBuffer str) { |
| 409 str.write(toString()); | 433 str.write(toString()); |
| 410 } | 434 } |
| 411 | 435 |
| 412 DocumentType clone() => new DocumentType(tagName, publicId, systemId); | 436 DocumentType clone() => new DocumentType(name, publicId, systemId); |
| 413 } | 437 } |
| 414 | 438 |
| 415 class Text extends Node { | 439 class Text extends Node { |
| 416 String data; | 440 String data; |
| 417 | 441 |
| 418 Text(this.data) : super(null); | 442 Text(this.data) : super._(); |
| 419 | 443 |
| 420 /// *Deprecated* use [data]. | 444 /// *Deprecated* use [data]. |
| 421 @deprecated String get value => data; | 445 @deprecated String get value => data; |
| 422 @deprecated set value(String x) { data = x; } | 446 @deprecated set value(String x) { data = x; } |
| 423 | 447 |
| 424 int get nodeType => Node.TEXT_NODE; | 448 int get nodeType => Node.TEXT_NODE; |
| 425 | 449 |
| 426 String toString() => '"$data"'; | 450 String toString() => '"$data"'; |
| 427 | 451 |
| 428 void _addOuterHtml(StringBuffer str) { | 452 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this); |
| 429 // Don't escape text for certain elements, notably <script>. | |
| 430 if (rcdataElements.contains(parent.tagName) || | |
| 431 parent.tagName == 'plaintext') { | |
| 432 str.write(data); | |
| 433 } else { | |
| 434 str.write(htmlSerializeEscape(data)); | |
| 435 } | |
| 436 } | |
| 437 | 453 |
| 438 Text clone() => new Text(data); | 454 Text clone() => new Text(data); |
| 439 | 455 |
| 440 String get text => data; | 456 String get text => data; |
| 441 set text(String value) { data = value; } | 457 set text(String value) { data = value; } |
| 442 } | 458 } |
| 443 | 459 |
| 444 class Element extends Node { | 460 class Element extends Node { |
| 445 final String namespace; | 461 final String namespaceUri; |
| 446 | 462 |
| 447 // TODO(jmesserly): deprecate in favor of Element.tag? Or rename? | 463 @deprecated String get namespace => namespaceUri; |
| 448 Element(String name, [this.namespace]) : super(name); | |
| 449 | 464 |
| 450 Element.tag(String name) : namespace = null, super(name); | 465 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name) |
| 466 /// of this element. |
| 467 String get localName => _tagName; |
| 468 |
| 469 // TODO(jmesserly): deprecate in favor of [Document.createElementNS]. |
| 470 // However we need every element to have a Document before this can work. |
| 471 Element(String name, [this.namespaceUri]) : super._(name); |
| 472 |
| 473 Element.tag(String name) : namespaceUri = null, super._(name); |
| 451 | 474 |
| 452 static final _START_TAG_REGEXP = new RegExp('<(\\w+)'); | 475 static final _START_TAG_REGEXP = new RegExp('<(\\w+)'); |
| 453 | 476 |
| 454 static final _CUSTOM_PARENT_TAG_MAP = const { | 477 static final _CUSTOM_PARENT_TAG_MAP = const { |
| 455 'body': 'html', | 478 'body': 'html', |
| 456 'head': 'html', | 479 'head': 'html', |
| 457 'caption': 'table', | 480 'caption': 'table', |
| 458 'td': 'tr', | 481 'td': 'tr', |
| 459 'colgroup': 'table', | 482 'colgroup': 'table', |
| 460 'col': 'colgroup', | 483 'col': 'colgroup', |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 499 throw new ArgumentError('HTML had ${fragment.children.length} ' | 522 throw new ArgumentError('HTML had ${fragment.children.length} ' |
| 500 'top level elements but 1 expected'); | 523 'top level elements but 1 expected'); |
| 501 } | 524 } |
| 502 element.remove(); | 525 element.remove(); |
| 503 return element; | 526 return element; |
| 504 } | 527 } |
| 505 | 528 |
| 506 int get nodeType => Node.ELEMENT_NODE; | 529 int get nodeType => Node.ELEMENT_NODE; |
| 507 | 530 |
| 508 String toString() { | 531 String toString() { |
| 509 if (namespace == null) return "<$tagName>"; | 532 if (namespaceUri == null) return "<$localName>"; |
| 510 return "<${Namespaces.getPrefix(namespace)} $tagName>"; | 533 return "<${Namespaces.getPrefix(namespaceUri)} $localName>"; |
| 511 } | 534 } |
| 512 | 535 |
| 513 String get text => _getText(this); | 536 String get text => _getText(this); |
| 514 set text(String value) => _setText(this, value); | 537 set text(String value) => _setText(this, value); |
| 515 | 538 |
| 539 /// Returns a fragment of HTML or XML that represents the element and its |
| 540 /// contents. |
| 541 String get outerHtml => _outerHtml; |
| 542 |
| 543 /// 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 |
| 545 /// the given string. |
| 546 String get innerHtml => _innerHtml; |
| 547 // TODO(jmesserly): deprecate in favor of: |
| 548 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id
_setInnerHtml> |
| 549 set innerHtml(String value) { _innerHtml = value; } |
| 550 |
| 516 void _addOuterHtml(StringBuffer str) { | 551 void _addOuterHtml(StringBuffer str) { |
| 517 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#
serializing-html-fragments | 552 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#
serializing-html-fragments |
| 518 // Element is the most complicated one. | 553 // Element is the most complicated one. |
| 519 if (namespace == null || | 554 if (namespaceUri == null || |
| 520 namespace == Namespaces.html || | 555 namespaceUri == Namespaces.html || |
| 521 namespace == Namespaces.mathml || | 556 namespaceUri == Namespaces.mathml || |
| 522 namespace == Namespaces.svg) { | 557 namespaceUri == Namespaces.svg) { |
| 523 str.write('<$tagName'); | 558 str.write('<$localName'); |
| 524 } else { | 559 } else { |
| 525 // TODO(jmesserly): the spec doesn't define "qualified name". | 560 // TODO(jmesserly): the spec doesn't define "qualified name". |
| 526 // I'm not sure if this is correct, but it should parse reasonably. | 561 // I'm not sure if this is correct, but it should parse reasonably. |
| 527 str.write('<${Namespaces.getPrefix(namespace)}:$tagName'); | 562 str.write('<${Namespaces.getPrefix(namespaceUri)}:$localName'); |
| 528 } | 563 } |
| 529 | 564 |
| 530 if (attributes.length > 0) { | 565 if (attributes.length > 0) { |
| 531 attributes.forEach((key, v) { | 566 attributes.forEach((key, v) { |
| 532 // Note: AttributeName.toString handles serialization of attribute | 567 // Note: AttributeName.toString handles serialization of attribute |
| 533 // namespace, if needed. | 568 // namespace, if needed. |
| 534 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); | 569 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); |
| 535 }); | 570 }); |
| 536 } | 571 } |
| 537 | 572 |
| 538 str.write('>'); | 573 str.write('>'); |
| 539 | 574 |
| 540 if (nodes.length > 0) { | 575 if (nodes.length > 0) { |
| 541 if (tagName == 'pre' || tagName == 'textarea' || tagName == 'listing') { | 576 if (localName == 'pre' || localName == 'textarea' || |
| 577 localName == 'listing') { |
| 542 final first = nodes[0]; | 578 final first = nodes[0]; |
| 543 if (first is Text && first.data.startsWith('\n')) { | 579 if (first is Text && first.data.startsWith('\n')) { |
| 544 // These nodes will remove a leading \n at parse time, so if we still | 580 // These nodes will remove a leading \n at parse time, so if we still |
| 545 // have one, it means we started with two. Add it back. | 581 // have one, it means we started with two. Add it back. |
| 546 str.write('\n'); | 582 str.write('\n'); |
| 547 } | 583 } |
| 548 } | 584 } |
| 549 | 585 |
| 550 _addInnerHtml(str); | 586 _addInnerHtml(str); |
| 551 } | 587 } |
| 552 | 588 |
| 553 // void elements must not have an end tag | 589 // void elements must not have an end tag |
| 554 // http://dev.w3.org/html5/markup/syntax.html#void-elements | 590 // http://dev.w3.org/html5/markup/syntax.html#void-elements |
| 555 if (!isVoidElement(tagName)) str.write('</$tagName>'); | 591 if (!isVoidElement(localName)) str.write('</$localName>'); |
| 556 } | 592 } |
| 557 | 593 |
| 558 Element clone() => new Element(tagName, namespace) | 594 Element clone() => new Element(localName, namespaceUri) |
| 559 ..attributes = new LinkedHashMap.from(attributes); | 595 ..attributes = new LinkedHashMap.from(attributes); |
| 560 | 596 |
| 561 String get id { | 597 String get id { |
| 562 var result = attributes['id']; | 598 var result = attributes['id']; |
| 563 return result != null ? result : ''; | 599 return result != null ? result : ''; |
| 564 } | 600 } |
| 565 | 601 |
| 566 set id(String value) { | 602 set id(String value) { |
| 567 if (value == null) { | 603 if (value == null) { |
| 568 attributes.remove('id'); | 604 attributes.remove('id'); |
| 569 } else { | 605 } else { |
| 570 attributes['id'] = value; | 606 attributes['id'] = value; |
| 571 } | 607 } |
| 572 } | 608 } |
| 573 } | 609 } |
| 574 | 610 |
| 575 class Comment extends Node { | 611 class Comment extends Node { |
| 576 String data; | 612 String data; |
| 577 | 613 |
| 578 Comment(this.data) : super(null); | 614 Comment(this.data) : super._(); |
| 579 | 615 |
| 580 int get nodeType => Node.COMMENT_NODE; | 616 int get nodeType => Node.COMMENT_NODE; |
| 581 | 617 |
| 582 String toString() => "<!-- $data -->"; | 618 String toString() => "<!-- $data -->"; |
| 583 | 619 |
| 584 void _addOuterHtml(StringBuffer str) { | 620 void _addOuterHtml(StringBuffer str) { |
| 585 str.write("<!--$data-->"); | 621 str.write("<!--$data-->"); |
| 586 } | 622 } |
| 587 | 623 |
| 588 Comment clone() => new Comment(data); | 624 Comment clone() => new Comment(data); |
| (...skipping 302 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 891 | 927 |
| 892 class _ConcatTextVisitor extends TreeVisitor { | 928 class _ConcatTextVisitor extends TreeVisitor { |
| 893 final _str = new StringBuffer(); | 929 final _str = new StringBuffer(); |
| 894 | 930 |
| 895 String toString() => _str.toString(); | 931 String toString() => _str.toString(); |
| 896 | 932 |
| 897 visitText(Text node) { | 933 visitText(Text node) { |
| 898 _str.write(node.data); | 934 _str.write(node.data); |
| 899 } | 935 } |
| 900 } | 936 } |
| OLD | NEW |