import { TAG_ID as $, NS, NUMBERED_HEADERS } from '../common/html.js';
import type { TreeAdapter, TreeAdapterTypeMap } from '../tree-adapters/interface.js';
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]);
export interface StackHandler<T extends TreeAdapterTypeMap> {
onItemPush: (node: T['parentNode'], tid: number, isTop: boolean) => void;
onItemPop: (node: T['parentNode'], isTop: boolean) => void;
}
export class OpenElementStack<T extends TreeAdapterTypeMap> {
items: T['parentNode'][] = [];
tagIDs: $[] = [];
current: T['parentNode'];
stackTop = -1;
tmplCount = 0;
currentTagId = $.UNKNOWN;
get currentTmplContentOrNode(): T['parentNode'] {
return this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : this.current;
}
constructor(
document: T['document'],
private treeAdapter: TreeAdapter<T>,
private handler: StackHandler<T>,
) {
this.current = document;
}
private _indexOf(element: T['element']): number {
return this.items.lastIndexOf(element, this.stackTop);
}
private _isInTemplate(): boolean {
return this.currentTagId === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
}
private _updateCurrentElement(): void {
this.current = this.items[this.stackTop];
this.currentTagId = this.tagIDs[this.stackTop];
}
push(element: T['element'], tagID: $): void {
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(): void {
const popped = this.current;
if (this.tmplCount > 0 && this._isInTemplate()) {
this.tmplCount--;
}
this.stackTop--;
this._updateCurrentElement();
this.handler.onItemPop(popped, true);
}
replace(oldElement: T['element'], newElement: T['element']): void {
const idx = this._indexOf(oldElement);
this.items[idx] = newElement;
if (idx === this.stackTop) {
this.current = newElement;
}
}
insertAfter(referenceElement: T['element'], newElement: T['element'], newElementID: $): void {
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: $): void {
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: number): void {
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: T['element']): void {
const idx = this._indexOf(element);
this.shortenToLength(idx < 0 ? 0 : idx);
}
private popUntilPopped(tagNames: Set<$>, targetNS: NS): void {
const idx = this._indexOfTagNames(tagNames, targetNS);
this.shortenToLength(idx < 0 ? 0 : idx);
}
popUntilNumberedHeaderPopped(): void {
this.popUntilPopped(NUMBERED_HEADERS, NS.HTML);
}
popUntilTableCellPopped(): void {
this.popUntilPopped(TABLE_CELLS, NS.HTML);
}
popAllUpToHtmlElement(): void {
this.tmplCount = 0;
this.shortenToLength(1);
}
private _indexOfTagNames(tagNames: Set<$>, namespace: NS): number {
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;
}
private clearBackTo(tagNames: Set<$>, targetNS: NS): void {
const idx = this._indexOfTagNames(tagNames, targetNS);
this.shortenToLength(idx + 1);
}
clearBackToTableContext(): void {
this.clearBackTo(TABLE_CONTEXT, NS.HTML);
}
clearBackToTableBodyContext(): void {
this.clearBackTo(TABLE_BODY_CONTEXT, NS.HTML);
}
clearBackToTableRowContext(): void {
this.clearBackTo(TABLE_ROW_CONTEXT, NS.HTML);
}
remove(element: T['element']): void {
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);
}
}
}
tryPeekProperlyNestedBodyElement(): T['element'] | null {
return this.stackTop >= 1 && this.tagIDs[1] === $.BODY ? this.items[1] : null;
}
contains(element: T['element']): boolean {
return this._indexOf(element) > -1;
}
getCommonAncestor(element: T['element']): T['element'] | null {
const elementIdx = this._indexOf(element) - 1;
return elementIdx >= 0 ? this.items[elementIdx] : null;
}
isRootHtmlElementCurrent(): boolean {
return this.stackTop === 0 && this.tagIDs[0] === $.HTML;
}
private hasInDynamicScope(tagName: $, htmlScope: Set<$>): boolean {
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: $): boolean {
return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML);
}
hasInListItemScope(tagName: $): boolean {
return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_LIST);
}
hasInButtonScope(tagName: $): boolean {
return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_BUTTON);
}
hasNumberedHeaderInScope(): boolean {
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: $): boolean {
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(): boolean {
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: $): boolean {
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;
}
generateImpliedEndTags(): void {
while (IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId)) {
this.pop();
}
}
generateImpliedEndTagsThoroughly(): void {
while (IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
this.pop();
}
}
generateImpliedEndTagsWithExclusion(exclusionId: $): void {
while (this.currentTagId !== exclusionId && IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
this.pop();
}
}
}