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