OLD | NEW |
1 /// Internals to the tree builders. | 1 /// Internals to the tree builders. |
2 library treebuilder; | 2 library treebuilder; |
3 | 3 |
4 import 'dart:collection'; | 4 import 'dart:collection'; |
5 import 'package:html5lib/dom.dart'; | 5 import 'package:html5lib/dom.dart'; |
| 6 import 'package:html5lib/parser.dart' show getElementNameTuple; |
6 import 'package:source_maps/span.dart' show FileSpan; | 7 import 'package:source_maps/span.dart' show FileSpan; |
7 import 'constants.dart'; | 8 import 'constants.dart'; |
8 import 'list_proxy.dart'; | 9 import 'list_proxy.dart'; |
9 import 'token.dart'; | 10 import 'token.dart'; |
10 import 'utils.dart'; | 11 import 'utils.dart'; |
11 | 12 |
12 // The scope markers are inserted when entering object elements, | 13 // The scope markers are inserted when entering object elements, |
13 // marquees, table cells, and table captions, and are used to prevent formatting | 14 // marquees, table cells, and table captions, and are used to prevent formatting |
14 // from "leaking" into tables, object elements, and marquees. | 15 // from "leaking" into tables, object elements, and marquees. |
15 const Node Marker = null; | 16 const Node Marker = null; |
16 | 17 |
17 // TODO(jmesserly): this should extend ListBase<Node>, but my simple attempt | 18 // TODO(jmesserly): this should extend ListBase<Element>, but my simple attempt |
18 // didn't work. | 19 // didn't work. |
19 class ActiveFormattingElements extends ListProxy<Node> { | 20 class ActiveFormattingElements extends ListProxy<Element> { |
20 ActiveFormattingElements() : super(); | 21 ActiveFormattingElements() : super(); |
21 | 22 |
22 // Override the "add" method. | 23 // Override the "add" method. |
23 // TODO(jmesserly): I'd rather not override this; can we do this in the | 24 // TODO(jmesserly): I'd rather not override this; can we do this in the |
24 // calling code instead? | 25 // calling code instead? |
25 void add(Node node) { | 26 void add(Element node) { |
26 int equalCount = 0; | 27 int equalCount = 0; |
27 if (node != Marker) { | 28 if (node != Marker) { |
28 for (Node element in reversed) { | 29 for (var element in reversed) { |
29 if (element == Marker) { | 30 if (element == Marker) { |
30 break; | 31 break; |
31 } | 32 } |
32 if (_nodesEqual(element, node)) { | 33 if (_nodesEqual(element, node)) { |
33 equalCount += 1; | 34 equalCount += 1; |
34 } | 35 } |
35 if (equalCount == 3) { | 36 if (equalCount == 3) { |
36 remove(element); | 37 remove(element); |
37 break; | 38 break; |
38 } | 39 } |
(...skipping 15 matching lines...) Expand all Loading... |
54 } | 55 } |
55 | 56 |
56 if (a[keyA] != valB) { | 57 if (a[keyA] != valB) { |
57 return false; | 58 return false; |
58 } | 59 } |
59 } | 60 } |
60 return true; | 61 return true; |
61 } | 62 } |
62 | 63 |
63 | 64 |
64 bool _nodesEqual(Node node1, Node node2) { | 65 bool _nodesEqual(Element node1, Element node2) { |
65 return node1.nameTuple == node2.nameTuple && | 66 return getElementNameTuple(node1) == getElementNameTuple(node2) && |
66 _mapEquals(node1.attributes, node2.attributes); | 67 _mapEquals(node1.attributes, node2.attributes); |
67 } | 68 } |
68 | 69 |
69 /// Basic treebuilder implementation. | 70 /// Basic treebuilder implementation. |
70 class TreeBuilder { | 71 class TreeBuilder { |
71 final String defaultNamespace; | 72 final String defaultNamespace; |
72 | 73 |
73 Document document; | 74 Document document; |
74 | 75 |
75 final openElements = <Node>[]; | 76 final List<Element> openElements = <Element>[]; |
76 | 77 |
77 final activeFormattingElements = new ActiveFormattingElements(); | 78 final activeFormattingElements = new ActiveFormattingElements(); |
78 | 79 |
79 Node headPointer; | 80 Node headPointer; |
80 | 81 |
81 Node formPointer; | 82 Node formPointer; |
82 | 83 |
83 /// Switch the function used to insert an element from the | 84 /// Switch the function used to insert an element from the |
84 /// normal one to the misnested table one and back again | 85 /// normal one to the misnested table one and back again |
85 bool insertFromTable; | 86 bool insertFromTable; |
(...skipping 12 matching lines...) Expand all Loading... |
98 formPointer = null; | 99 formPointer = null; |
99 | 100 |
100 insertFromTable = false; | 101 insertFromTable = false; |
101 | 102 |
102 document = new Document(); | 103 document = new Document(); |
103 } | 104 } |
104 | 105 |
105 bool elementInScope(target, {String variant}) { | 106 bool elementInScope(target, {String variant}) { |
106 //If we pass a node in we match that. if we pass a string | 107 //If we pass a node in we match that. if we pass a string |
107 //match any node with that name | 108 //match any node with that name |
108 bool exactNode = target is Node && target.nameTuple != null; | 109 bool exactNode = target is Node; |
109 | 110 |
110 List listElements1 = scopingElements; | 111 List listElements1 = scopingElements; |
111 List listElements2 = const []; | 112 List listElements2 = const []; |
112 bool invert = false; | 113 bool invert = false; |
113 if (variant != null) { | 114 if (variant != null) { |
114 switch (variant) { | 115 switch (variant) { |
115 case "button": | 116 case "button": |
116 listElements2 = const [const Pair(Namespaces.html, "button")]; | 117 listElements2 = const [const Pair(Namespaces.html, "button")]; |
117 break; | 118 break; |
118 case "list": | 119 case "list": |
119 listElements2 = const [const Pair(Namespaces.html, "ol"), | 120 listElements2 = const [const Pair(Namespaces.html, "ol"), |
120 const Pair(Namespaces.html, "ul")]; | 121 const Pair(Namespaces.html, "ul")]; |
121 break; | 122 break; |
122 case "table": | 123 case "table": |
123 listElements1 = const [const Pair(Namespaces.html, "html"), | 124 listElements1 = const [const Pair(Namespaces.html, "html"), |
124 const Pair(Namespaces.html, "table")]; | 125 const Pair(Namespaces.html, "table")]; |
125 break; | 126 break; |
126 case "select": | 127 case "select": |
127 listElements1 = const [const Pair(Namespaces.html, "optgroup"), | 128 listElements1 = const [const Pair(Namespaces.html, "optgroup"), |
128 const Pair(Namespaces.html, "option")]; | 129 const Pair(Namespaces.html, "option")]; |
129 invert = true; | 130 invert = true; |
130 break; | 131 break; |
131 default: | 132 default: |
132 throw new StateError('We should never reach this point'); | 133 throw new StateError('We should never reach this point'); |
133 } | 134 } |
134 } | 135 } |
135 | 136 |
136 for (Node node in openElements.reversed) { | 137 for (var node in openElements.reversed) { |
137 if (node.tagName == target && !exactNode || | 138 if (!exactNode && node.localName == target || |
138 node == target && exactNode) { | 139 exactNode && node == target) { |
139 return true; | 140 return true; |
140 } else if (invert != | 141 } else if (invert != |
141 (listElements1.contains(node.nameTuple) || | 142 (listElements1.contains(getElementNameTuple(node)) || |
142 listElements2.contains(node.nameTuple))) { | 143 listElements2.contains(getElementNameTuple(node)))) { |
143 return false; | 144 return false; |
144 } | 145 } |
145 } | 146 } |
146 | 147 |
147 throw new StateError('We should never reach this point'); | 148 throw new StateError('We should never reach this point'); |
148 } | 149 } |
149 | 150 |
150 void reconstructActiveFormattingElements() { | 151 void reconstructActiveFormattingElements() { |
151 // Within this algorithm the order of steps described in the | 152 // Within this algorithm the order of steps described in the |
152 // specification is not quite the same as the order of steps in the | 153 // specification is not quite the same as the order of steps in the |
(...skipping 25 matching lines...) Expand all Loading... |
178 | 179 |
179 while (true) { | 180 while (true) { |
180 // Step 7 | 181 // Step 7 |
181 i += 1; | 182 i += 1; |
182 | 183 |
183 // Step 8 | 184 // Step 8 |
184 entry = activeFormattingElements[i]; | 185 entry = activeFormattingElements[i]; |
185 | 186 |
186 // TODO(jmesserly): optimize this. No need to create a token. | 187 // TODO(jmesserly): optimize this. No need to create a token. |
187 var cloneToken = new StartTagToken( | 188 var cloneToken = new StartTagToken( |
188 entry.tagName, | 189 entry.localName, |
189 namespace: entry.namespace, | 190 namespace: entry.namespaceUri, |
190 data: new LinkedHashMap.from(entry.attributes)) | 191 data: new LinkedHashMap.from(entry.attributes)) |
191 ..span = entry.sourceSpan; | 192 ..span = entry.sourceSpan; |
192 | 193 |
193 // Step 9 | 194 // Step 9 |
194 var element = insertElement(cloneToken); | 195 var element = insertElement(cloneToken); |
195 | 196 |
196 // Step 10 | 197 // Step 10 |
197 activeFormattingElements[i] = element; | 198 activeFormattingElements[i] = element; |
198 | 199 |
199 // Step 11 | 200 // Step 11 |
200 if (element == activeFormattingElements.last) { | 201 if (element == activeFormattingElements.last) { |
201 break; | 202 break; |
202 } | 203 } |
203 } | 204 } |
204 } | 205 } |
205 | 206 |
206 void clearActiveFormattingElements() { | 207 void clearActiveFormattingElements() { |
207 var entry = activeFormattingElements.removeLast(); | 208 var entry = activeFormattingElements.removeLast(); |
208 while (activeFormattingElements.length > 0 && entry != Marker) { | 209 while (activeFormattingElements.length > 0 && entry != Marker) { |
209 entry = activeFormattingElements.removeLast(); | 210 entry = activeFormattingElements.removeLast(); |
210 } | 211 } |
211 } | 212 } |
212 | 213 |
213 /// Check if an element exists between the end of the active | 214 /// Check if an element exists between the end of the active |
214 /// formatting elements and the last marker. If it does, return it, else | 215 /// formatting elements and the last marker. If it does, return it, else |
215 /// return null. | 216 /// return null. |
216 Node elementInActiveFormattingElements(String name) { | 217 Element elementInActiveFormattingElements(String name) { |
217 for (Node item in activeFormattingElements.reversed) { | 218 for (var item in activeFormattingElements.reversed) { |
218 // Check for Marker first because if it's a Marker it doesn't have a | 219 // Check for Marker first because if it's a Marker it doesn't have a |
219 // name attribute. | 220 // name attribute. |
220 if (item == Marker) { | 221 if (item == Marker) { |
221 break; | 222 break; |
222 } else if (item.tagName == name) { | 223 } else if (item.localName == name) { |
223 return item; | 224 return item; |
224 } | 225 } |
225 } | 226 } |
226 return null; | 227 return null; |
227 } | 228 } |
228 | 229 |
229 void insertRoot(Token token) { | 230 void insertRoot(Token token) { |
230 var element = createElement(token); | 231 var element = createElement(token); |
231 openElements.add(element); | 232 openElements.add(element); |
232 document.nodes.add(element); | 233 document.nodes.add(element); |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
269 ..attributes = token.data | 270 ..attributes = token.data |
270 ..sourceSpan = token.span; | 271 ..sourceSpan = token.span; |
271 openElements.last.nodes.add(element); | 272 openElements.last.nodes.add(element); |
272 openElements.add(element); | 273 openElements.add(element); |
273 return element; | 274 return element; |
274 } | 275 } |
275 | 276 |
276 Element insertElementTable(token) { | 277 Element insertElementTable(token) { |
277 /// Create an element and insert it into the tree | 278 /// Create an element and insert it into the tree |
278 var element = createElement(token); | 279 var element = createElement(token); |
279 if (!tableInsertModeElements.contains(openElements.last.tagName)) { | 280 if (!tableInsertModeElements.contains(openElements.last.localName)) { |
280 return insertElementNormal(token); | 281 return insertElementNormal(token); |
281 } else { | 282 } else { |
282 // We should be in the InTable mode. This means we want to do | 283 // We should be in the InTable mode. This means we want to do |
283 // special magic element rearranging | 284 // special magic element rearranging |
284 var nodePos = getTableMisnestedNodePosition(); | 285 var nodePos = getTableMisnestedNodePosition(); |
285 if (nodePos[1] == null) { | 286 if (nodePos[1] == null) { |
286 // TODO(jmesserly): I don't think this is reachable. If insertFromTable | 287 // TODO(jmesserly): I don't think this is reachable. If insertFromTable |
287 // is true, there will be a <table> element open, and it always has a | 288 // is true, there will be a <table> element open, and it always has a |
288 // parent pointer. | 289 // parent pointer. |
289 nodePos[0].nodes.add(element); | 290 nodePos[0].nodes.add(element); |
290 } else { | 291 } else { |
291 nodePos[0].insertBefore(element, nodePos[1]); | 292 nodePos[0].insertBefore(element, nodePos[1]); |
292 } | 293 } |
293 openElements.add(element); | 294 openElements.add(element); |
294 } | 295 } |
295 return element; | 296 return element; |
296 } | 297 } |
297 | 298 |
298 /// Insert text data. | 299 /// Insert text data. |
299 void insertText(String data, FileSpan span) { | 300 void insertText(String data, FileSpan span) { |
300 var parent = openElements.last; | 301 var parent = openElements.last; |
301 | 302 |
302 if (!insertFromTable || insertFromTable && | 303 if (!insertFromTable || insertFromTable && |
303 !tableInsertModeElements.contains(openElements.last.tagName)) { | 304 !tableInsertModeElements.contains(openElements.last.localName)) { |
304 _insertText(parent, data, span); | 305 _insertText(parent, data, span); |
305 } else { | 306 } else { |
306 // We should be in the InTable mode. This means we want to do | 307 // We should be in the InTable mode. This means we want to do |
307 // special magic element rearranging | 308 // special magic element rearranging |
308 var nodePos = getTableMisnestedNodePosition(); | 309 var nodePos = getTableMisnestedNodePosition(); |
309 _insertText(nodePos[0], data, span, nodePos[1]); | 310 _insertText(nodePos[0], data, span, nodePos[1]); |
310 } | 311 } |
311 } | 312 } |
312 | 313 |
313 /// Insert [data] as text in the current node, positioned before the | 314 /// Insert [data] as text in the current node, positioned before the |
(...skipping 26 matching lines...) Expand all Loading... |
340 | 341 |
341 /// Get the foster parent element, and sibling to insert before | 342 /// Get the foster parent element, and sibling to insert before |
342 /// (or null) when inserting a misnested table node | 343 /// (or null) when inserting a misnested table node |
343 List<Node> getTableMisnestedNodePosition() { | 344 List<Node> getTableMisnestedNodePosition() { |
344 // The foster parent element is the one which comes before the most | 345 // The foster parent element is the one which comes before the most |
345 // recently opened table element | 346 // recently opened table element |
346 // XXX - this is really inelegant | 347 // XXX - this is really inelegant |
347 Node lastTable = null; | 348 Node lastTable = null; |
348 Node fosterParent = null; | 349 Node fosterParent = null; |
349 var insertBefore = null; | 350 var insertBefore = null; |
350 for (Node elm in openElements.reversed) { | 351 for (var elm in openElements.reversed) { |
351 if (elm.tagName == "table") { | 352 if (elm.localName == "table") { |
352 lastTable = elm; | 353 lastTable = elm; |
353 break; | 354 break; |
354 } | 355 } |
355 } | 356 } |
356 if (lastTable != null) { | 357 if (lastTable != null) { |
357 // XXX - we should really check that this parent is actually a | 358 // XXX - we should really check that this parent is actually a |
358 // node here | 359 // node here |
359 if (lastTable.parent != null) { | 360 if (lastTable.parent != null) { |
360 fosterParent = lastTable.parent; | 361 fosterParent = lastTable.parent; |
361 insertBefore = lastTable; | 362 insertBefore = lastTable; |
362 } else { | 363 } else { |
363 fosterParent = openElements[openElements.indexOf(lastTable) - 1]; | 364 fosterParent = openElements[openElements.indexOf(lastTable) - 1]; |
364 } | 365 } |
365 } else { | 366 } else { |
366 fosterParent = openElements[0]; | 367 fosterParent = openElements[0]; |
367 } | 368 } |
368 return [fosterParent, insertBefore]; | 369 return [fosterParent, insertBefore]; |
369 } | 370 } |
370 | 371 |
371 void generateImpliedEndTags([String exclude]) { | 372 void generateImpliedEndTags([String exclude]) { |
372 var name = openElements.last.tagName; | 373 var name = openElements.last.localName; |
373 // XXX td, th and tr are not actually needed | 374 // XXX td, th and tr are not actually needed |
374 if (name != exclude && const ["dd", "dt", "li", "option", "optgroup", "p", | 375 if (name != exclude && const ["dd", "dt", "li", "option", "optgroup", "p", |
375 "rp", "rt"].contains(name)) { | 376 "rp", "rt"].contains(name)) { |
376 openElements.removeLast(); | 377 openElements.removeLast(); |
377 // XXX This is not entirely what the specification says. We should | 378 // XXX This is not entirely what the specification says. We should |
378 // investigate it more closely. | 379 // investigate it more closely. |
379 generateImpliedEndTags(exclude); | 380 generateImpliedEndTags(exclude); |
380 } | 381 } |
381 } | 382 } |
382 | 383 |
383 /// Return the final tree. | 384 /// Return the final tree. |
384 Document getDocument() => document; | 385 Document getDocument() => document; |
385 | 386 |
386 /// Return the final fragment. | 387 /// Return the final fragment. |
387 DocumentFragment getFragment() { | 388 DocumentFragment getFragment() { |
388 //XXX assert innerHTML | 389 //XXX assert innerHTML |
389 var fragment = new DocumentFragment(); | 390 var fragment = new DocumentFragment(); |
390 openElements[0].reparentChildren(fragment); | 391 openElements[0].reparentChildren(fragment); |
391 return fragment; | 392 return fragment; |
392 } | 393 } |
393 } | 394 } |
OLD | NEW |