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 |