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(); } 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(targetIdx < 0 ? 0 : targetIdx); } 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(idx < 0 ? 0 : idx); } popUntilPopped(tagNames, targetNS) { const idx = this._indexOfTagNames(tagNames, targetNS); this.shortenToLength(idx < 0 ? 0 : idx); } popUntilNumberedHeaderPopped() { this.popUntilPopped(NUMBERED_HEADERS, NS.HTML); } popUntilTableCellPopped() { this.popUntilPopped(TABLE_CELLS, NS.HTML); } popAllUpToHtmlElement() { //NOTE: here we assume that the root 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 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 (IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId)) { this.pop(); } } generateImpliedEndTagsThoroughly() { while (IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) { this.pop(); } } generateImpliedEndTagsWithExclusion(exclusionId) { while (this.currentTagId !== exclusionId && IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) { this.pop(); } } }