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