| import { TAG_ID as $, NS, NUMBERED_HEADERS } from '../common/html.js'; |
| //Element utils |
| const IMPLICIT_END_TAG_REQUIRED = new Set([$.DD, $.DT, $.LI, $.OPTGROUP, $.OPTION, $.P, $.RB, $.RP, $.RT, $.RTC]); |
| const IMPLICIT_END_TAG_REQUIRED_THOROUGHLY = new Set([ |
| ...IMPLICIT_END_TAG_REQUIRED, |
| $.CAPTION, |
| $.COLGROUP, |
| $.TBODY, |
| $.TD, |
| $.TFOOT, |
| $.TH, |
| $.THEAD, |
| $.TR, |
| ]); |
| const SCOPING_ELEMENTS_HTML = new Set([ |
| $.APPLET, |
| $.CAPTION, |
| $.HTML, |
| $.MARQUEE, |
| $.OBJECT, |
| $.TABLE, |
| $.TD, |
| $.TEMPLATE, |
| $.TH, |
| ]); |
| const SCOPING_ELEMENTS_HTML_LIST = new Set([...SCOPING_ELEMENTS_HTML, $.OL, $.UL]); |
| const SCOPING_ELEMENTS_HTML_BUTTON = new Set([...SCOPING_ELEMENTS_HTML, $.BUTTON]); |
| const SCOPING_ELEMENTS_MATHML = new Set([$.ANNOTATION_XML, $.MI, $.MN, $.MO, $.MS, $.MTEXT]); |
| const SCOPING_ELEMENTS_SVG = new Set([$.DESC, $.FOREIGN_OBJECT, $.TITLE]); |
| const TABLE_ROW_CONTEXT = new Set([$.TR, $.TEMPLATE, $.HTML]); |
| const TABLE_BODY_CONTEXT = new Set([$.TBODY, $.TFOOT, $.THEAD, $.TEMPLATE, $.HTML]); |
| const TABLE_CONTEXT = new Set([$.TABLE, $.TEMPLATE, $.HTML]); |
| const TABLE_CELLS = new Set([$.TD, $.TH]); |
| //Stack of open elements |
| export class OpenElementStack { |
| get currentTmplContentOrNode() { |
| return this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : this.current; |
| } |
| constructor(document, treeAdapter, handler) { |
| this.treeAdapter = treeAdapter; |
| this.handler = handler; |
| this.items = []; |
| this.tagIDs = []; |
| this.stackTop = -1; |
| this.tmplCount = 0; |
| this.currentTagId = $.UNKNOWN; |
| this.current = document; |
| } |
| //Index of element |
| _indexOf(element) { |
| return this.items.lastIndexOf(element, this.stackTop); |
| } |
| //Update current element |
| _isInTemplate() { |
| return this.currentTagId === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML; |
| } |
| _updateCurrentElement() { |
| this.current = this.items[this.stackTop]; |
| this.currentTagId = this.tagIDs[this.stackTop]; |
| } |
| //Mutations |
| push(element, tagID) { |
| this.stackTop++; |
| this.items[this.stackTop] = element; |
| this.current = element; |
| this.tagIDs[this.stackTop] = tagID; |
| this.currentTagId = tagID; |
| if (this._isInTemplate()) { |
| this.tmplCount++; |
| } |
| this.handler.onItemPush(element, tagID, true); |
| } |
| pop() { |
| const popped = this.current; |
| if (this.tmplCount > 0 && this._isInTemplate()) { |
| this.tmplCount--; |
| } |
| this.stackTop--; |
| this._updateCurrentElement(); |
| this.handler.onItemPop(popped, true); |
| } |
| replace(oldElement, newElement) { |
| const idx = this._indexOf(oldElement); |
| this.items[idx] = newElement; |
| if (idx === this.stackTop) { |
| this.current = newElement; |
| } |
| } |
| insertAfter(referenceElement, newElement, newElementID) { |
| const insertionIdx = this._indexOf(referenceElement) + 1; |
| this.items.splice(insertionIdx, 0, newElement); |
| this.tagIDs.splice(insertionIdx, 0, newElementID); |
| this.stackTop++; |
| if (insertionIdx === this.stackTop) { |
| this._updateCurrentElement(); |
| } |
| if (this.current && this.currentTagId !== undefined) { |
| this.handler.onItemPush(this.current, this.currentTagId, insertionIdx === this.stackTop); |
| } |
| } |
| popUntilTagNamePopped(tagName) { |
| let targetIdx = this.stackTop + 1; |
| do { |
| targetIdx = this.tagIDs.lastIndexOf(tagName, targetIdx - 1); |
| } while (targetIdx > 0 && this.treeAdapter.getNamespaceURI(this.items[targetIdx]) !== NS.HTML); |
| this.shortenToLength(Math.max(targetIdx, 0)); |
| } |
| shortenToLength(idx) { |
| while (this.stackTop >= idx) { |
| const popped = this.current; |
| if (this.tmplCount > 0 && this._isInTemplate()) { |
| this.tmplCount -= 1; |
| } |
| this.stackTop--; |
| this._updateCurrentElement(); |
| this.handler.onItemPop(popped, this.stackTop < idx); |
| } |
| } |
| popUntilElementPopped(element) { |
| const idx = this._indexOf(element); |
| this.shortenToLength(Math.max(idx, 0)); |
| } |
| popUntilPopped(tagNames, targetNS) { |
| const idx = this._indexOfTagNames(tagNames, targetNS); |
| this.shortenToLength(Math.max(idx, 0)); |
| } |
| popUntilNumberedHeaderPopped() { |
| this.popUntilPopped(NUMBERED_HEADERS, NS.HTML); |
| } |
| popUntilTableCellPopped() { |
| this.popUntilPopped(TABLE_CELLS, NS.HTML); |
| } |
| popAllUpToHtmlElement() { |
| //NOTE: here we assume that the root <html> element is always first in the open element stack, so |
| //we perform this fast stack clean up. |
| this.tmplCount = 0; |
| this.shortenToLength(1); |
| } |
| _indexOfTagNames(tagNames, namespace) { |
| for (let i = this.stackTop; i >= 0; i--) { |
| if (tagNames.has(this.tagIDs[i]) && this.treeAdapter.getNamespaceURI(this.items[i]) === namespace) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| clearBackTo(tagNames, targetNS) { |
| const idx = this._indexOfTagNames(tagNames, targetNS); |
| this.shortenToLength(idx + 1); |
| } |
| clearBackToTableContext() { |
| this.clearBackTo(TABLE_CONTEXT, NS.HTML); |
| } |
| clearBackToTableBodyContext() { |
| this.clearBackTo(TABLE_BODY_CONTEXT, NS.HTML); |
| } |
| clearBackToTableRowContext() { |
| this.clearBackTo(TABLE_ROW_CONTEXT, NS.HTML); |
| } |
| remove(element) { |
| const idx = this._indexOf(element); |
| if (idx >= 0) { |
| if (idx === this.stackTop) { |
| this.pop(); |
| } |
| else { |
| this.items.splice(idx, 1); |
| this.tagIDs.splice(idx, 1); |
| this.stackTop--; |
| this._updateCurrentElement(); |
| this.handler.onItemPop(element, false); |
| } |
| } |
| } |
| //Search |
| tryPeekProperlyNestedBodyElement() { |
| //Properly nested <body> element (should be second element in stack). |
| return this.stackTop >= 1 && this.tagIDs[1] === $.BODY ? this.items[1] : null; |
| } |
| contains(element) { |
| return this._indexOf(element) > -1; |
| } |
| getCommonAncestor(element) { |
| const elementIdx = this._indexOf(element) - 1; |
| return elementIdx >= 0 ? this.items[elementIdx] : null; |
| } |
| isRootHtmlElementCurrent() { |
| return this.stackTop === 0 && this.tagIDs[0] === $.HTML; |
| } |
| //Element in scope |
| hasInDynamicScope(tagName, htmlScope) { |
| for (let i = this.stackTop; i >= 0; i--) { |
| const tn = this.tagIDs[i]; |
| switch (this.treeAdapter.getNamespaceURI(this.items[i])) { |
| case NS.HTML: { |
| if (tn === tagName) |
| return true; |
| if (htmlScope.has(tn)) |
| return false; |
| break; |
| } |
| case NS.SVG: { |
| if (SCOPING_ELEMENTS_SVG.has(tn)) |
| return false; |
| break; |
| } |
| case NS.MATHML: { |
| if (SCOPING_ELEMENTS_MATHML.has(tn)) |
| return false; |
| break; |
| } |
| } |
| } |
| return true; |
| } |
| hasInScope(tagName) { |
| return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML); |
| } |
| hasInListItemScope(tagName) { |
| return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_LIST); |
| } |
| hasInButtonScope(tagName) { |
| return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_BUTTON); |
| } |
| hasNumberedHeaderInScope() { |
| for (let i = this.stackTop; i >= 0; i--) { |
| const tn = this.tagIDs[i]; |
| switch (this.treeAdapter.getNamespaceURI(this.items[i])) { |
| case NS.HTML: { |
| if (NUMBERED_HEADERS.has(tn)) |
| return true; |
| if (SCOPING_ELEMENTS_HTML.has(tn)) |
| return false; |
| break; |
| } |
| case NS.SVG: { |
| if (SCOPING_ELEMENTS_SVG.has(tn)) |
| return false; |
| break; |
| } |
| case NS.MATHML: { |
| if (SCOPING_ELEMENTS_MATHML.has(tn)) |
| return false; |
| break; |
| } |
| } |
| } |
| return true; |
| } |
| hasInTableScope(tagName) { |
| for (let i = this.stackTop; i >= 0; i--) { |
| if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) { |
| continue; |
| } |
| switch (this.tagIDs[i]) { |
| case tagName: { |
| return true; |
| } |
| case $.TABLE: |
| case $.HTML: { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| hasTableBodyContextInTableScope() { |
| for (let i = this.stackTop; i >= 0; i--) { |
| if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) { |
| continue; |
| } |
| switch (this.tagIDs[i]) { |
| case $.TBODY: |
| case $.THEAD: |
| case $.TFOOT: { |
| return true; |
| } |
| case $.TABLE: |
| case $.HTML: { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| hasInSelectScope(tagName) { |
| for (let i = this.stackTop; i >= 0; i--) { |
| if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) { |
| continue; |
| } |
| switch (this.tagIDs[i]) { |
| case tagName: { |
| return true; |
| } |
| case $.OPTION: |
| case $.OPTGROUP: { |
| break; |
| } |
| default: { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| //Implied end tags |
| generateImpliedEndTags() { |
| while (this.currentTagId !== undefined && IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId)) { |
| this.pop(); |
| } |
| } |
| generateImpliedEndTagsThoroughly() { |
| while (this.currentTagId !== undefined && IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) { |
| this.pop(); |
| } |
| } |
| generateImpliedEndTagsWithExclusion(exclusionId) { |
| while (this.currentTagId !== undefined && |
| this.currentTagId !== exclusionId && |
| IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) { |
| this.pop(); |
| } |
| } |
| } |