Spaces:
Paused
Paused
| 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(); | |
| } | |
| } | |
| } | |