| 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 |