123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- 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 <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 (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();
- }
- }
- }
|