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 it is missing many types and APIs. |
3 library dom; | 3 library dom; |
4 | 4 |
| 5 // TODO(jmesserly): lots to do here. Originally I wanted to generate this using |
| 6 // our Blink IDL generator, but another idea is to directly use the excellent |
| 7 // http://dom.spec.whatwg.org/ and http://html.spec.whatwg.org/ and just |
| 8 // implement that. |
| 9 |
5 import 'dart:collection'; | 10 import 'dart:collection'; |
6 import 'package:source_maps/span.dart' show FileSpan; | 11 import 'package:source_maps/span.dart' show FileSpan; |
7 | 12 |
8 import 'src/constants.dart'; | 13 import 'src/constants.dart'; |
| 14 import 'src/css_class_set.dart'; |
9 import 'src/list_proxy.dart'; | 15 import 'src/list_proxy.dart'; |
| 16 import 'src/query_selector.dart' as query; |
10 import 'src/token.dart'; | 17 import 'src/token.dart'; |
11 import 'src/tokenizer.dart'; | 18 import 'src/tokenizer.dart'; |
12 import 'src/utils.dart'; | |
13 import 'dom_parsing.dart'; | 19 import 'dom_parsing.dart'; |
14 import 'parser.dart'; | 20 import 'parser.dart'; |
15 | 21 |
| 22 export 'src/css_class_set.dart' show CssClassSet; |
| 23 |
16 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes | 24 // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes |
17 // that exposes namespace info. | 25 // that exposes namespace info. |
18 class AttributeName implements Comparable { | 26 class AttributeName implements Comparable { |
19 /// The namespace prefix, e.g. `xlink`. | 27 /// The namespace prefix, e.g. `xlink`. |
20 final String prefix; | 28 final String prefix; |
21 | 29 |
22 /// The attribute name, e.g. `title`. | 30 /// The attribute name, e.g. `title`. |
23 final String name; | 31 final String name; |
24 | 32 |
25 /// The namespace url, e.g. `http://www.w3.org/1999/xlink` | 33 /// The namespace url, e.g. `http://www.w3.org/1999/xlink` |
(...skipping 27 matching lines...) Expand all Loading... |
53 if (cmp != 0) return cmp; | 61 if (cmp != 0) return cmp; |
54 return namespace.compareTo(other.namespace); | 62 return namespace.compareTo(other.namespace); |
55 } | 63 } |
56 | 64 |
57 bool operator ==(x) { | 65 bool operator ==(x) { |
58 if (x is! AttributeName) return false; | 66 if (x is! AttributeName) return false; |
59 return prefix == x.prefix && name == x.name && namespace == x.namespace; | 67 return prefix == x.prefix && name == x.name && namespace == x.namespace; |
60 } | 68 } |
61 } | 69 } |
62 | 70 |
| 71 // http://dom.spec.whatwg.org/#parentnode |
| 72 abstract class _ParentNode implements Node { |
| 73 // TODO(jmesserly): this is only a partial implementation |
| 74 |
| 75 /// Seaches for the first descendant node matching the given selectors, using |
| 76 /// a preorder traversal. |
| 77 /// |
| 78 /// NOTE: Not all selectors from |
| 79 /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/) |
| 80 /// are implemented. For example, nth-child does not implement An+B syntax |
| 81 /// and *-of-type is not implemented. If a selector is not implemented this |
| 82 /// method will throw [UniplmentedError]. |
| 83 Element querySelector(String selector) => |
| 84 query.querySelector(this, selector); |
| 85 |
| 86 /// Returns all descendant nodes matching the given selectors, using a |
| 87 /// preorder traversal. |
| 88 /// |
| 89 /// NOTE: Not all selectors from |
| 90 /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/) |
| 91 /// are implemented. For example, nth-child does not implement An+B syntax |
| 92 /// and *-of-type is not implemented. If a selector is not implemented this |
| 93 /// method will throw [UniplmentedError]. |
| 94 List<Element> querySelectorAll(String selector) => |
| 95 query.querySelectorAll(this, selector); |
| 96 } |
| 97 |
| 98 // http://dom.spec.whatwg.org/#interface-nonelementparentnode |
| 99 abstract class _NonElementParentNode implements _ParentNode { |
| 100 // TODO(jmesserly): could be faster, should throw on invalid id. |
| 101 Element getElementById(String id) => querySelector('#$id'); |
| 102 } |
| 103 |
| 104 // This doesn't exist as an interface in the spec, but it's useful to merge |
| 105 // common methods from these: |
| 106 // http://dom.spec.whatwg.org/#interface-document |
| 107 // http://dom.spec.whatwg.org/#element |
| 108 abstract class _ElementAndDocument implements _ParentNode { |
| 109 // TODO(jmesserly): could be faster, should throw on invalid tag/class names. |
| 110 |
| 111 List<Element> getElementsByTagName(String localName) => |
| 112 querySelectorAll(localName); |
| 113 |
| 114 List<Element> getElementsByClassName(String classNames) => |
| 115 querySelectorAll(classNames.splitMapJoin(' ', |
| 116 onNonMatch: (m) => m.isNotEmpty ? '.$m' : m, |
| 117 onMatch: (m) => '')); |
| 118 } |
| 119 |
63 /// Really basic implementation of a DOM-core like Node. | 120 /// Really basic implementation of a DOM-core like Node. |
64 abstract class Node { | 121 abstract class Node { |
65 static const int ATTRIBUTE_NODE = 2; | 122 static const int ATTRIBUTE_NODE = 2; |
66 static const int CDATA_SECTION_NODE = 4; | 123 static const int CDATA_SECTION_NODE = 4; |
67 static const int COMMENT_NODE = 8; | 124 static const int COMMENT_NODE = 8; |
68 static const int DOCUMENT_FRAGMENT_NODE = 11; | 125 static const int DOCUMENT_FRAGMENT_NODE = 11; |
69 static const int DOCUMENT_NODE = 9; | 126 static const int DOCUMENT_NODE = 9; |
70 static const int DOCUMENT_TYPE_NODE = 10; | 127 static const int DOCUMENT_TYPE_NODE = 10; |
71 static const int ELEMENT_NODE = 1; | 128 static const int ELEMENT_NODE = 1; |
72 static const int ENTITY_NODE = 6; | 129 static const int ENTITY_NODE = 6; |
73 static const int ENTITY_REFERENCE_NODE = 5; | 130 static const int ENTITY_REFERENCE_NODE = 5; |
74 static const int NOTATION_NODE = 12; | 131 static const int NOTATION_NODE = 12; |
75 static const int PROCESSING_INSTRUCTION_NODE = 7; | 132 static const int PROCESSING_INSTRUCTION_NODE = 7; |
76 static const int TEXT_NODE = 3; | 133 static const int TEXT_NODE = 3; |
77 | 134 |
78 /// Note: For now we use it to implement the deprecated tagName property. | 135 /// The parent of the current node (or null for the document node). |
79 final String _tagName; | 136 Node parentNode; |
80 | 137 |
81 /// *Deprecated* use [Element.localName] instead. | 138 /// The parent element of this node. |
82 /// Note: after removal, this will be replaced by a correct version that | 139 /// |
83 /// returns uppercase [as specified](http://dom.spec.whatwg.org/#dom-element-t
agname). | 140 /// Returns null if this node either does not have a parent or its parent is |
84 @deprecated String get tagName => _tagName; | 141 /// not an element. |
85 | 142 Element get parent => parentNode is Element ? parentNode : null; |
86 /// The parent of the current node (or null for the document node). | |
87 Node parent; | |
88 | 143 |
89 // TODO(jmesserly): should move to Element. | 144 // TODO(jmesserly): should move to Element. |
90 /// A map holding name, value pairs for attributes of the node. | 145 /// A map holding name, value pairs for attributes of the node. |
91 /// | 146 /// |
92 /// Note that attribute order needs to be stable for serialization, so we use | 147 /// Note that attribute order needs to be stable for serialization, so we use |
93 /// a LinkedHashMap. Each key is a [String] or [AttributeName]. | 148 /// a LinkedHashMap. Each key is a [String] or [AttributeName]. |
94 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); | 149 LinkedHashMap<dynamic, String> attributes = new LinkedHashMap(); |
95 | 150 |
96 /// A list of child nodes of the current node. This must | 151 /// A list of child nodes of the current node. This must |
97 /// include all elements but not necessarily other node types. | 152 /// include all elements but not necessarily other node types. |
98 final NodeList nodes = new NodeList._(); | 153 final NodeList nodes = new NodeList._(); |
99 | 154 |
100 List<Element> _elements; | 155 List<Element> _elements; |
101 | 156 |
102 // TODO(jmesserly): consider using an Expando for this, and put it in | 157 // TODO(jmesserly): consider using an Expando for this, and put it in |
103 // dom_parsing. Need to check the performance affect. | 158 // dom_parsing. Need to check the performance affect. |
104 /// The source span of this node, if it was created by the [HtmlParser]. | 159 /// The source span of this node, if it was created by the [HtmlParser]. |
105 FileSpan sourceSpan; | 160 FileSpan sourceSpan; |
106 | 161 |
107 /// The attribute spans if requested. Otherwise null. | 162 /// The attribute spans if requested. Otherwise null. |
108 LinkedHashMap<dynamic, FileSpan> _attributeSpans; | 163 LinkedHashMap<dynamic, FileSpan> _attributeSpans; |
109 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; | 164 LinkedHashMap<dynamic, FileSpan> _attributeValueSpans; |
110 | 165 |
111 /// *Deprecated* use [new Element.tag] instead. | 166 Node._() { |
112 @deprecated Node(String tagName) : this._(tagName); | |
113 | |
114 Node._([this._tagName]) { | |
115 nodes._parent = this; | 167 nodes._parent = this; |
116 } | 168 } |
117 | 169 |
118 /// If [sourceSpan] is available, this contains the spans of each attribute. | 170 /// If [sourceSpan] is available, this contains the spans of each attribute. |
119 /// The span of an attribute is the entire attribute, including the name and | 171 /// The span of an attribute is the entire attribute, including the name and |
120 /// quotes (if any). For example, the span of "attr" in `<a attr="value">` | 172 /// quotes (if any). For example, the span of "attr" in `<a attr="value">` |
121 /// would be the text `attr="value"`. | 173 /// would be the text `attr="value"`. |
122 LinkedHashMap<dynamic, FileSpan> get attributeSpans { | 174 LinkedHashMap<dynamic, FileSpan> get attributeSpans { |
123 _ensureAttributeSpans(); | 175 _ensureAttributeSpans(); |
124 return _attributeSpans; | 176 return _attributeSpans; |
125 } | 177 } |
126 | 178 |
127 /// If [sourceSpan] is available, this contains the spans of each attribute's | 179 /// If [sourceSpan] is available, this contains the spans of each attribute's |
128 /// value. Unlike [attributeSpans], this span will inlcude only the value. | 180 /// value. Unlike [attributeSpans], this span will inlcude only the value. |
129 /// For example, the value span of "attr" in `<a attr="value">` would be the | 181 /// For example, the value span of "attr" in `<a attr="value">` would be the |
130 /// text `value`. | 182 /// text `value`. |
131 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans { | 183 LinkedHashMap<dynamic, FileSpan> get attributeValueSpans { |
132 _ensureAttributeSpans(); | 184 _ensureAttributeSpans(); |
133 return _attributeValueSpans; | 185 return _attributeValueSpans; |
134 } | 186 } |
135 | 187 |
136 List<Element> get children { | 188 List<Element> get children { |
137 if (_elements == null) { | 189 if (_elements == null) { |
138 _elements = new FilteredElementList(this); | 190 _elements = new FilteredElementList(this); |
139 } | 191 } |
140 return _elements; | 192 return _elements; |
141 } | 193 } |
142 | 194 |
143 // TODO(jmesserly): needs to support deep clone. | 195 /// Returns a copy of this node. |
144 /// Return a shallow copy of the current node i.e. a node with the same | 196 /// |
145 /// name and attributes but with no parent or child nodes. | 197 /// If [deep] is `true`, then all of this node's children and decendents are |
146 Node clone(); | 198 /// copied as well. If [deep] is `false`, then only this node is copied. |
147 | 199 Node clone(bool deep); |
148 /// *Deprecated* use [Element.namespaceUri] instead. | |
149 @deprecated String get namespace => null; | |
150 | 200 |
151 int get nodeType; | 201 int get nodeType; |
152 | 202 |
153 /// *Deprecated* use [text], [Text.data] or [Comment.data]. | |
154 @deprecated String get value => null; | |
155 | |
156 /// *Deprecated* use [nodeType]. | |
157 @deprecated int get $dom_nodeType => nodeType; | |
158 | |
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 | 203 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface |
167 String get _outerHtml { | 204 String get _outerHtml { |
168 var str = new StringBuffer(); | 205 var str = new StringBuffer(); |
169 _addOuterHtml(str); | 206 _addOuterHtml(str); |
170 return str.toString(); | 207 return str.toString(); |
171 } | 208 } |
172 | 209 |
173 String get _innerHtml { | 210 String get _innerHtml { |
174 var str = new StringBuffer(); | 211 var str = new StringBuffer(); |
175 _addInnerHtml(str); | 212 _addInnerHtml(str); |
176 return str.toString(); | 213 return str.toString(); |
177 } | 214 } |
178 | 215 |
179 set _innerHtml(String value) { | |
180 nodes.clear(); | |
181 // TODO(jmesserly): should be able to get the same effect by adding the | |
182 // fragment directly. | |
183 nodes.addAll(parseFragment(value, container: _tagName).nodes); | |
184 } | |
185 | |
186 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent | 216 // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent |
187 String get text => null; | 217 String get text => null; |
188 set text(String value) {} | 218 set text(String value) {} |
189 | 219 |
190 void append(Node node) => nodes.add(node); | 220 void append(Node node) => nodes.add(node); |
191 | 221 |
192 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; | 222 Node get firstChild => nodes.isNotEmpty ? nodes[0] : null; |
193 | 223 |
194 void _addOuterHtml(StringBuffer str); | 224 void _addOuterHtml(StringBuffer str); |
195 | 225 |
196 void _addInnerHtml(StringBuffer str) { | 226 void _addInnerHtml(StringBuffer str) { |
197 for (Node child in nodes) child._addOuterHtml(str); | 227 for (Node child in nodes) child._addOuterHtml(str); |
198 } | 228 } |
199 | 229 |
200 Node remove() { | 230 Node remove() { |
201 // TODO(jmesserly): is parent == null an error? | 231 // TODO(jmesserly): is parent == null an error? |
202 if (parent != null) { | 232 if (parentNode != null) { |
203 parent.nodes.remove(this); | 233 parentNode.nodes.remove(this); |
204 } | 234 } |
205 return this; | 235 return this; |
206 } | 236 } |
207 | 237 |
208 /// Insert [node] as a child of the current node, before [refNode] in the | 238 /// Insert [node] as a child of the current node, before [refNode] in the |
209 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode] | 239 /// list of child nodes. Raises [UnsupportedOperationException] if [refNode] |
210 /// is not a child of the current node. If refNode is null, this adds to the | 240 /// is not a child of the current node. If refNode is null, this adds to the |
211 /// end of the list. | 241 /// end of the list. |
212 void insertBefore(Node node, Node refNode) { | 242 void insertBefore(Node node, Node refNode) { |
213 if (refNode == null) { | 243 if (refNode == null) { |
214 nodes.add(node); | 244 nodes.add(node); |
215 } else { | 245 } else { |
216 nodes.insert(nodes.indexOf(refNode), node); | 246 nodes.insert(nodes.indexOf(refNode), node); |
217 } | 247 } |
218 } | 248 } |
219 | 249 |
220 /// Replaces this node with another node. | 250 /// Replaces this node with another node. |
221 Node replaceWith(Node otherNode) { | 251 Node replaceWith(Node otherNode) { |
222 if (parent == null) { | 252 if (parentNode == null) { |
223 throw new UnsupportedError('Node must have a parent to replace it.'); | 253 throw new UnsupportedError('Node must have a parent to replace it.'); |
224 } | 254 } |
225 parent.nodes[parent.nodes.indexOf(this)] = otherNode; | 255 parentNode.nodes[parentNode.nodes.indexOf(this)] = otherNode; |
226 return this; | 256 return this; |
227 } | 257 } |
228 | 258 |
229 // TODO(jmesserly): should this be a property or remove? | 259 // TODO(jmesserly): should this be a property or remove? |
230 /// Return true if the node has children or text. | 260 /// Return true if the node has children or text. |
231 bool hasContent() => nodes.length > 0; | 261 bool hasContent() => nodes.length > 0; |
232 | 262 |
233 /// *Deprecated* construct a pair using the namespaceUri and the name. | |
234 @deprecated Pair<String, String> get nameTuple => | |
235 this is Element ? getElementNameTuple(this) : null; | |
236 | |
237 /// Move all the children of the current node to [newParent]. | 263 /// Move all the children of the current node to [newParent]. |
238 /// This is needed so that trees that don't store text as nodes move the | 264 /// This is needed so that trees that don't store text as nodes move the |
239 /// text in the correct way. | 265 /// text in the correct way. |
240 void reparentChildren(Node newParent) { | 266 void reparentChildren(Node newParent) { |
241 newParent.nodes.addAll(nodes); | 267 newParent.nodes.addAll(nodes); |
242 nodes.clear(); | 268 nodes.clear(); |
243 } | 269 } |
244 | 270 |
245 /// *Deprecated* use [querySelector] instead. | |
246 @deprecated | |
247 Element query(String selectors) => querySelector(selectors); | |
248 | |
249 /// *Deprecated* use [querySelectorAll] instead. | |
250 @deprecated | |
251 List<Element> queryAll(String selectors) => querySelectorAll(selectors); | |
252 | |
253 /// Seaches for the first descendant node matching the given selectors, using
a | |
254 /// preorder traversal. NOTE: right now, this supports only a single type | |
255 /// selectors, e.g. `node.query('div')`. | |
256 | |
257 Element querySelector(String selectors) => | |
258 _queryType(_typeSelector(selectors)); | |
259 | |
260 /// Returns all descendant nodes matching the given selectors, using a | |
261 /// preorder traversal. NOTE: right now, this supports only a single type | |
262 /// selectors, e.g. `node.queryAll('div')`. | |
263 List<Element> querySelectorAll(String selectors) { | |
264 var results = new List<Element>(); | |
265 _queryAllType(_typeSelector(selectors), results); | |
266 return results; | |
267 } | |
268 | |
269 bool hasChildNodes() => !nodes.isEmpty; | 271 bool hasChildNodes() => !nodes.isEmpty; |
270 | 272 |
271 bool contains(Node node) => nodes.contains(node); | 273 bool contains(Node node) => nodes.contains(node); |
272 | 274 |
273 String _typeSelector(String selectors) { | |
274 selectors = selectors.trim(); | |
275 if (!_isTypeSelector(selectors)) { | |
276 throw new UnimplementedError('only type selectors are implemented'); | |
277 } | |
278 return selectors; | |
279 } | |
280 | |
281 /// Checks if this is a type selector. | 275 /// Checks if this is a type selector. |
282 /// See <http://www.w3.org/TR/CSS2/grammar.html>. | 276 /// See <http://www.w3.org/TR/CSS2/grammar.html>. |
283 /// Note: this doesn't support '*', the universal selector, non-ascii chars or | 277 /// Note: this doesn't support '*', the universal selector, non-ascii chars or |
284 /// escape chars. | 278 /// escape chars. |
285 bool _isTypeSelector(String selector) { | 279 bool _isTypeSelector(String selector) { |
286 // Parser: | 280 // Parser: |
287 | 281 |
288 // element_name | 282 // element_name |
289 // : IDENT | '*' | 283 // : IDENT | '*' |
290 // ; | 284 // ; |
(...skipping 20 matching lines...) Expand all Loading... |
311 | 305 |
312 for (; i < len; i++) { | 306 for (; i < len; i++) { |
313 if (!isLetterOrDigit(selector[i]) && selector.codeUnitAt(i) != DASH) { | 307 if (!isLetterOrDigit(selector[i]) && selector.codeUnitAt(i) != DASH) { |
314 return false; | 308 return false; |
315 } | 309 } |
316 } | 310 } |
317 | 311 |
318 return true; | 312 return true; |
319 } | 313 } |
320 | 314 |
321 Element _queryType(String tag) { | |
322 for (var node in nodes) { | |
323 if (node is! Element) continue; | |
324 if (node.localName == tag) return node; | |
325 var result = node._queryType(tag); | |
326 if (result != null) return result; | |
327 } | |
328 return null; | |
329 } | |
330 | |
331 void _queryAllType(String tag, List<Element> results) { | |
332 for (var node in nodes) { | |
333 if (node is! Element) continue; | |
334 if (node.localName == tag) results.add(node); | |
335 node._queryAllType(tag, results); | |
336 } | |
337 } | |
338 | |
339 /// Initialize [attributeSpans] using [sourceSpan]. | 315 /// Initialize [attributeSpans] using [sourceSpan]. |
340 void _ensureAttributeSpans() { | 316 void _ensureAttributeSpans() { |
341 if (_attributeSpans != null) return; | 317 if (_attributeSpans != null) return; |
342 | 318 |
343 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); | 319 _attributeSpans = new LinkedHashMap<dynamic, FileSpan>(); |
344 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); | 320 _attributeValueSpans = new LinkedHashMap<dynamic, FileSpan>(); |
345 | 321 |
346 if (sourceSpan == null) return; | 322 if (sourceSpan == null) return; |
347 | 323 |
348 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true, | 324 var tokenizer = new HtmlTokenizer(sourceSpan.text, generateSpans: true, |
349 attributeSpans: true); | 325 attributeSpans: true); |
350 | 326 |
351 tokenizer.moveNext(); | 327 tokenizer.moveNext(); |
352 var token = tokenizer.current as StartTagToken; | 328 var token = tokenizer.current as StartTagToken; |
353 | 329 |
354 if (token.attributeSpans == null) return; // no attributes | 330 if (token.attributeSpans == null) return; // no attributes |
355 | 331 |
356 for (var attr in token.attributeSpans) { | 332 for (var attr in token.attributeSpans) { |
357 var offset = sourceSpan.start.offset; | 333 var offset = sourceSpan.start.offset; |
358 _attributeSpans[attr.name] = sourceSpan.file.span( | 334 _attributeSpans[attr.name] = sourceSpan.file.span( |
359 offset + attr.start, offset + attr.end); | 335 offset + attr.start, offset + attr.end); |
360 if (attr.startValue != null) { | 336 if (attr.startValue != null) { |
361 _attributeValueSpans[attr.name] = sourceSpan.file.span( | 337 _attributeValueSpans[attr.name] = sourceSpan.file.span( |
362 offset + attr.startValue, offset + attr.endValue); | 338 offset + attr.startValue, offset + attr.endValue); |
363 } | 339 } |
364 } | 340 } |
365 } | 341 } |
| 342 |
| 343 _clone(Node shallowClone, bool deep) { |
| 344 if (deep) { |
| 345 for (var child in nodes) { |
| 346 shallowClone.append(child.clone(true)); |
| 347 } |
| 348 } |
| 349 return shallowClone; |
| 350 } |
366 } | 351 } |
367 | 352 |
368 class Document extends Node { | 353 class Document extends Node |
| 354 with _ParentNode, _NonElementParentNode, _ElementAndDocument { |
| 355 |
369 Document() : super._(); | 356 Document() : super._(); |
370 factory Document.html(String html) => parse(html); | 357 factory Document.html(String html) => parse(html); |
371 | 358 |
372 int get nodeType => Node.DOCUMENT_NODE; | 359 int get nodeType => Node.DOCUMENT_NODE; |
373 | 360 |
374 // TODO(jmesserly): optmize this if needed | 361 // TODO(jmesserly): optmize this if needed |
375 Element get documentElement => querySelector('html'); | 362 Element get documentElement => querySelector('html'); |
376 Element get head => documentElement.querySelector('head'); | 363 Element get head => documentElement.querySelector('head'); |
377 Element get body => documentElement.querySelector('body'); | 364 Element get body => documentElement.querySelector('body'); |
378 | 365 |
379 /// Returns a fragment of HTML or XML that represents the element and its | 366 /// Returns a fragment of HTML or XML that represents the element and its |
380 /// contents. | 367 /// contents. |
381 // TODO(jmesserly): this API is not specified in: | 368 // TODO(jmesserly): this API is not specified in: |
382 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead | 369 // <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 | 370 // only Element has outerHtml. However it is quite useful. Should we move it |
384 // to dom_parsing, where we keep other custom APIs? | 371 // to dom_parsing, where we keep other custom APIs? |
385 String get outerHtml => _outerHtml; | 372 String get outerHtml => _outerHtml; |
386 | 373 |
387 String toString() => "#document"; | 374 String toString() => "#document"; |
388 | 375 |
389 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); | 376 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); |
390 | 377 |
391 Document clone() => new Document(); | 378 Document clone(bool deep) => _clone(new Document(), deep); |
| 379 |
| 380 Element createElement(String tag) => new Element.tag(tag); |
| 381 |
| 382 // TODO(jmesserly): this is only a partial implementation of: |
| 383 // http://dom.spec.whatwg.org/#dom-document-createelementns |
| 384 Element createElementNS(String namespaceUri, String tag) { |
| 385 if (namespaceUri == '') namespaceUri = null; |
| 386 return new Element._(tag, namespaceUri); |
| 387 } |
| 388 |
| 389 DocumentFragment createDocumentFragment() => new DocumentFragment(); |
392 } | 390 } |
393 | 391 |
394 class DocumentFragment extends Document { | 392 class DocumentFragment extends Node |
395 DocumentFragment(); | 393 with _ParentNode, _NonElementParentNode { |
| 394 |
| 395 DocumentFragment() : super._(); |
396 factory DocumentFragment.html(String html) => parseFragment(html); | 396 factory DocumentFragment.html(String html) => parseFragment(html); |
397 | 397 |
398 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; | 398 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; |
399 | 399 |
| 400 /// Returns a fragment of HTML or XML that represents the element and its |
| 401 /// contents. |
| 402 // TODO(jmesserly): this API is not specified in: |
| 403 // <http://domparsing.spec.whatwg.org/> nor is it in dart:html, instead |
| 404 // only Element has outerHtml. However it is quite useful. Should we move it |
| 405 // to dom_parsing, where we keep other custom APIs? |
| 406 String get outerHtml => _outerHtml; |
| 407 |
400 String toString() => "#document-fragment"; | 408 String toString() => "#document-fragment"; |
401 | 409 |
402 DocumentFragment clone() => new DocumentFragment(); | 410 DocumentFragment clone(bool deep) => _clone(new DocumentFragment(), deep); |
| 411 |
| 412 void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); |
403 | 413 |
404 String get text => _getText(this); | 414 String get text => _getText(this); |
405 set text(String value) => _setText(this, value); | 415 set text(String value) => _setText(this, value); |
406 } | 416 } |
407 | 417 |
408 class DocumentType extends Node { | 418 class DocumentType extends Node { |
409 final String name; | 419 final String name; |
410 final String publicId; | 420 final String publicId; |
411 final String systemId; | 421 final String systemId; |
412 | 422 |
413 DocumentType(String name, this.publicId, this.systemId) | 423 DocumentType(String name, this.publicId, this.systemId) |
414 // Note: once Node.tagName is removed, don't pass "name" to super | 424 // Note: once Node.tagName is removed, don't pass "name" to super |
415 : name = name, super._(name); | 425 : name = name, super._(); |
416 | 426 |
417 int get nodeType => Node.DOCUMENT_TYPE_NODE; | 427 int get nodeType => Node.DOCUMENT_TYPE_NODE; |
418 | 428 |
419 String toString() { | 429 String toString() { |
420 if (publicId != null || systemId != null) { | 430 if (publicId != null || systemId != null) { |
421 // TODO(jmesserly): the html5 serialization spec does not add these. But | 431 // TODO(jmesserly): the html5 serialization spec does not add these. But |
422 // it seems useful, and the parser can handle it, so for now keeping it. | 432 // it seems useful, and the parser can handle it, so for now keeping it. |
423 var pid = publicId != null ? publicId : ''; | 433 var pid = publicId != null ? publicId : ''; |
424 var sid = systemId != null ? systemId : ''; | 434 var sid = systemId != null ? systemId : ''; |
425 return '<!DOCTYPE $name "$pid" "$sid">'; | 435 return '<!DOCTYPE $name "$pid" "$sid">'; |
426 } else { | 436 } else { |
427 return '<!DOCTYPE $name>'; | 437 return '<!DOCTYPE $name>'; |
428 } | 438 } |
429 } | 439 } |
430 | 440 |
431 | 441 |
432 void _addOuterHtml(StringBuffer str) { | 442 void _addOuterHtml(StringBuffer str) { |
433 str.write(toString()); | 443 str.write(toString()); |
434 } | 444 } |
435 | 445 |
436 DocumentType clone() => new DocumentType(name, publicId, systemId); | 446 DocumentType clone(bool deep) => new DocumentType(name, publicId, systemId); |
437 } | 447 } |
438 | 448 |
439 class Text extends Node { | 449 class Text extends Node { |
440 String data; | 450 String data; |
441 | 451 |
442 Text(this.data) : super._(); | 452 Text(this.data) : super._(); |
443 | 453 |
444 /// *Deprecated* use [data]. | |
445 @deprecated String get value => data; | |
446 @deprecated set value(String x) { data = x; } | |
447 | |
448 int get nodeType => Node.TEXT_NODE; | 454 int get nodeType => Node.TEXT_NODE; |
449 | 455 |
450 String toString() => '"$data"'; | 456 String toString() => '"$data"'; |
451 | 457 |
452 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this); | 458 void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this); |
453 | 459 |
454 Text clone() => new Text(data); | 460 Text clone(bool deep) => new Text(data); |
455 | 461 |
456 String get text => data; | 462 String get text => data; |
457 set text(String value) { data = value; } | 463 set text(String value) { data = value; } |
458 } | 464 } |
459 | 465 |
460 class Element extends Node { | 466 // TODO(jmesserly): Elements should have a pointer back to their document |
| 467 class Element extends Node with _ParentNode, _ElementAndDocument { |
461 final String namespaceUri; | 468 final String namespaceUri; |
462 | 469 |
463 @deprecated String get namespace => namespaceUri; | |
464 | |
465 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name) | 470 /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name) |
466 /// of this element. | 471 /// of this element. |
467 String get localName => _tagName; | 472 final String localName; |
468 | 473 |
469 // TODO(jmesserly): deprecate in favor of [Document.createElementNS]. | 474 Element._(this.localName, [this.namespaceUri]) : super._(); |
470 // However we need every element to have a Document before this can work. | |
471 Element(String name, [this.namespaceUri]) : super._(name); | |
472 | 475 |
473 Element.tag(String name) : namespaceUri = null, super._(name); | 476 Element.tag(this.localName) : namespaceUri = Namespaces.html, super._(); |
474 | 477 |
475 static final _START_TAG_REGEXP = new RegExp('<(\\w+)'); | 478 static final _START_TAG_REGEXP = new RegExp('<(\\w+)'); |
476 | 479 |
477 static final _CUSTOM_PARENT_TAG_MAP = const { | 480 static final _CUSTOM_PARENT_TAG_MAP = const { |
478 'body': 'html', | 481 'body': 'html', |
479 'head': 'html', | 482 'head': 'html', |
480 'caption': 'table', | 483 'caption': 'table', |
481 'td': 'tr', | 484 'td': 'tr', |
482 'colgroup': 'table', | 485 'colgroup': 'table', |
483 'col': 'colgroup', | 486 'col': 'colgroup', |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
521 } else { | 524 } else { |
522 throw new ArgumentError('HTML had ${fragment.children.length} ' | 525 throw new ArgumentError('HTML had ${fragment.children.length} ' |
523 'top level elements but 1 expected'); | 526 'top level elements but 1 expected'); |
524 } | 527 } |
525 element.remove(); | 528 element.remove(); |
526 return element; | 529 return element; |
527 } | 530 } |
528 | 531 |
529 int get nodeType => Node.ELEMENT_NODE; | 532 int get nodeType => Node.ELEMENT_NODE; |
530 | 533 |
| 534 // TODO(jmesserly): we can make this faster |
| 535 Element get previousElementSibling { |
| 536 if (parentNode == null) return null; |
| 537 var siblings = parentNode.nodes; |
| 538 for (int i = siblings.indexOf(this) - 1; i >= 0; i--) { |
| 539 var s = siblings[i]; |
| 540 if (s is Element) return s; |
| 541 } |
| 542 return null; |
| 543 } |
| 544 |
| 545 Element get nextElementSibling { |
| 546 if (parentNode == null) return null; |
| 547 var siblings = parentNode.nodes; |
| 548 for (int i = siblings.indexOf(this) + 1; i < siblings.length; i++) { |
| 549 var s = siblings[i]; |
| 550 if (s is Element) return s; |
| 551 } |
| 552 return null; |
| 553 } |
| 554 |
531 String toString() { | 555 String toString() { |
532 if (namespaceUri == null) return "<$localName>"; | 556 var prefix = Namespaces.getPrefix(namespaceUri); |
533 return "<${Namespaces.getPrefix(namespaceUri)} $localName>"; | 557 return "<${prefix == null ? '' : '$prefix '}$localName>"; |
534 } | 558 } |
535 | 559 |
536 String get text => _getText(this); | 560 String get text => _getText(this); |
537 set text(String value) => _setText(this, value); | 561 set text(String value) => _setText(this, value); |
538 | 562 |
539 /// Returns a fragment of HTML or XML that represents the element and its | 563 /// Returns a fragment of HTML or XML that represents the element and its |
540 /// contents. | 564 /// contents. |
541 String get outerHtml => _outerHtml; | 565 String get outerHtml => _outerHtml; |
542 | 566 |
543 /// Returns a fragment of HTML or XML that represents the element's contents. | 567 /// 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 | 568 /// Can be set, to replace the contents of the element with nodes parsed from |
545 /// the given string. | 569 /// the given string. |
546 String get innerHtml => _innerHtml; | 570 String get innerHtml => _innerHtml; |
547 // TODO(jmesserly): deprecate in favor of: | 571 // TODO(jmesserly): deprecate in favor of: |
548 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id
_setInnerHtml> | 572 // <https://api.dartlang.org/apidocs/channels/stable/#dart-dom-html.Element@id
_setInnerHtml> |
549 set innerHtml(String value) { _innerHtml = value; } | 573 set innerHtml(String value) { |
| 574 nodes.clear(); |
| 575 // TODO(jmesserly): should be able to get the same effect by adding the |
| 576 // fragment directly. |
| 577 nodes.addAll(parseFragment(value, container: localName).nodes); |
| 578 } |
550 | 579 |
551 void _addOuterHtml(StringBuffer str) { | 580 void _addOuterHtml(StringBuffer str) { |
552 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#
serializing-html-fragments | 581 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#
serializing-html-fragments |
553 // Element is the most complicated one. | 582 // Element is the most complicated one. |
554 if (namespaceUri == null || | 583 str.write('<${_getSerializationPrefix(namespaceUri)}$localName'); |
555 namespaceUri == Namespaces.html || | |
556 namespaceUri == Namespaces.mathml || | |
557 namespaceUri == Namespaces.svg) { | |
558 str.write('<$localName'); | |
559 } else { | |
560 // TODO(jmesserly): the spec doesn't define "qualified name". | |
561 // I'm not sure if this is correct, but it should parse reasonably. | |
562 str.write('<${Namespaces.getPrefix(namespaceUri)}:$localName'); | |
563 } | |
564 | 584 |
565 if (attributes.length > 0) { | 585 if (attributes.length > 0) { |
566 attributes.forEach((key, v) { | 586 attributes.forEach((key, v) { |
567 // Note: AttributeName.toString handles serialization of attribute | 587 // Note: AttributeName.toString handles serialization of attribute |
568 // namespace, if needed. | 588 // namespace, if needed. |
569 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); | 589 str.write(' $key="${htmlSerializeEscape(v, attributeMode: true)}"'); |
570 }); | 590 }); |
571 } | 591 } |
572 | 592 |
573 str.write('>'); | 593 str.write('>'); |
(...skipping 10 matching lines...) Expand all Loading... |
584 } | 604 } |
585 | 605 |
586 _addInnerHtml(str); | 606 _addInnerHtml(str); |
587 } | 607 } |
588 | 608 |
589 // void elements must not have an end tag | 609 // void elements must not have an end tag |
590 // http://dev.w3.org/html5/markup/syntax.html#void-elements | 610 // http://dev.w3.org/html5/markup/syntax.html#void-elements |
591 if (!isVoidElement(localName)) str.write('</$localName>'); | 611 if (!isVoidElement(localName)) str.write('</$localName>'); |
592 } | 612 } |
593 | 613 |
594 Element clone() => new Element(localName, namespaceUri) | 614 static String _getSerializationPrefix(String uri) { |
595 ..attributes = new LinkedHashMap.from(attributes); | 615 if (uri == null || |
| 616 uri == Namespaces.html || |
| 617 uri == Namespaces.mathml || |
| 618 uri == Namespaces.svg) { |
| 619 return ''; |
| 620 } |
| 621 var prefix = Namespaces.getPrefix(uri); |
| 622 // TODO(jmesserly): the spec doesn't define "qualified name". |
| 623 // I'm not sure if this is correct, but it should parse reasonably. |
| 624 return prefix == null ? '' : '$prefix:'; |
| 625 } |
596 | 626 |
| 627 Element clone(bool deep) { |
| 628 var result = new Element._(localName, namespaceUri) |
| 629 ..attributes = new LinkedHashMap.from(attributes); |
| 630 return _clone(result, deep); |
| 631 } |
| 632 |
| 633 // http://dom.spec.whatwg.org/#dom-element-id |
597 String get id { | 634 String get id { |
598 var result = attributes['id']; | 635 var result = attributes['id']; |
599 return result != null ? result : ''; | 636 return result != null ? result : ''; |
600 } | 637 } |
601 | 638 |
602 set id(String value) { | 639 set id(String value) { |
603 if (value == null) { | 640 attributes['id'] = '$value'; |
604 attributes.remove('id'); | |
605 } else { | |
606 attributes['id'] = value; | |
607 } | |
608 } | 641 } |
| 642 |
| 643 // http://dom.spec.whatwg.org/#dom-element-classname |
| 644 String get className { |
| 645 var result = attributes['class']; |
| 646 return result != null ? result : ''; |
| 647 } |
| 648 |
| 649 set className(String value) { |
| 650 attributes['class'] = '$value'; |
| 651 } |
| 652 |
| 653 /** |
| 654 * The set of CSS classes applied to this element. |
| 655 * |
| 656 * This set makes it easy to add, remove or toggle the classes applied to |
| 657 * this element. |
| 658 * |
| 659 * element.classes.add('selected'); |
| 660 * element.classes.toggle('isOnline'); |
| 661 * element.classes.remove('selected'); |
| 662 */ |
| 663 CssClassSet get classes => new ElementCssClassSet(this); |
609 } | 664 } |
610 | 665 |
611 class Comment extends Node { | 666 class Comment extends Node { |
612 String data; | 667 String data; |
613 | 668 |
614 Comment(this.data) : super._(); | 669 Comment(this.data) : super._(); |
615 | 670 |
616 int get nodeType => Node.COMMENT_NODE; | 671 int get nodeType => Node.COMMENT_NODE; |
617 | 672 |
618 String toString() => "<!-- $data -->"; | 673 String toString() => "<!-- $data -->"; |
619 | 674 |
620 void _addOuterHtml(StringBuffer str) { | 675 void _addOuterHtml(StringBuffer str) { |
621 str.write("<!--$data-->"); | 676 str.write("<!--$data-->"); |
622 } | 677 } |
623 | 678 |
624 Comment clone() => new Comment(data); | 679 Comment clone(bool deep) => new Comment(data); |
625 | 680 |
626 String get text => data; | 681 String get text => data; |
627 set text(String value) { | 682 set text(String value) { |
628 this.data = value; | 683 this.data = value; |
629 } | 684 } |
630 } | 685 } |
631 | 686 |
632 | 687 |
633 // TODO(jmesserly): fix this to extend one of the corelib classes if possible. | 688 // TODO(jmesserly): fix this to extend one of the corelib classes if possible. |
634 // (The requirement to remove the node from the old node list makes it tricky.) | 689 // (The requirement to remove the node from the old node list makes it tricky.) |
635 // TODO(jmesserly): is there any way to share code with the _NodeListImpl? | 690 // TODO(jmesserly): is there any way to share code with the _NodeListImpl? |
636 class NodeList extends ListProxy<Node> { | 691 class NodeList extends ListProxy<Node> { |
637 // Note: this is conceptually final, but because of circular reference | 692 // Note: this is conceptually final, but because of circular reference |
638 // between Node and NodeList we initialize it after construction. | 693 // between Node and NodeList we initialize it after construction. |
639 Node _parent; | 694 Node _parent; |
640 | 695 |
641 NodeList._(); | 696 NodeList._(); |
642 | 697 |
643 Node get first => this[0]; | 698 Node get first => this[0]; |
644 | 699 |
645 Node _setParent(Node node) { | 700 Node _setParent(Node node) { |
646 // Note: we need to remove the node from its previous parent node, if any, | 701 // Note: we need to remove the node from its previous parent node, if any, |
647 // before updating its parent pointer to point at our parent. | 702 // before updating its parent pointer to point at our parent. |
648 node.remove(); | 703 node.remove(); |
649 node.parent = _parent; | 704 node.parentNode = _parent; |
650 return node; | 705 return node; |
651 } | 706 } |
652 | 707 |
653 void add(Node value) { | 708 void add(Node value) { |
654 if (value is DocumentFragment) { | 709 if (value is DocumentFragment) { |
655 addAll(value.nodes); | 710 addAll(value.nodes); |
656 } else { | 711 } else { |
657 super.add(_setParent(value)); | 712 super.add(_setParent(value)); |
658 } | 713 } |
659 } | 714 } |
(...skipping 14 matching lines...) Expand all Loading... |
674 } | 729 } |
675 | 730 |
676 void insert(int index, Node value) { | 731 void insert(int index, Node value) { |
677 if (value is DocumentFragment) { | 732 if (value is DocumentFragment) { |
678 insertAll(index, value.nodes); | 733 insertAll(index, value.nodes); |
679 } else { | 734 } else { |
680 super.insert(index, _setParent(value)); | 735 super.insert(index, _setParent(value)); |
681 } | 736 } |
682 } | 737 } |
683 | 738 |
684 Node removeLast() => super.removeLast()..parent = null; | 739 Node removeLast() => super.removeLast()..parentNode = null; |
685 | 740 |
686 Node removeAt(int i) => super.removeAt(i)..parent = null; | 741 Node removeAt(int i) => super.removeAt(i)..parentNode = null; |
687 | 742 |
688 void clear() { | 743 void clear() { |
689 for (var node in this) node.parent = null; | 744 for (var node in this) node.parentNode = null; |
690 super.clear(); | 745 super.clear(); |
691 } | 746 } |
692 | 747 |
693 void operator []=(int index, Node value) { | 748 void operator []=(int index, Node value) { |
694 if (value is DocumentFragment) { | 749 if (value is DocumentFragment) { |
695 removeAt(index); | 750 removeAt(index); |
696 insertAll(index, value.nodes); | 751 insertAll(index, value.nodes); |
697 } else { | 752 } else { |
698 this[index].parent = null; | 753 this[index].parentNode = null; |
699 super[index] = _setParent(value); | 754 super[index] = _setParent(value); |
700 } | 755 } |
701 } | 756 } |
702 | 757 |
703 // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see | 758 // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see |
704 // http://code.google.com/p/dart/issues/detail?id=5371 | 759 // http://code.google.com/p/dart/issues/detail?id=5371 |
705 void setRange(int start, int rangeLength, List<Node> from, | 760 void setRange(int start, int rangeLength, List<Node> from, |
706 [int startFrom = 0]) { | 761 [int startFrom = 0]) { |
707 if (from is NodeList) { | 762 if (from is NodeList) { |
708 // Note: this is presumed to make a copy | 763 // Note: this is presumed to make a copy |
709 from = from.sublist(startFrom, startFrom + rangeLength); | 764 from = from.sublist(startFrom, startFrom + rangeLength); |
710 } | 765 } |
711 // Note: see comment in [addAll]. We need to be careful about the order of | 766 // Note: see comment in [addAll]. We need to be careful about the order of |
712 // operations if [from] is also a NodeList. | 767 // operations if [from] is also a NodeList. |
713 for (int i = rangeLength - 1; i >= 0; i--) { | 768 for (int i = rangeLength - 1; i >= 0; i--) { |
714 this[start + i] = from[startFrom + i]; | 769 this[start + i] = from[startFrom + i]; |
715 } | 770 } |
716 } | 771 } |
717 | 772 |
718 void replaceRange(int start, int end, Iterable<Node> newContents) { | 773 void replaceRange(int start, int end, Iterable<Node> newContents) { |
719 removeRange(start, end); | 774 removeRange(start, end); |
720 insertAll(start, newContents); | 775 insertAll(start, newContents); |
721 } | 776 } |
722 | 777 |
723 void removeRange(int start, int rangeLength) { | 778 void removeRange(int start, int rangeLength) { |
724 for (int i = start; i < rangeLength; i++) this[i].parent = null; | 779 for (int i = start; i < rangeLength; i++) this[i].parentNode = null; |
725 super.removeRange(start, rangeLength); | 780 super.removeRange(start, rangeLength); |
726 } | 781 } |
727 | 782 |
728 void removeWhere(bool test(Element e)) { | 783 void removeWhere(bool test(Element e)) { |
729 for (var node in where(test)) { | 784 for (var node in where(test)) { |
730 node.parent = null; | 785 node.parentNode = null; |
731 } | 786 } |
732 super.removeWhere(test); | 787 super.removeWhere(test); |
733 } | 788 } |
734 | 789 |
735 void retainWhere(bool test(Element e)) { | 790 void retainWhere(bool test(Element e)) { |
736 for (var node in where((n) => !test(n))) { | 791 for (var node in where((n) => !test(n))) { |
737 node.parent = null; | 792 node.parentNode = null; |
738 } | 793 } |
739 super.retainWhere(test); | 794 super.retainWhere(test); |
740 } | 795 } |
741 | 796 |
742 void insertAll(int index, Iterable<Node> collection) { | 797 void insertAll(int index, Iterable<Node> collection) { |
743 // Note: we need to be careful how we copy nodes. See note in addAll. | 798 // Note: we need to be careful how we copy nodes. See note in addAll. |
744 var list = _flattenDocFragments(collection); | 799 var list = _flattenDocFragments(collection); |
745 for (var node in list.reversed) _setParent(node); | 800 for (var node in list.reversed) _setParent(node); |
746 super.insertAll(index, list); | 801 super.insertAll(index, list); |
747 } | 802 } |
(...skipping 208 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
956 | 1011 |
957 class _ConcatTextVisitor extends TreeVisitor { | 1012 class _ConcatTextVisitor extends TreeVisitor { |
958 final _str = new StringBuffer(); | 1013 final _str = new StringBuffer(); |
959 | 1014 |
960 String toString() => _str.toString(); | 1015 String toString() => _str.toString(); |
961 | 1016 |
962 visitText(Text node) { | 1017 visitText(Text node) { |
963 _str.write(node.data); | 1018 _str.write(node.data); |
964 } | 1019 } |
965 } | 1020 } |
OLD | NEW |