| OLD | NEW |
| (Empty) |
| 1 /// Internals to the tree builders. | |
| 2 library treebuilder; | |
| 3 | |
| 4 import 'dart:collection'; | |
| 5 import 'package:html5lib/dom.dart'; | |
| 6 import 'package:html5lib/parser.dart' show getElementNameTuple; | |
| 7 import 'package:source_span/source_span.dart'; | |
| 8 import 'constants.dart'; | |
| 9 import 'list_proxy.dart'; | |
| 10 import 'token.dart'; | |
| 11 import 'utils.dart'; | |
| 12 | |
| 13 // The scope markers are inserted when entering object elements, | |
| 14 // marquees, table cells, and table captions, and are used to prevent formatting | |
| 15 // from "leaking" into tables, object elements, and marquees. | |
| 16 const Node Marker = null; | |
| 17 | |
| 18 // TODO(jmesserly): this should extend ListBase<Element>, but my simple attempt | |
| 19 // didn't work. | |
| 20 class ActiveFormattingElements extends ListProxy<Element> { | |
| 21 ActiveFormattingElements() : super(); | |
| 22 | |
| 23 // Override the "add" method. | |
| 24 // TODO(jmesserly): I'd rather not override this; can we do this in the | |
| 25 // calling code instead? | |
| 26 void add(Element node) { | |
| 27 int equalCount = 0; | |
| 28 if (node != Marker) { | |
| 29 for (var element in reversed) { | |
| 30 if (element == Marker) { | |
| 31 break; | |
| 32 } | |
| 33 if (_nodesEqual(element, node)) { | |
| 34 equalCount += 1; | |
| 35 } | |
| 36 if (equalCount == 3) { | |
| 37 remove(element); | |
| 38 break; | |
| 39 } | |
| 40 } | |
| 41 } | |
| 42 super.add(node); | |
| 43 } | |
| 44 } | |
| 45 | |
| 46 // TODO(jmesserly): this should exist in corelib... | |
| 47 bool _mapEquals(Map a, Map b) { | |
| 48 if (a.length != b.length) return false; | |
| 49 if (a.length == 0) return true; | |
| 50 | |
| 51 for (var keyA in a.keys) { | |
| 52 var valB = b[keyA]; | |
| 53 if (valB == null && !b.containsKey(keyA)) { | |
| 54 return false; | |
| 55 } | |
| 56 | |
| 57 if (a[keyA] != valB) { | |
| 58 return false; | |
| 59 } | |
| 60 } | |
| 61 return true; | |
| 62 } | |
| 63 | |
| 64 | |
| 65 bool _nodesEqual(Element node1, Element node2) { | |
| 66 return getElementNameTuple(node1) == getElementNameTuple(node2) && | |
| 67 _mapEquals(node1.attributes, node2.attributes); | |
| 68 } | |
| 69 | |
| 70 /// Basic treebuilder implementation. | |
| 71 class TreeBuilder { | |
| 72 final String defaultNamespace; | |
| 73 | |
| 74 Document document; | |
| 75 | |
| 76 final List<Element> openElements = <Element>[]; | |
| 77 | |
| 78 final activeFormattingElements = new ActiveFormattingElements(); | |
| 79 | |
| 80 Node headPointer; | |
| 81 | |
| 82 Node formPointer; | |
| 83 | |
| 84 /// Switch the function used to insert an element from the | |
| 85 /// normal one to the misnested table one and back again | |
| 86 bool insertFromTable; | |
| 87 | |
| 88 TreeBuilder(bool namespaceHTMLElements) | |
| 89 : defaultNamespace = namespaceHTMLElements ? Namespaces.html : null { | |
| 90 reset(); | |
| 91 } | |
| 92 | |
| 93 void reset() { | |
| 94 openElements.clear(); | |
| 95 activeFormattingElements.clear(); | |
| 96 | |
| 97 //XXX - rename these to headElement, formElement | |
| 98 headPointer = null; | |
| 99 formPointer = null; | |
| 100 | |
| 101 insertFromTable = false; | |
| 102 | |
| 103 document = new Document(); | |
| 104 } | |
| 105 | |
| 106 bool elementInScope(target, {String variant}) { | |
| 107 //If we pass a node in we match that. if we pass a string | |
| 108 //match any node with that name | |
| 109 bool exactNode = target is Node; | |
| 110 | |
| 111 List listElements1 = scopingElements; | |
| 112 List listElements2 = const []; | |
| 113 bool invert = false; | |
| 114 if (variant != null) { | |
| 115 switch (variant) { | |
| 116 case "button": | |
| 117 listElements2 = const [const Pair(Namespaces.html, "button")]; | |
| 118 break; | |
| 119 case "list": | |
| 120 listElements2 = const [const Pair(Namespaces.html, "ol"), | |
| 121 const Pair(Namespaces.html, "ul")]; | |
| 122 break; | |
| 123 case "table": | |
| 124 listElements1 = const [const Pair(Namespaces.html, "html"), | |
| 125 const Pair(Namespaces.html, "table")]; | |
| 126 break; | |
| 127 case "select": | |
| 128 listElements1 = const [const Pair(Namespaces.html, "optgroup"), | |
| 129 const Pair(Namespaces.html, "option")]; | |
| 130 invert = true; | |
| 131 break; | |
| 132 default: | |
| 133 throw new StateError('We should never reach this point'); | |
| 134 } | |
| 135 } | |
| 136 | |
| 137 for (var node in openElements.reversed) { | |
| 138 if (!exactNode && node.localName == target || | |
| 139 exactNode && node == target) { | |
| 140 return true; | |
| 141 } else if (invert != | |
| 142 (listElements1.contains(getElementNameTuple(node)) || | |
| 143 listElements2.contains(getElementNameTuple(node)))) { | |
| 144 return false; | |
| 145 } | |
| 146 } | |
| 147 | |
| 148 throw new StateError('We should never reach this point'); | |
| 149 } | |
| 150 | |
| 151 void reconstructActiveFormattingElements() { | |
| 152 // Within this algorithm the order of steps described in the | |
| 153 // specification is not quite the same as the order of steps in the | |
| 154 // code. It should still do the same though. | |
| 155 | |
| 156 // Step 1: stop the algorithm when there's nothing to do. | |
| 157 if (activeFormattingElements.length == 0) { | |
| 158 return; | |
| 159 } | |
| 160 | |
| 161 // Step 2 and step 3: we start with the last element. So i is -1. | |
| 162 int i = activeFormattingElements.length - 1; | |
| 163 var entry = activeFormattingElements[i]; | |
| 164 if (entry == Marker || openElements.contains(entry)) { | |
| 165 return; | |
| 166 } | |
| 167 | |
| 168 // Step 6 | |
| 169 while (entry != Marker && !openElements.contains(entry)) { | |
| 170 if (i == 0) { | |
| 171 //This will be reset to 0 below | |
| 172 i = -1; | |
| 173 break; | |
| 174 } | |
| 175 i -= 1; | |
| 176 // Step 5: let entry be one earlier in the list. | |
| 177 entry = activeFormattingElements[i]; | |
| 178 } | |
| 179 | |
| 180 while (true) { | |
| 181 // Step 7 | |
| 182 i += 1; | |
| 183 | |
| 184 // Step 8 | |
| 185 entry = activeFormattingElements[i]; | |
| 186 | |
| 187 // TODO(jmesserly): optimize this. No need to create a token. | |
| 188 var cloneToken = new StartTagToken( | |
| 189 entry.localName, | |
| 190 namespace: entry.namespaceUri, | |
| 191 data: new LinkedHashMap.from(entry.attributes)) | |
| 192 ..span = entry.sourceSpan; | |
| 193 | |
| 194 // Step 9 | |
| 195 var element = insertElement(cloneToken); | |
| 196 | |
| 197 // Step 10 | |
| 198 activeFormattingElements[i] = element; | |
| 199 | |
| 200 // Step 11 | |
| 201 if (element == activeFormattingElements.last) { | |
| 202 break; | |
| 203 } | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 void clearActiveFormattingElements() { | |
| 208 var entry = activeFormattingElements.removeLast(); | |
| 209 while (activeFormattingElements.length > 0 && entry != Marker) { | |
| 210 entry = activeFormattingElements.removeLast(); | |
| 211 } | |
| 212 } | |
| 213 | |
| 214 /// Check if an element exists between the end of the active | |
| 215 /// formatting elements and the last marker. If it does, return it, else | |
| 216 /// return null. | |
| 217 Element elementInActiveFormattingElements(String name) { | |
| 218 for (var item in activeFormattingElements.reversed) { | |
| 219 // Check for Marker first because if it's a Marker it doesn't have a | |
| 220 // name attribute. | |
| 221 if (item == Marker) { | |
| 222 break; | |
| 223 } else if (item.localName == name) { | |
| 224 return item; | |
| 225 } | |
| 226 } | |
| 227 return null; | |
| 228 } | |
| 229 | |
| 230 void insertRoot(Token token) { | |
| 231 var element = createElement(token); | |
| 232 openElements.add(element); | |
| 233 document.nodes.add(element); | |
| 234 } | |
| 235 | |
| 236 void insertDoctype(DoctypeToken token) { | |
| 237 var doctype = new DocumentType(token.name, token.publicId, token.systemId) | |
| 238 ..sourceSpan = token.span; | |
| 239 document.nodes.add(doctype); | |
| 240 } | |
| 241 | |
| 242 void insertComment(StringToken token, [Node parent]) { | |
| 243 if (parent == null) { | |
| 244 parent = openElements.last; | |
| 245 } | |
| 246 parent.nodes.add(new Comment(token.data)..sourceSpan = token.span); | |
| 247 } | |
| 248 | |
| 249 /// Create an element but don't insert it anywhere | |
| 250 Element createElement(StartTagToken token) { | |
| 251 var name = token.name; | |
| 252 var namespace = token.namespace; | |
| 253 if (namespace == null) namespace = defaultNamespace; | |
| 254 var element = document.createElementNS(namespace, name) | |
| 255 ..attributes = token.data | |
| 256 ..sourceSpan = token.span; | |
| 257 return element; | |
| 258 } | |
| 259 | |
| 260 Element insertElement(StartTagToken token) { | |
| 261 if (insertFromTable) return insertElementTable(token); | |
| 262 return insertElementNormal(token); | |
| 263 } | |
| 264 | |
| 265 Element insertElementNormal(StartTagToken token) { | |
| 266 var name = token.name; | |
| 267 var namespace = token.namespace; | |
| 268 if (namespace == null) namespace = defaultNamespace; | |
| 269 var element = document.createElementNS(namespace, name) | |
| 270 ..attributes = token.data | |
| 271 ..sourceSpan = token.span; | |
| 272 openElements.last.nodes.add(element); | |
| 273 openElements.add(element); | |
| 274 return element; | |
| 275 } | |
| 276 | |
| 277 Element insertElementTable(token) { | |
| 278 /// Create an element and insert it into the tree | |
| 279 var element = createElement(token); | |
| 280 if (!tableInsertModeElements.contains(openElements.last.localName)) { | |
| 281 return insertElementNormal(token); | |
| 282 } else { | |
| 283 // We should be in the InTable mode. This means we want to do | |
| 284 // special magic element rearranging | |
| 285 var nodePos = getTableMisnestedNodePosition(); | |
| 286 if (nodePos[1] == null) { | |
| 287 // TODO(jmesserly): I don't think this is reachable. If insertFromTable | |
| 288 // is true, there will be a <table> element open, and it always has a | |
| 289 // parent pointer. | |
| 290 nodePos[0].nodes.add(element); | |
| 291 } else { | |
| 292 nodePos[0].insertBefore(element, nodePos[1]); | |
| 293 } | |
| 294 openElements.add(element); | |
| 295 } | |
| 296 return element; | |
| 297 } | |
| 298 | |
| 299 /// Insert text data. | |
| 300 void insertText(String data, FileSpan span) { | |
| 301 var parent = openElements.last; | |
| 302 | |
| 303 if (!insertFromTable || insertFromTable && | |
| 304 !tableInsertModeElements.contains(openElements.last.localName)) { | |
| 305 _insertText(parent, data, span); | |
| 306 } else { | |
| 307 // We should be in the InTable mode. This means we want to do | |
| 308 // special magic element rearranging | |
| 309 var nodePos = getTableMisnestedNodePosition(); | |
| 310 _insertText(nodePos[0], data, span, nodePos[1]); | |
| 311 } | |
| 312 } | |
| 313 | |
| 314 /// Insert [data] as text in the current node, positioned before the | |
| 315 /// start of node [refNode] or to the end of the node's text. | |
| 316 static void _insertText(Node parent, String data, FileSpan span, | |
| 317 [Element refNode]) { | |
| 318 var nodes = parent.nodes; | |
| 319 if (refNode == null) { | |
| 320 if (nodes.length > 0 && nodes.last is Text) { | |
| 321 Text last = nodes.last; | |
| 322 last.data = '${last.data}$data'; | |
| 323 | |
| 324 if (span != null) { | |
| 325 last.sourceSpan = span.file.span(last.sourceSpan.start.offset, | |
| 326 span.end.offset); | |
| 327 } | |
| 328 } else { | |
| 329 nodes.add(new Text(data)..sourceSpan = span); | |
| 330 } | |
| 331 } else { | |
| 332 int index = nodes.indexOf(refNode); | |
| 333 if (index > 0 && nodes[index - 1] is Text) { | |
| 334 Text last = nodes[index - 1]; | |
| 335 last.data = '${last.data}$data'; | |
| 336 } else { | |
| 337 nodes.insert(index, new Text(data)..sourceSpan = span); | |
| 338 } | |
| 339 } | |
| 340 } | |
| 341 | |
| 342 /// Get the foster parent element, and sibling to insert before | |
| 343 /// (or null) when inserting a misnested table node | |
| 344 List<Node> getTableMisnestedNodePosition() { | |
| 345 // The foster parent element is the one which comes before the most | |
| 346 // recently opened table element | |
| 347 // XXX - this is really inelegant | |
| 348 Node lastTable = null; | |
| 349 Node fosterParent = null; | |
| 350 var insertBefore = null; | |
| 351 for (var elm in openElements.reversed) { | |
| 352 if (elm.localName == "table") { | |
| 353 lastTable = elm; | |
| 354 break; | |
| 355 } | |
| 356 } | |
| 357 if (lastTable != null) { | |
| 358 // XXX - we should really check that this parent is actually a | |
| 359 // node here | |
| 360 if (lastTable.parentNode != null) { | |
| 361 fosterParent = lastTable.parentNode; | |
| 362 insertBefore = lastTable; | |
| 363 } else { | |
| 364 fosterParent = openElements[openElements.indexOf(lastTable) - 1]; | |
| 365 } | |
| 366 } else { | |
| 367 fosterParent = openElements[0]; | |
| 368 } | |
| 369 return [fosterParent, insertBefore]; | |
| 370 } | |
| 371 | |
| 372 void generateImpliedEndTags([String exclude]) { | |
| 373 var name = openElements.last.localName; | |
| 374 // XXX td, th and tr are not actually needed | |
| 375 if (name != exclude && const ["dd", "dt", "li", "option", "optgroup", "p", | |
| 376 "rp", "rt"].contains(name)) { | |
| 377 openElements.removeLast(); | |
| 378 // XXX This is not entirely what the specification says. We should | |
| 379 // investigate it more closely. | |
| 380 generateImpliedEndTags(exclude); | |
| 381 } | |
| 382 } | |
| 383 | |
| 384 /// Return the final tree. | |
| 385 Document getDocument() => document; | |
| 386 | |
| 387 /// Return the final fragment. | |
| 388 DocumentFragment getFragment() { | |
| 389 //XXX assert innerHTML | |
| 390 var fragment = new DocumentFragment(); | |
| 391 openElements[0].reparentChildren(fragment); | |
| 392 return fragment; | |
| 393 } | |
| 394 } | |
| OLD | NEW |