1'use strict'; 2 3var HTML = require('../common/html'); 4 5//Aliases 6var $ = HTML.TAG_NAMES, 7 NS = HTML.NAMESPACES; 8 9//Element utils 10 11//OPTIMIZATION: Integer comparisons are low-cost, so we can use very fast tag name length filters here. 12//It's faster than using dictionary. 13function isImpliedEndTagRequired(tn) { 14 switch (tn.length) { 15 case 1: 16 return tn === $.P; 17 18 case 2: 19 return tn === $.RP || tn === $.RT || tn === $.DD || tn === $.DT || tn === $.LI; 20 21 case 6: 22 return tn === $.OPTION; 23 24 case 8: 25 return tn === $.OPTGROUP; 26 } 27 28 return false; 29} 30 31function isScopingElement(tn, ns) { 32 switch (tn.length) { 33 case 2: 34 if (tn === $.TD || tn === $.TH) 35 return ns === NS.HTML; 36 37 else if (tn === $.MI || tn === $.MO || tn == $.MN || tn === $.MS) 38 return ns === NS.MATHML; 39 40 break; 41 42 case 4: 43 if (tn === $.HTML) 44 return ns === NS.HTML; 45 46 else if (tn === $.DESC) 47 return ns === NS.SVG; 48 49 break; 50 51 case 5: 52 if (tn === $.TABLE) 53 return ns === NS.HTML; 54 55 else if (tn === $.MTEXT) 56 return ns === NS.MATHML; 57 58 else if (tn === $.TITLE) 59 return ns === NS.SVG; 60 61 break; 62 63 case 6: 64 return (tn === $.APPLET || tn === $.OBJECT) && ns === NS.HTML; 65 66 case 7: 67 return (tn === $.CAPTION || tn === $.MARQUEE) && ns === NS.HTML; 68 69 case 8: 70 return tn === $.TEMPLATE && ns === NS.HTML; 71 72 case 13: 73 return tn === $.FOREIGN_OBJECT && ns === NS.SVG; 74 75 case 14: 76 return tn === $.ANNOTATION_XML && ns === NS.MATHML; 77 } 78 79 return false; 80} 81 82//Stack of open elements 83var OpenElementStack = module.exports = function (document, treeAdapter) { 84 this.stackTop = -1; 85 this.items = []; 86 this.current = document; 87 this.currentTagName = null; 88 this.currentTmplContent = null; 89 this.tmplCount = 0; 90 this.treeAdapter = treeAdapter; 91}; 92 93//Index of element 94OpenElementStack.prototype._indexOf = function (element) { 95 var idx = -1; 96 97 for (var i = this.stackTop; i >= 0; i--) { 98 if (this.items[i] === element) { 99 idx = i; 100 break; 101 } 102 } 103 return idx; 104}; 105 106//Update current element 107OpenElementStack.prototype._isInTemplate = function () { 108 if (this.currentTagName !== $.TEMPLATE) 109 return false; 110 111 return this.treeAdapter.getNamespaceURI(this.current) === NS.HTML; 112}; 113 114OpenElementStack.prototype._updateCurrentElement = function () { 115 this.current = this.items[this.stackTop]; 116 this.currentTagName = this.current && this.treeAdapter.getTagName(this.current); 117 118 this.currentTmplContent = this._isInTemplate() ? this.treeAdapter.getChildNodes(this.current)[0] : null; 119}; 120 121//Mutations 122OpenElementStack.prototype.push = function (element) { 123 this.items[++this.stackTop] = element; 124 this._updateCurrentElement(); 125 126 if (this._isInTemplate()) 127 this.tmplCount++; 128 129}; 130 131OpenElementStack.prototype.pop = function () { 132 this.stackTop--; 133 134 if (this.tmplCount > 0 && this._isInTemplate()) 135 this.tmplCount--; 136 137 this._updateCurrentElement(); 138}; 139 140OpenElementStack.prototype.replace = function (oldElement, newElement) { 141 var idx = this._indexOf(oldElement); 142 this.items[idx] = newElement; 143 144 if (idx === this.stackTop) 145 this._updateCurrentElement(); 146}; 147 148OpenElementStack.prototype.insertAfter = function (referenceElement, newElement) { 149 var insertionIdx = this._indexOf(referenceElement) + 1; 150 151 this.items.splice(insertionIdx, 0, newElement); 152 153 if (insertionIdx == ++this.stackTop) 154 this._updateCurrentElement(); 155}; 156 157OpenElementStack.prototype.popUntilTagNamePopped = function (tagName) { 158 while (this.stackTop > -1) { 159 var tn = this.currentTagName; 160 161 this.pop(); 162 163 if (tn === tagName) 164 break; 165 } 166}; 167 168OpenElementStack.prototype.popUntilTemplatePopped = function () { 169 while (this.stackTop > -1) { 170 var tn = this.currentTagName, 171 ns = this.treeAdapter.getNamespaceURI(this.current); 172 173 this.pop(); 174 175 if (tn === $.TEMPLATE && ns === NS.HTML) 176 break; 177 } 178}; 179 180OpenElementStack.prototype.popUntilElementPopped = function (element) { 181 while (this.stackTop > -1) { 182 var poppedElement = this.current; 183 184 this.pop(); 185 186 if (poppedElement === element) 187 break; 188 } 189}; 190 191OpenElementStack.prototype.popUntilNumberedHeaderPopped = function () { 192 while (this.stackTop > -1) { 193 var tn = this.currentTagName; 194 195 this.pop(); 196 197 if (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6) 198 break; 199 } 200}; 201 202OpenElementStack.prototype.popAllUpToHtmlElement = function () { 203 //NOTE: here we assume that root <html> element is always first in the open element stack, so 204 //we perform this fast stack clean up. 205 this.stackTop = 0; 206 this._updateCurrentElement(); 207}; 208 209OpenElementStack.prototype.clearBackToTableContext = function () { 210 while (this.currentTagName !== $.TABLE && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) 211 this.pop(); 212}; 213 214OpenElementStack.prototype.clearBackToTableBodyContext = function () { 215 while (this.currentTagName !== $.TBODY && this.currentTagName !== $.TFOOT && 216 this.currentTagName !== $.THEAD && this.currentTagName !== $.TEMPLATE && 217 this.currentTagName !== $.HTML) { 218 this.pop(); 219 } 220}; 221 222OpenElementStack.prototype.clearBackToTableRowContext = function () { 223 while (this.currentTagName !== $.TR && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) 224 this.pop(); 225}; 226 227OpenElementStack.prototype.remove = function (element) { 228 for (var i = this.stackTop; i >= 0; i--) { 229 if (this.items[i] === element) { 230 this.items.splice(i, 1); 231 this.stackTop--; 232 this._updateCurrentElement(); 233 break; 234 } 235 } 236}; 237 238//Search 239OpenElementStack.prototype.tryPeekProperlyNestedBodyElement = function () { 240 //Properly nested <body> element (should be second element in stack). 241 var element = this.items[1]; 242 return element && this.treeAdapter.getTagName(element) === $.BODY ? element : null; 243}; 244 245OpenElementStack.prototype.contains = function (element) { 246 return this._indexOf(element) > -1; 247}; 248 249OpenElementStack.prototype.getCommonAncestor = function (element) { 250 var elementIdx = this._indexOf(element); 251 252 return --elementIdx >= 0 ? this.items[elementIdx] : null; 253}; 254 255OpenElementStack.prototype.isRootHtmlElementCurrent = function () { 256 return this.stackTop === 0 && this.currentTagName === $.HTML; 257}; 258 259//Element in scope 260OpenElementStack.prototype.hasInScope = function (tagName) { 261 for (var i = this.stackTop; i >= 0; i--) { 262 var tn = this.treeAdapter.getTagName(this.items[i]); 263 264 if (tn === tagName) 265 return true; 266 267 var ns = this.treeAdapter.getNamespaceURI(this.items[i]); 268 269 if (isScopingElement(tn, ns)) 270 return false; 271 } 272 273 return true; 274}; 275 276OpenElementStack.prototype.hasNumberedHeaderInScope = function () { 277 for (var i = this.stackTop; i >= 0; i--) { 278 var tn = this.treeAdapter.getTagName(this.items[i]); 279 280 if (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6) 281 return true; 282 283 if (isScopingElement(tn, this.treeAdapter.getNamespaceURI(this.items[i]))) 284 return false; 285 } 286 287 return true; 288}; 289 290OpenElementStack.prototype.hasInListItemScope = function (tagName) { 291 for (var i = this.stackTop; i >= 0; i--) { 292 var tn = this.treeAdapter.getTagName(this.items[i]); 293 294 if (tn === tagName) 295 return true; 296 297 var ns = this.treeAdapter.getNamespaceURI(this.items[i]); 298 299 if (((tn === $.UL || tn === $.OL) && ns === NS.HTML) || isScopingElement(tn, ns)) 300 return false; 301 } 302 303 return true; 304}; 305 306OpenElementStack.prototype.hasInButtonScope = function (tagName) { 307 for (var i = this.stackTop; i >= 0; i--) { 308 var tn = this.treeAdapter.getTagName(this.items[i]); 309 310 if (tn === tagName) 311 return true; 312 313 var ns = this.treeAdapter.getNamespaceURI(this.items[i]); 314 315 if ((tn === $.BUTTON && ns === NS.HTML) || isScopingElement(tn, ns)) 316 return false; 317 } 318 319 return true; 320}; 321 322OpenElementStack.prototype.hasInTableScope = function (tagName) { 323 for (var i = this.stackTop; i >= 0; i--) { 324 var tn = this.treeAdapter.getTagName(this.items[i]); 325 326 if (tn === tagName) 327 return true; 328 329 var ns = this.treeAdapter.getNamespaceURI(this.items[i]); 330 331 if ((tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML) && ns === NS.HTML) 332 return false; 333 } 334 335 return true; 336}; 337 338OpenElementStack.prototype.hasTableBodyContextInTableScope = function () { 339 for (var i = this.stackTop; i >= 0; i--) { 340 var tn = this.treeAdapter.getTagName(this.items[i]); 341 342 if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT) 343 return true; 344 345 var ns = this.treeAdapter.getNamespaceURI(this.items[i]); 346 347 if ((tn === $.TABLE || tn === $.HTML) && ns === NS.HTML) 348 return false; 349 } 350 351 return true; 352}; 353 354OpenElementStack.prototype.hasInSelectScope = function (tagName) { 355 for (var i = this.stackTop; i >= 0; i--) { 356 var tn = this.treeAdapter.getTagName(this.items[i]); 357 358 if (tn === tagName) 359 return true; 360 361 var ns = this.treeAdapter.getNamespaceURI(this.items[i]); 362 363 if (tn !== $.OPTION && tn !== $.OPTGROUP && ns === NS.HTML) 364 return false; 365 } 366 367 return true; 368}; 369 370//Implied end tags 371OpenElementStack.prototype.generateImpliedEndTags = function () { 372 while (isImpliedEndTagRequired(this.currentTagName)) 373 this.pop(); 374}; 375 376OpenElementStack.prototype.generateImpliedEndTagsWithExclusion = function (exclusionTagName) { 377 while (isImpliedEndTagRequired(this.currentTagName) && this.currentTagName !== exclusionTagName) 378 this.pop(); 379}; 380