1'use strict'; 2 3const HTML = require('../common/html'); 4 5//Aliases 6const $ = HTML.TAG_NAMES; 7const 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 === $.RB || tn === $.RP || tn === $.RT || tn === $.DD || tn === $.DT || tn === $.LI; 20 21 case 3: 22 return tn === $.RTC; 23 24 case 6: 25 return tn === $.OPTION; 26 27 case 8: 28 return tn === $.OPTGROUP; 29 } 30 31 return false; 32} 33 34function isImpliedEndTagRequiredThoroughly(tn) { 35 switch (tn.length) { 36 case 1: 37 return tn === $.P; 38 39 case 2: 40 return ( 41 tn === $.RB || 42 tn === $.RP || 43 tn === $.RT || 44 tn === $.DD || 45 tn === $.DT || 46 tn === $.LI || 47 tn === $.TD || 48 tn === $.TH || 49 tn === $.TR 50 ); 51 52 case 3: 53 return tn === $.RTC; 54 55 case 5: 56 return tn === $.TBODY || tn === $.TFOOT || tn === $.THEAD; 57 58 case 6: 59 return tn === $.OPTION; 60 61 case 7: 62 return tn === $.CAPTION; 63 64 case 8: 65 return tn === $.OPTGROUP || tn === $.COLGROUP; 66 } 67 68 return false; 69} 70 71function isScopingElement(tn, ns) { 72 switch (tn.length) { 73 case 2: 74 if (tn === $.TD || tn === $.TH) { 75 return ns === NS.HTML; 76 } else if (tn === $.MI || tn === $.MO || tn === $.MN || tn === $.MS) { 77 return ns === NS.MATHML; 78 } 79 80 break; 81 82 case 4: 83 if (tn === $.HTML) { 84 return ns === NS.HTML; 85 } else if (tn === $.DESC) { 86 return ns === NS.SVG; 87 } 88 89 break; 90 91 case 5: 92 if (tn === $.TABLE) { 93 return ns === NS.HTML; 94 } else if (tn === $.MTEXT) { 95 return ns === NS.MATHML; 96 } else if (tn === $.TITLE) { 97 return ns === NS.SVG; 98 } 99 100 break; 101 102 case 6: 103 return (tn === $.APPLET || tn === $.OBJECT) && ns === NS.HTML; 104 105 case 7: 106 return (tn === $.CAPTION || tn === $.MARQUEE) && ns === NS.HTML; 107 108 case 8: 109 return tn === $.TEMPLATE && ns === NS.HTML; 110 111 case 13: 112 return tn === $.FOREIGN_OBJECT && ns === NS.SVG; 113 114 case 14: 115 return tn === $.ANNOTATION_XML && ns === NS.MATHML; 116 } 117 118 return false; 119} 120 121//Stack of open elements 122class OpenElementStack { 123 constructor(document, treeAdapter) { 124 this.stackTop = -1; 125 this.items = []; 126 this.current = document; 127 this.currentTagName = null; 128 this.currentTmplContent = null; 129 this.tmplCount = 0; 130 this.treeAdapter = treeAdapter; 131 } 132 133 //Index of element 134 _indexOf(element) { 135 let idx = -1; 136 137 for (let i = this.stackTop; i >= 0; i--) { 138 if (this.items[i] === element) { 139 idx = i; 140 break; 141 } 142 } 143 return idx; 144 } 145 146 //Update current element 147 _isInTemplate() { 148 return this.currentTagName === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML; 149 } 150 151 _updateCurrentElement() { 152 this.current = this.items[this.stackTop]; 153 this.currentTagName = this.current && this.treeAdapter.getTagName(this.current); 154 155 this.currentTmplContent = this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : null; 156 } 157 158 //Mutations 159 push(element) { 160 this.items[++this.stackTop] = element; 161 this._updateCurrentElement(); 162 163 if (this._isInTemplate()) { 164 this.tmplCount++; 165 } 166 } 167 168 pop() { 169 this.stackTop--; 170 171 if (this.tmplCount > 0 && this._isInTemplate()) { 172 this.tmplCount--; 173 } 174 175 this._updateCurrentElement(); 176 } 177 178 replace(oldElement, newElement) { 179 const idx = this._indexOf(oldElement); 180 181 this.items[idx] = newElement; 182 183 if (idx === this.stackTop) { 184 this._updateCurrentElement(); 185 } 186 } 187 188 insertAfter(referenceElement, newElement) { 189 const insertionIdx = this._indexOf(referenceElement) + 1; 190 191 this.items.splice(insertionIdx, 0, newElement); 192 193 if (insertionIdx === ++this.stackTop) { 194 this._updateCurrentElement(); 195 } 196 } 197 198 popUntilTagNamePopped(tagName) { 199 while (this.stackTop > -1) { 200 const tn = this.currentTagName; 201 const ns = this.treeAdapter.getNamespaceURI(this.current); 202 203 this.pop(); 204 205 if (tn === tagName && ns === NS.HTML) { 206 break; 207 } 208 } 209 } 210 211 popUntilElementPopped(element) { 212 while (this.stackTop > -1) { 213 const poppedElement = this.current; 214 215 this.pop(); 216 217 if (poppedElement === element) { 218 break; 219 } 220 } 221 } 222 223 popUntilNumberedHeaderPopped() { 224 while (this.stackTop > -1) { 225 const tn = this.currentTagName; 226 const ns = this.treeAdapter.getNamespaceURI(this.current); 227 228 this.pop(); 229 230 if ( 231 tn === $.H1 || 232 tn === $.H2 || 233 tn === $.H3 || 234 tn === $.H4 || 235 tn === $.H5 || 236 (tn === $.H6 && ns === NS.HTML) 237 ) { 238 break; 239 } 240 } 241 } 242 243 popUntilTableCellPopped() { 244 while (this.stackTop > -1) { 245 const tn = this.currentTagName; 246 const ns = this.treeAdapter.getNamespaceURI(this.current); 247 248 this.pop(); 249 250 if (tn === $.TD || (tn === $.TH && ns === NS.HTML)) { 251 break; 252 } 253 } 254 } 255 256 popAllUpToHtmlElement() { 257 //NOTE: here we assume that root <html> element is always first in the open element stack, so 258 //we perform this fast stack clean up. 259 this.stackTop = 0; 260 this._updateCurrentElement(); 261 } 262 263 clearBackToTableContext() { 264 while ( 265 (this.currentTagName !== $.TABLE && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) || 266 this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML 267 ) { 268 this.pop(); 269 } 270 } 271 272 clearBackToTableBodyContext() { 273 while ( 274 (this.currentTagName !== $.TBODY && 275 this.currentTagName !== $.TFOOT && 276 this.currentTagName !== $.THEAD && 277 this.currentTagName !== $.TEMPLATE && 278 this.currentTagName !== $.HTML) || 279 this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML 280 ) { 281 this.pop(); 282 } 283 } 284 285 clearBackToTableRowContext() { 286 while ( 287 (this.currentTagName !== $.TR && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) || 288 this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML 289 ) { 290 this.pop(); 291 } 292 } 293 294 remove(element) { 295 for (let i = this.stackTop; i >= 0; i--) { 296 if (this.items[i] === element) { 297 this.items.splice(i, 1); 298 this.stackTop--; 299 this._updateCurrentElement(); 300 break; 301 } 302 } 303 } 304 305 //Search 306 tryPeekProperlyNestedBodyElement() { 307 //Properly nested <body> element (should be second element in stack). 308 const element = this.items[1]; 309 310 return element && this.treeAdapter.getTagName(element) === $.BODY ? element : null; 311 } 312 313 contains(element) { 314 return this._indexOf(element) > -1; 315 } 316 317 getCommonAncestor(element) { 318 let elementIdx = this._indexOf(element); 319 320 return --elementIdx >= 0 ? this.items[elementIdx] : null; 321 } 322 323 isRootHtmlElementCurrent() { 324 return this.stackTop === 0 && this.currentTagName === $.HTML; 325 } 326 327 //Element in scope 328 hasInScope(tagName) { 329 for (let i = this.stackTop; i >= 0; i--) { 330 const tn = this.treeAdapter.getTagName(this.items[i]); 331 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 332 333 if (tn === tagName && ns === NS.HTML) { 334 return true; 335 } 336 337 if (isScopingElement(tn, ns)) { 338 return false; 339 } 340 } 341 342 return true; 343 } 344 345 hasNumberedHeaderInScope() { 346 for (let i = this.stackTop; i >= 0; i--) { 347 const tn = this.treeAdapter.getTagName(this.items[i]); 348 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 349 350 if ( 351 (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6) && 352 ns === NS.HTML 353 ) { 354 return true; 355 } 356 357 if (isScopingElement(tn, ns)) { 358 return false; 359 } 360 } 361 362 return true; 363 } 364 365 hasInListItemScope(tagName) { 366 for (let i = this.stackTop; i >= 0; i--) { 367 const tn = this.treeAdapter.getTagName(this.items[i]); 368 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 369 370 if (tn === tagName && ns === NS.HTML) { 371 return true; 372 } 373 374 if (((tn === $.UL || tn === $.OL) && ns === NS.HTML) || isScopingElement(tn, ns)) { 375 return false; 376 } 377 } 378 379 return true; 380 } 381 382 hasInButtonScope(tagName) { 383 for (let i = this.stackTop; i >= 0; i--) { 384 const tn = this.treeAdapter.getTagName(this.items[i]); 385 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 386 387 if (tn === tagName && ns === NS.HTML) { 388 return true; 389 } 390 391 if ((tn === $.BUTTON && ns === NS.HTML) || isScopingElement(tn, ns)) { 392 return false; 393 } 394 } 395 396 return true; 397 } 398 399 hasInTableScope(tagName) { 400 for (let i = this.stackTop; i >= 0; i--) { 401 const tn = this.treeAdapter.getTagName(this.items[i]); 402 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 403 404 if (ns !== NS.HTML) { 405 continue; 406 } 407 408 if (tn === tagName) { 409 return true; 410 } 411 412 if (tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML) { 413 return false; 414 } 415 } 416 417 return true; 418 } 419 420 hasTableBodyContextInTableScope() { 421 for (let i = this.stackTop; i >= 0; i--) { 422 const tn = this.treeAdapter.getTagName(this.items[i]); 423 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 424 425 if (ns !== NS.HTML) { 426 continue; 427 } 428 429 if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT) { 430 return true; 431 } 432 433 if (tn === $.TABLE || tn === $.HTML) { 434 return false; 435 } 436 } 437 438 return true; 439 } 440 441 hasInSelectScope(tagName) { 442 for (let i = this.stackTop; i >= 0; i--) { 443 const tn = this.treeAdapter.getTagName(this.items[i]); 444 const ns = this.treeAdapter.getNamespaceURI(this.items[i]); 445 446 if (ns !== NS.HTML) { 447 continue; 448 } 449 450 if (tn === tagName) { 451 return true; 452 } 453 454 if (tn !== $.OPTION && tn !== $.OPTGROUP) { 455 return false; 456 } 457 } 458 459 return true; 460 } 461 462 //Implied end tags 463 generateImpliedEndTags() { 464 while (isImpliedEndTagRequired(this.currentTagName)) { 465 this.pop(); 466 } 467 } 468 469 generateImpliedEndTagsThoroughly() { 470 while (isImpliedEndTagRequiredThoroughly(this.currentTagName)) { 471 this.pop(); 472 } 473 } 474 475 generateImpliedEndTagsWithExclusion(exclusionTagName) { 476 while (isImpliedEndTagRequired(this.currentTagName) && this.currentTagName !== exclusionTagName) { 477 this.pop(); 478 } 479 } 480} 481 482module.exports = OpenElementStack; 483