OLD | NEW |
---|---|
1 /** | 1 /// 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 | 2 /// 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 */ | |
5 library dom; | 3 library dom; |
6 | 4 |
7 import 'dart:collection'; | 5 import 'dart:collection'; |
8 import 'package:source_maps/span.dart' show FileSpan; | 6 import 'package:source_maps/span.dart' show FileSpan; |
9 | 7 |
10 import 'src/constants.dart'; | 8 import 'src/constants.dart'; |
11 import 'src/list_proxy.dart'; | 9 import 'src/list_proxy.dart'; |
12 import 'src/token.dart'; | 10 import 'src/token.dart'; |
13 import 'src/tokenizer.dart'; | 11 import 'src/tokenizer.dart'; |
14 import 'src/utils.dart'; | 12 import 'src/utils.dart'; |
15 import 'dom_parsing.dart'; | 13 import 'dom_parsing.dart'; |
16 import 'parser.dart'; | 14 import 'parser.dart'; |
17 | 15 |
18 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes | 16 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes |
19 // that exposes namespace info. | 17 // that exposes namespace info. |
20 class AttributeName implements Comparable { | 18 class AttributeName implements Comparable { |
21 /** The namespace prefix, e.g. `xlink`. */ | 19 /// The namespace prefix, e.g. `xlink`. |
22 final String prefix; | 20 final String prefix; |
23 | 21 |
24 /** The attribute name, e.g. `title`. */ | 22 /// The attribute name, e.g. `title`. |
25 final String name; | 23 final String name; |
26 | 24 |
27 /** The namespace url, e.g. `http://www.w3.org/1999/xlink` */ | 25 /// The namespace url, e.g. `http://www.w3.org/1999/xlink` |
28 final String namespace; | 26 final String namespace; |
29 | 27 |
30 const AttributeName(this.prefix, this.name, this.namespace); | 28 const AttributeName(this.prefix, this.name, this.namespace); |
31 | 29 |
32 String toString() { | 30 String toString() { |
33 // Implement: | 31 // Implement: |
34 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments | 32 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html# serializing-html-fragments |
35 // If we get here we know we are xml, xmlns, or xlink, because of | 33 // If we get here we know we are xml, xmlns, or xlink, because of |
36 // [HtmlParser.adjustForeignAttriubtes] is the only place we create | 34 // [HtmlParser.adjustForeignAttriubtes] is the only place we create |
37 // an AttributeName. | 35 // an AttributeName. |
(...skipping 17 matching lines...) Expand all Loading... | |
55 if (cmp != 0) return cmp; | 53 if (cmp != 0) return cmp; |
56 return namespace.compareTo(other.namespace); | 54 return namespace.compareTo(other.namespace); |
57 } | 55 } |
58 | 56 |
59 bool operator ==(x) { | 57 bool operator ==(x) { |
60 if (x is! AttributeName) return false; | 58 if (x is! AttributeName) return false; |
61 return prefix == x.prefix && name == x.name && namespace == x.namespace; | 59 return prefix == x.prefix && name == x.name && namespace == x.namespace; |
62 } | 60 } |
63 } | 61 } |
64 | 62 |
65 /** Really basic implementation of a DOM-core like Node. */ | 63 /// Really basic implementation of a DOM-core like Node. |
66 abstract class Node { | 64 abstract class Node { |
67 static const int ATTRIBUTE_NODE = 2; | 65 static const int ATTRIBUTE_NODE = 2; |
68 static const int CDATA_SECTION_NODE = 4; | 66 static const int CDATA_SECTION_NODE = 4; |
69 static const int COMMENT_NODE = 8; | 67 static const int COMMENT_NODE = 8; |
70 static const int DOCUMENT_FRAGMENT_NODE = 11; | 68 static const int DOCUMENT_FRAGMENT_NODE = 11; |
71 static const int DOCUMENT_NODE = 9; | 69 static const int DOCUMENT_NODE = 9; |
72 static const int DOCUMENT_TYPE_NODE = 10; | 70 static const int DOCUMENT_TYPE_NODE = 10; |
73 static const int ELEMENT_NODE = 1; | 71 static const int ELEMENT_NODE = 1; |
74 static const int ENTITY_NODE = 6; | 72 static const int ENTITY_NODE = 6; |
75 static const int ENTITY_REFERENCE_NODE = 5; | 73 static const int ENTITY_REFERENCE_NODE = 5; |
76 static const int NOTATION_NODE = 12; | 74 static const int NOTATION_NODE = 12; |
77 static const int PROCESSING_INSTRUCTION_NODE = 7; | 75 static const int PROCESSING_INSTRUCTION_NODE = 7; |
78 static const int TEXT_NODE = 3; | 76 static const int TEXT_NODE = 3; |
79 | 77 |
80 // TODO(jmesserly): this should be on Element | 78 // TODO(jmesserly): this should be on Element |
81 /** The tag name associated with the node. */ | 79 /// The tag name associated with the node. |
82 final String tagName; | 80 final String tagName; |
83 | 81 |
84 /** The parent of the current node (or null for the document node). */ | 82 /// The parent of the current node (or null for the document node). |
85 Node parent; | 83 Node parent; |
86 | 84 |
87 // TODO(jmesserly): should move to Element. | 85 // TODO(jmesserly): should move to Element. |
88 /** | 86 /// A map holding name, value pairs for attributes of the node. |
89 * A map holding name, value pairs for attributes of the node. | 87 /// |
90 * | 88 /// Note that attribute order needs to be stable for serialization, so we use a |
Siggi Cherem (dart-lang)
2014/02/24 23:03:29
80
Jennifer Messerly
2014/02/25 00:32:08
funny. this was the line I was iterating on when I
| |
91 * Note that attribute order needs to be stable for serialization, so we use a | 89 /// LinkedHashMap. Each key is a [String] or [AttributeName]. |
92 * LinkedHashMap. Each key is a [String] or [AttributeName]. | |
93 */ | |
94 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); | 90 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); |
95 | 91 |
96 /** | 92 /// A list of child nodes of the current node. This must |
97 * A list of child nodes of the current node. This must | 93 /// include all elements but not necessarily other node types. |
98 * include all elements but not necessarily other node types. | |
99 */ | |
100 final NodeList nodes = new NodeList._(); | 94 final NodeList nodes = new NodeList._(); |
101 | 95 |
102 List<Element> _elements; | 96 List<Element> _elements; |
103 | 97 |
104 // TODO(jmesserly): consider using an Expando for this, and put it in | 98 // TODO(jmesserly): consider using an Expando for this, and put it in |
105 // dom_parsing. Need to check the performance affect. | 99 // dom_parsing. Need to check the performance affect. |
106 /** The source span of this node, if it was created by the [HtmlParser]. */ | 100 /// The source span of this node, if it was created by the [HtmlParser]. |
107 FileSpan sourceSpan; | 101 FileSpan sourceSpan; |
108 | 102 |
109 /** The attribute spans if requested. Otherwise null. */ | 103 /// The attribute spans if requested. Otherwise null. |
110 LinkedHashMap<dynamic, FileSpan> _attributeSpans; | 104 LinkedHashMap<dynamic, FileSpan> _attributeSpans; |
111 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; | 105 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; |
112 | 106 |
113 Node(this.tagName) { | 107 Node(this.tagName) { |
114 nodes._parent = this; | 108 nodes._parent = this; |
115 } | 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"`. | |
122 */ | |
123 LinkedHashMap<dynamic, FileSpan> get attributeSpans { | 115 LinkedHashMap<dynamic, FileSpan> get attributeSpans { |
124 _ensureAttributeSpans(); | 116 _ensureAttributeSpans(); |
125 return _attributeSpans; | 117 return _attributeSpans; |
126 } | 118 } |
127 | 119 |
128 /** | 120 /// If [sourceSpan] is available, this contains the spans of each attribute's |
129 * If [sourceSpan] is available, this contains the spans of each attribute's | 121 /// value. Unlike [attributeSpans], this span will inlcude only the value. |
130 * value. Unlike [attributeSpans], this span will inlcude only the value. | 122 /// For example, the value span of "attr" in `<a attr="value">` would be the |
131 * For example, the value span of "attr" in `<a attr="value">` would be the | 123 /// text `value`. |
132 * text `value`. | |
133 */ | |
134 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans { | 124 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans { |
135 _ensureAttributeSpans(); | 125 _ensureAttributeSpans(); |
136 return _attributeValueSpans; | 126 return _attributeValueSpans; |
137 } | 127 } |
138 | 128 |
139 List<Element> get children { | 129 List<Element> get children { |
140 if (_elements == null) { | 130 if (_elements == null) { |
141 _elements = new FilteredElementList(this); | 131 _elements = new FilteredElementList(this); |
142 } | 132 } |
143 return _elements; | 133 return _elements; |
144 } | 134 } |
145 | 135 |
146 // TODO(jmesserly): needs to support deep clone. | 136 // TODO(jmesserly): needs to support deep clone. |
147 /** | 137 /// 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 | 138 /// name and attributes but with no parent or child nodes. |
149 * name and attributes but with no parent or child nodes. | |
150 */ | |
151 Node clone(); | 139 Node clone(); |
152 | 140 |
153 String get namespace => null; | 141 String get namespace => null; |
154 | 142 |
155 int get nodeType; | 143 int get nodeType; |
156 | 144 |
157 /** *Deprecated* use [text], [Text.data] or [Comment.data]. */ | 145 /// *Deprecated* use [text], [Text.data] or [Comment.data]. |
158 @deprecated String get value => null; | 146 @deprecated String get value => null; |
159 | 147 |
160 /** *Deprecated* use [nodeType]. */ | 148 /// *Deprecated* use [nodeType]. |
161 @deprecated int get $dom_nodeType => nodeType; | 149 @deprecated int get $dom_nodeType => nodeType; |
162 | 150 |
163 String get outerHtml { | 151 String get outerHtml { |
164 var str = new StringBuffer(); | 152 var str = new StringBuffer(); |
165 _addOuterHtml(str); | 153 _addOuterHtml(str); |
166 return str.toString(); | 154 return str.toString(); |
167 } | 155 } |
168 | 156 |
169 String get innerHtml { | 157 String get innerHtml { |
170 var str = new StringBuffer(); | 158 var str = new StringBuffer(); |
(...skipping 25 matching lines...) Expand all Loading... | |
196 String toString() => tagName; | 184 String toString() => tagName; |
197 | 185 |
198 Node remove() { | 186 Node remove() { |
199 // TODO(jmesserly): is parent == null an error? | 187 // TODO(jmesserly): is parent == null an error? |
200 if (parent != null) { | 188 if (parent != null) { |
201 parent.nodes.remove(this); | 189 parent.nodes.remove(this); |
202 } | 190 } |
203 return this; | 191 return this; |
204 } | 192 } |
205 | 193 |
206 /** | 194 /// Insert [node] as a child of the current node, before [refNode] in the |
207 * Insert [node] as a child of the current node, before [refNode] in the | 195 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode] |
208 * list of child nodes. Raises [UnsupportedOperationException] if [refNode] | 196 /// is not a child of the current node. If refNode is null, this adds to the |
209 * is not a child of the current node. If refNode is null, this adds to the | 197 /// end of the list. |
210 * end of the list. | |
211 */ | |
212 void insertBefore(Node node, Node refNode) { | 198 void insertBefore(Node node, Node refNode) { |
213 if (refNode == null) { | 199 if (refNode == null) { |
214 nodes.add(node); | 200 nodes.add(node); |
215 } else { | 201 } else { |
216 nodes.insert(nodes.indexOf(refNode), node); | 202 nodes.insert(nodes.indexOf(refNode), node); |
217 } | 203 } |
218 } | 204 } |
219 | 205 |
220 /** Replaces this node with another node. */ | 206 /// Replaces this node with another node. |
221 Node replaceWith(Node otherNode) { | 207 Node replaceWith(Node otherNode) { |
222 if (parent == null) { | 208 if (parent == null) { |
223 throw new UnsupportedError('Node must have a parent to replace it.'); | 209 throw new UnsupportedError('Node must have a parent to replace it.'); |
224 } | 210 } |
225 parent.nodes[parent.nodes.indexOf(this)] = otherNode; | 211 parent.nodes[parent.nodes.indexOf(this)] = otherNode; |
226 return this; | 212 return this; |
227 } | 213 } |
228 | 214 |
229 // TODO(jmesserly): should this be a property or remove? | 215 // TODO(jmesserly): should this be a property or remove? |
230 /** Return true if the node has children or text. */ | 216 /// Return true if the node has children or text. |
231 bool hasContent() => nodes.length > 0; | 217 bool hasContent() => nodes.length > 0; |
232 | 218 |
233 Pair<String, String> get nameTuple { | 219 Pair<String, String> get nameTuple { |
234 var ns = namespace != null ? namespace : Namespaces.html; | 220 var ns = namespace != null ? namespace : Namespaces.html; |
235 return new Pair(ns, tagName); | 221 return new Pair(ns, tagName); |
236 } | 222 } |
237 | 223 |
238 /** | 224 /// Move all the children of the current node to [newParent]. |
239 * 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 |
240 * This is needed so that trees that don't store text as nodes move the | 226 /// text in the correct way. |
241 * text in the correct way. | |
242 */ | |
243 void reparentChildren(Node newParent) { | 227 void reparentChildren(Node newParent) { |
244 newParent.nodes.addAll(nodes); | 228 newParent.nodes.addAll(nodes); |
245 nodes.clear(); | 229 nodes.clear(); |
246 } | 230 } |
247 | 231 |
248 /** *Deprecated* use [querySelector] instead. */ | 232 /// *Deprecated* use [querySelector] instead. |
249 @deprecated | 233 @deprecated |
250 Element query(String selectors) => querySelector(selectors); | 234 Element query(String selectors) => querySelector(selectors); |
251 | 235 |
252 /** *Deprecated* use [querySelectorAll] instead. */ | 236 /// *Deprecated* use [querySelectorAll] instead. |
253 @deprecated | 237 @deprecated |
254 List<Element> queryAll(String selectors) => querySelectorAll(selectors); | 238 List<Element> queryAll(String selectors) => querySelectorAll(selectors); |
255 | 239 |
256 /** | 240 /// Seaches for the first descendant node matching the given selectors, using a |
257 * Seaches for the first descendant node matching the given selectors, using a | 241 /// preorder traversal. NOTE: right now, this supports only a single type |
258 * preorder traversal. NOTE: right now, this supports only a single type | 242 /// selectors, e.g. `node.query('div')`. |
259 * selectors, e.g. `node.query('div')`. | |
260 */ | |
261 | 243 |
262 Element querySelector(String selectors) => | 244 Element querySelector(String selectors) => |
263 _queryType(_typeSelector(selectors)); | 245 _queryType(_typeSelector(selectors)); |
264 | 246 |
265 /** | 247 /// Returns all descendant nodes matching the given selectors, using a |
266 * Returns all descendant nodes matching the given selectors, using a | 248 /// preorder traversal. NOTE: right now, this supports only a single type |
267 * preorder traversal. NOTE: right now, this supports only a single type | 249 /// selectors, e.g. `node.queryAll('div')`. |
268 * selectors, e.g. `node.queryAll('div')`. | |
269 */ | |
270 List<Element> querySelectorAll(String selectors) { | 250 List<Element> querySelectorAll(String selectors) { |
271 var results = new List<Element>(); | 251 var results = new List<Element>(); |
272 _queryAllType(_typeSelector(selectors), results); | 252 _queryAllType(_typeSelector(selectors), results); |
273 return results; | 253 return results; |
274 } | 254 } |
275 | 255 |
276 bool hasChildNodes() => !nodes.isEmpty; | 256 bool hasChildNodes() => !nodes.isEmpty; |
277 | 257 |
278 bool contains(Node node) => nodes.contains(node); | 258 bool contains(Node node) => nodes.contains(node); |
279 | 259 |
280 String _typeSelector(String selectors) { | 260 String _typeSelector(String selectors) { |
281 selectors = selectors.trim(); | 261 selectors = selectors.trim(); |
282 if (!_isTypeSelector(selectors)) { | 262 if (!_isTypeSelector(selectors)) { |
283 throw new UnimplementedError('only type selectors are implemented'); | 263 throw new UnimplementedError('only type selectors are implemented'); |
284 } | 264 } |
285 return selectors; | 265 return selectors; |
286 } | 266 } |
287 | 267 |
288 /** | 268 /// Checks if this is a type selector. |
289 * Checks if this is a type selector. | 269 /// See <http://www.w3.org/TR/CSS2/grammar.html>. |
290 * See <http://www.w3.org/TR/CSS2/grammar.html>. | 270 /// Note: this doesn't support '*', the universal selector, non-ascii chars or |
291 * Note: this doesn't support '*', the universal selector, non-ascii chars or | 271 /// escape chars. |
292 * escape chars. | |
293 */ | |
294 bool _isTypeSelector(String selector) { | 272 bool _isTypeSelector(String selector) { |
295 // Parser: | 273 // Parser: |
296 | 274 |
297 // element_name | 275 // element_name |
298 // : IDENT | '*' | 276 // : IDENT | '*' |
299 // ; | 277 // ; |
300 | 278 |
301 // Lexer: | 279 // Lexer: |
302 | 280 |
303 // nmstart [_a-z]|{nonascii}|{escape} | 281 // nmstart [_a-z]|{nonascii}|{escape} |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
338 } | 316 } |
339 | 317 |
340 void _queryAllType(String tag, List<Element> results) { | 318 void _queryAllType(String tag, List<Element> results) { |
341 for (var node in nodes) { | 319 for (var node in nodes) { |
342 if (node is! Element) continue; | 320 if (node is! Element) continue; |
343 if (node.tagName == tag) results.add(node); | 321 if (node.tagName == tag) results.add(node); |
344 node._queryAllType(tag, results); | 322 node._queryAllType(tag, results); |
345 } | 323 } |
346 } | 324 } |
347 | 325 |
348 /** Initialize [attributeSpans] using [sourceSpan]. */ | 326 /// Initialize [attributeSpans] using [sourceSpan]. |
349 void _ensureAttributeSpans() { | 327 void _ensureAttributeSpans() { |
350 if (_attributeSpans != null) return; | 328 if (_attributeSpans != null) return; |
351 | 329 |
352 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); | 330 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); |
353 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); | 331 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); |
354 | 332 |
355 if (sourceSpan == null) return; | 333 if (sourceSpan == null) return; |
356 | 334 |
357 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true, | 335 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true, |
358 attributeSpans: true); | 336 attributeSpans: true); |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
432 } | 410 } |
433 | 411 |
434 DocumentType clone() => new DocumentType(tagName, publicId, systemId); | 412 DocumentType clone() => new DocumentType(tagName, publicId, systemId); |
435 } | 413 } |
436 | 414 |
437 class Text extends Node { | 415 class Text extends Node { |
438 String data; | 416 String data; |
439 | 417 |
440 Text(this.data) : super(null); | 418 Text(this.data) : super(null); |
441 | 419 |
442 /** *Deprecated* use [data]. */ | 420 /// *Deprecated* use [data]. |
443 @deprecated String get value => data; | 421 @deprecated String get value => data; |
444 @deprecated set value(String x) { data = x; } | 422 @deprecated set value(String x) { data = x; } |
445 | 423 |
446 int get nodeType => Node.TEXT_NODE; | 424 int get nodeType => Node.TEXT_NODE; |
447 | 425 |
448 String toString() => '"$data"'; | 426 String toString() => '"$data"'; |
449 | 427 |
450 void _addOuterHtml(StringBuffer str) { | 428 void _addOuterHtml(StringBuffer str) { |
451 // Don't escape text for certain elements, notably <script>. | 429 // Don't escape text for certain elements, notably <script>. |
452 if (rcdataElements.contains(parent.tagName) || | 430 if (rcdataElements.contains(parent.tagName) || |
(...skipping 260 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
713 super.retainWhere(test); | 691 super.retainWhere(test); |
714 } | 692 } |
715 | 693 |
716 void insertAll(int index, List<Node> nodes) { | 694 void insertAll(int index, List<Node> nodes) { |
717 for (var node in nodes) _setParent(node); | 695 for (var node in nodes) _setParent(node); |
718 super.insertAll(index, nodes); | 696 super.insertAll(index, nodes); |
719 } | 697 } |
720 } | 698 } |
721 | 699 |
722 | 700 |
723 /** | 701 /// An indexable collection of a node's descendants in the document tree, |
724 * An indexable collection of a node's descendants in the document tree, | 702 /// filtered so that only elements are in the collection. |
725 * filtered so that only elements are in the collection. | |
726 */ | |
727 // TODO(jmesserly): this was copied from dart:html | 703 // TODO(jmesserly): this was copied from dart:html |
728 // TODO(jmesserly): "implements List<Element>" is a workaround for analyzer bug. | 704 // TODO(jmesserly): "implements List<Element>" is a workaround for analyzer bug. |
729 class FilteredElementList extends IterableBase<Element> with ListMixin<Element> | 705 class FilteredElementList extends IterableBase<Element> with ListMixin<Element> |
730 implements List<Element> { | 706 implements List<Element> { |
731 | 707 |
732 final Node _node; | 708 final Node _node; |
733 final List<Node> _childNodes; | 709 final List<Node> _childNodes; |
734 | 710 |
735 /** | 711 /// Creates a collection of the elements that descend from a node. |
736 * Creates a collection of the elements that descend from a node. | 712 /// |
737 * | 713 /// Example usage: |
738 * Example usage: | 714 /// |
739 * | 715 /// var filteredElements = new FilteredElementList(query("#container")); |
740 * var filteredElements = new FilteredElementList(query("#container")); | 716 /// // filteredElements is [a, b, c]. |
741 * // filteredElements is [a, b, c]. | |
742 */ | |
743 FilteredElementList(Node node): _childNodes = node.nodes, _node = node; | 717 FilteredElementList(Node node): _childNodes = node.nodes, _node = node; |
744 | 718 |
745 // We can't memoize this, since it's possible that children will be messed | 719 // We can't memoize this, since it's possible that children will be messed |
746 // with externally to this class. | 720 // with externally to this class. |
747 // | 721 // |
748 // TODO(nweiz): we don't always need to create a new list. For example | 722 // TODO(nweiz): we don't always need to create a new list. For example |
749 // forEach, every, any, ... could directly work on the _childNodes. | 723 // forEach, every, any, ... could directly work on the _childNodes. |
750 List<Element> get _filtered => | 724 List<Element> get _filtered => |
751 new List<Element>.from(_childNodes.where((n) => n is Element)); | 725 new List<Element>.from(_childNodes.where((n) => n is Element)); |
752 | 726 |
(...skipping 164 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
917 | 891 |
918 class _ConcatTextVisitor extends TreeVisitor { | 892 class _ConcatTextVisitor extends TreeVisitor { |
919 final _str = new StringBuffer(); | 893 final _str = new StringBuffer(); |
920 | 894 |
921 String toString() => _str.toString(); | 895 String toString() => _str.toString(); |
922 | 896 |
923 visitText(Text node) { | 897 visitText(Text node) { |
924 _str.write(node.data); | 898 _str.write(node.data); |
925 } | 899 } |
926 } | 900 } |
OLD | NEW |