• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import { Tokenizer, TokenizerMode, type TokenHandler } from '../tokenizer/index.js';
2import { OpenElementStack, type StackHandler } from './open-element-stack.js';
3import { FormattingElementList, EntryType, type ElementEntry } from './formatting-element-list.js';
4import { defaultTreeAdapter, type DefaultTreeAdapterMap } from '../tree-adapters/default.js';
5import * as doctype from '../common/doctype.js';
6import * as foreignContent from '../common/foreign-content.js';
7import { ERR, type ParserErrorHandler } from '../common/error-codes.js';
8import * as unicode from '../common/unicode.js';
9import {
10    TAG_ID as $,
11    TAG_NAMES as TN,
12    NS,
13    ATTRS,
14    SPECIAL_ELEMENTS,
15    DOCUMENT_MODE,
16    NUMBERED_HEADERS,
17    getTagID,
18} from '../common/html.js';
19import type { TreeAdapter, TreeAdapterTypeMap } from '../tree-adapters/interface.js';
20import {
21    TokenType,
22    getTokenAttr,
23    type Token,
24    type CommentToken,
25    type CharacterToken,
26    type TagToken,
27    type DoctypeToken,
28    type EOFToken,
29    type LocationWithAttributes,
30    type ElementLocation,
31} from '../common/token.js';
32
33//Misc constants
34const HIDDEN_INPUT_TYPE = 'hidden';
35
36//Adoption agency loops iteration count
37const AA_OUTER_LOOP_ITER = 8;
38const AA_INNER_LOOP_ITER = 3;
39
40//Insertion modes
41enum InsertionMode {
42    INITIAL,
43    BEFORE_HTML,
44    BEFORE_HEAD,
45    IN_HEAD,
46    IN_HEAD_NO_SCRIPT,
47    AFTER_HEAD,
48    IN_BODY,
49    TEXT,
50    IN_TABLE,
51    IN_TABLE_TEXT,
52    IN_CAPTION,
53    IN_COLUMN_GROUP,
54    IN_TABLE_BODY,
55    IN_ROW,
56    IN_CELL,
57    IN_SELECT,
58    IN_SELECT_IN_TABLE,
59    IN_TEMPLATE,
60    AFTER_BODY,
61    IN_FRAMESET,
62    AFTER_FRAMESET,
63    AFTER_AFTER_BODY,
64    AFTER_AFTER_FRAMESET,
65}
66
67const BASE_LOC = {
68    startLine: -1,
69    startCol: -1,
70    startOffset: -1,
71    endLine: -1,
72    endCol: -1,
73    endOffset: -1,
74};
75
76const TABLE_STRUCTURE_TAGS = new Set([$.TABLE, $.TBODY, $.TFOOT, $.THEAD, $.TR]);
77
78export interface ParserOptions<T extends TreeAdapterTypeMap> {
79    /**
80     * The [scripting flag](https://html.spec.whatwg.org/multipage/parsing.html#scripting-flag). If set
81     * to `true`, `noscript` element content will be parsed as text.
82     *
83     *  @default `true`
84     */
85    scriptingEnabled?: boolean;
86
87    /**
88     * Enables source code location information. When enabled, each node (except the root node)
89     * will have a `sourceCodeLocation` property. If the node is not an empty element, `sourceCodeLocation` will
90     * be a {@link ElementLocation} object, otherwise it will be {@link Location}.
91     * If the element was implicitly created by the parser (as part of
92     * [tree correction](https://html.spec.whatwg.org/multipage/syntax.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser)),
93     * its `sourceCodeLocation` property will be `undefined`.
94     *
95     * @default `false`
96     */
97    sourceCodeLocationInfo?: boolean;
98
99    /**
100     * Specifies the resulting tree format.
101     *
102     * @default `treeAdapters.default`
103     */
104    treeAdapter?: TreeAdapter<T>;
105
106    /**
107     * Callback for parse errors.
108     *
109     * @default `null`
110     */
111    onParseError?: ParserErrorHandler | null;
112}
113
114const defaultParserOptions: Required<ParserOptions<DefaultTreeAdapterMap>> = {
115    scriptingEnabled: true,
116    sourceCodeLocationInfo: false,
117    treeAdapter: defaultTreeAdapter,
118    onParseError: null,
119};
120
121//Parser
122export class Parser<T extends TreeAdapterTypeMap> implements TokenHandler, StackHandler<T> {
123    treeAdapter: TreeAdapter<T>;
124    /** @internal */
125    onParseError: ParserErrorHandler | null;
126    protected currentToken: Token | null = null;
127    public options: Required<ParserOptions<T>>;
128    public document: T['document'];
129
130    public constructor(
131        options?: ParserOptions<T>,
132        document?: T['document'],
133        /** @internal */
134        public fragmentContext: T['element'] | null = null,
135        /** @internal */
136        public scriptHandler: null | ((pendingScript: T['element']) => void) = null,
137    ) {
138        this.options = {
139            ...defaultParserOptions,
140            ...options,
141        } as Required<ParserOptions<T>>;
142
143        this.treeAdapter = this.options.treeAdapter;
144        this.onParseError = this.options.onParseError;
145
146        // Always enable location info if we report parse errors.
147        if (this.onParseError) {
148            this.options.sourceCodeLocationInfo = true;
149        }
150
151        this.document = document ?? this.treeAdapter.createDocument();
152
153        this.tokenizer = new Tokenizer(this.options, this);
154        this.activeFormattingElements = new FormattingElementList(this.treeAdapter);
155
156        this.fragmentContextID = fragmentContext ? getTagID(this.treeAdapter.getTagName(fragmentContext)) : $.UNKNOWN;
157        this._setContextModes(fragmentContext ?? this.document, this.fragmentContextID);
158
159        this.openElements = new OpenElementStack(this.document, this.treeAdapter, this);
160    }
161
162    // API
163    public static parse<T extends TreeAdapterTypeMap>(html: string, options?: ParserOptions<T>): T['document'] {
164        const parser = new this(options);
165
166        parser.tokenizer.write(html, true);
167
168        return parser.document;
169    }
170
171    public static getFragmentParser<T extends TreeAdapterTypeMap>(
172        fragmentContext?: T['parentNode'] | null,
173        options?: ParserOptions<T>,
174    ): Parser<T> {
175        const opts: Required<ParserOptions<T>> = {
176            ...defaultParserOptions,
177            ...options,
178        } as Required<ParserOptions<T>>;
179
180        //NOTE: use a <template> element as the fragment context if no context element was provided,
181        //so we will parse in a "forgiving" manner
182        fragmentContext ??= opts.treeAdapter.createElement(TN.TEMPLATE, NS.HTML, []);
183
184        //NOTE: create a fake element which will be used as the `document` for fragment parsing.
185        //This is important for jsdom, where a new `document` cannot be created. This led to
186        //fragment parsing messing with the main `document`.
187        const documentMock = opts.treeAdapter.createElement('documentmock', NS.HTML, []);
188
189        const parser = new this(opts, documentMock, fragmentContext);
190
191        if (parser.fragmentContextID === $.TEMPLATE) {
192            parser.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE);
193        }
194
195        parser._initTokenizerForFragmentParsing();
196        parser._insertFakeRootElement();
197        parser._resetInsertionMode();
198        parser._findFormInFragmentContext();
199
200        return parser;
201    }
202
203    public getFragment(): T['documentFragment'] {
204        const rootElement = this.treeAdapter.getFirstChild(this.document) as T['parentNode'];
205        const fragment = this.treeAdapter.createDocumentFragment();
206
207        this._adoptNodes(rootElement, fragment);
208
209        return fragment;
210    }
211
212    tokenizer: Tokenizer;
213
214    public stopped = false;
215    /** @internal */
216    insertionMode = InsertionMode.INITIAL;
217    /** @internal */
218    originalInsertionMode = InsertionMode.INITIAL;
219
220    /** @internal */
221    fragmentContextID: $;
222
223    /** @internal */
224    headElement: null | T['element'] = null;
225    /** @internal */
226    formElement: null | T['element'] = null;
227
228    /** @internal */
229    openElements: OpenElementStack<T>;
230    /** @internal */
231    activeFormattingElements: FormattingElementList<T>;
232    /** Indicates that the current node is not an element in the HTML namespace */
233    protected currentNotInHTML = false;
234
235    /**
236     * The template insertion mode stack is maintained from the left.
237     * Ie. the topmost element will always have index 0.
238     *
239     * @internal
240     */
241    tmplInsertionModeStack: InsertionMode[] = [];
242
243    /** @internal */
244    pendingCharacterTokens: CharacterToken[] = [];
245    /** @internal */
246    hasNonWhitespacePendingCharacterToken = false;
247
248    /** @internal */
249    framesetOk = true;
250    /** @internal */
251    skipNextNewLine = false;
252    /** @internal */
253    fosterParentingEnabled = false;
254
255    //Errors
256
257    /** @internal */
258    _err(token: Token, code: ERR, beforeToken?: boolean): void {
259        if (!this.onParseError) return;
260
261        const loc = token.location ?? BASE_LOC;
262        const err = {
263            code,
264            startLine: loc.startLine,
265            startCol: loc.startCol,
266            startOffset: loc.startOffset,
267            endLine: beforeToken ? loc.startLine : loc.endLine,
268            endCol: beforeToken ? loc.startCol : loc.endCol,
269            endOffset: beforeToken ? loc.startOffset : loc.endOffset,
270        };
271
272        this.onParseError(err);
273    }
274
275    //Stack events
276
277    /** @internal */
278    onItemPush(node: T['parentNode'], tid: number, isTop: boolean): void {
279        this.treeAdapter.onItemPush?.(node);
280        if (isTop && this.openElements.stackTop > 0) this._setContextModes(node, tid);
281    }
282
283    /** @internal */
284    onItemPop(node: T['parentNode'], isTop: boolean): void {
285        if (this.options.sourceCodeLocationInfo) {
286            this._setEndLocation(node, this.currentToken!);
287        }
288
289        this.treeAdapter.onItemPop?.(node, this.openElements.current);
290
291        if (isTop) {
292            let current;
293            let currentTagId;
294
295            if (this.openElements.stackTop === 0 && this.fragmentContext) {
296                current = this.fragmentContext;
297                currentTagId = this.fragmentContextID;
298            } else {
299                ({ current, currentTagId } = this.openElements);
300            }
301
302            this._setContextModes(current, currentTagId);
303        }
304    }
305
306    protected _setContextModes(current: T['parentNode'], tid: number): void {
307        const isHTML = current === this.document || this.treeAdapter.getNamespaceURI(current) === NS.HTML;
308
309        this.currentNotInHTML = !isHTML;
310        this.tokenizer.inForeignNode = !isHTML && !this._isIntegrationPoint(tid, current);
311    }
312
313    /** @protected */
314    _switchToTextParsing(
315        currentToken: TagToken,
316        nextTokenizerState: (typeof TokenizerMode)[keyof typeof TokenizerMode],
317    ): void {
318        this._insertElement(currentToken, NS.HTML);
319        this.tokenizer.state = nextTokenizerState;
320        this.originalInsertionMode = this.insertionMode;
321        this.insertionMode = InsertionMode.TEXT;
322    }
323
324    switchToPlaintextParsing(): void {
325        this.insertionMode = InsertionMode.TEXT;
326        this.originalInsertionMode = InsertionMode.IN_BODY;
327        this.tokenizer.state = TokenizerMode.PLAINTEXT;
328    }
329
330    //Fragment parsing
331
332    /** @protected */
333    _getAdjustedCurrentElement(): T['element'] {
334        return this.openElements.stackTop === 0 && this.fragmentContext
335            ? this.fragmentContext
336            : this.openElements.current;
337    }
338
339    /** @protected */
340    _findFormInFragmentContext(): void {
341        let node = this.fragmentContext;
342
343        while (node) {
344            if (this.treeAdapter.getTagName(node) === TN.FORM) {
345                this.formElement = node;
346                break;
347            }
348
349            node = this.treeAdapter.getParentNode(node);
350        }
351    }
352
353    protected _initTokenizerForFragmentParsing(): void {
354        if (!this.fragmentContext || this.treeAdapter.getNamespaceURI(this.fragmentContext) !== NS.HTML) {
355            return;
356        }
357
358        switch (this.fragmentContextID) {
359            case $.TITLE:
360            case $.TEXTAREA: {
361                this.tokenizer.state = TokenizerMode.RCDATA;
362                break;
363            }
364            case $.STYLE:
365            case $.XMP:
366            case $.IFRAME:
367            case $.NOEMBED:
368            case $.NOFRAMES:
369            case $.NOSCRIPT: {
370                this.tokenizer.state = TokenizerMode.RAWTEXT;
371                break;
372            }
373            case $.SCRIPT: {
374                this.tokenizer.state = TokenizerMode.SCRIPT_DATA;
375                break;
376            }
377            case $.PLAINTEXT: {
378                this.tokenizer.state = TokenizerMode.PLAINTEXT;
379                break;
380            }
381            default:
382            // Do nothing
383        }
384    }
385
386    //Tree mutation
387
388    /** @protected */
389    _setDocumentType(token: DoctypeToken): void {
390        const name = token.name || '';
391        const publicId = token.publicId || '';
392        const systemId = token.systemId || '';
393
394        this.treeAdapter.setDocumentType(this.document, name, publicId, systemId);
395
396        if (token.location) {
397            const documentChildren = this.treeAdapter.getChildNodes(this.document);
398            const docTypeNode = documentChildren.find((node) => this.treeAdapter.isDocumentTypeNode(node));
399
400            if (docTypeNode) {
401                this.treeAdapter.setNodeSourceCodeLocation(docTypeNode, token.location);
402            }
403        }
404    }
405
406    /** @protected */
407    _attachElementToTree(element: T['element'], location: LocationWithAttributes | null): void {
408        if (this.options.sourceCodeLocationInfo) {
409            const loc = location && {
410                ...location,
411                startTag: location,
412            };
413
414            this.treeAdapter.setNodeSourceCodeLocation(element, loc);
415        }
416
417        if (this._shouldFosterParentOnInsertion()) {
418            this._fosterParentElement(element);
419        } else {
420            const parent = this.openElements.currentTmplContentOrNode;
421
422            this.treeAdapter.appendChild(parent, element);
423        }
424    }
425
426    /**
427     * For self-closing tags. Add an element to the tree, but skip adding it
428     * to the stack.
429     */
430
431    /** @protected */
432    _appendElement(token: TagToken, namespaceURI: NS): void {
433        const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs);
434
435        this._attachElementToTree(element, token.location);
436    }
437
438    /** @protected */
439    _insertElement(token: TagToken, namespaceURI: NS): void {
440        const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs);
441
442        this._attachElementToTree(element, token.location);
443        this.openElements.push(element, token.tagID);
444    }
445
446    /** @protected */
447    _insertFakeElement(tagName: string, tagID: $): void {
448        const element = this.treeAdapter.createElement(tagName, NS.HTML, []);
449
450        this._attachElementToTree(element, null);
451        this.openElements.push(element, tagID);
452    }
453
454    /** @protected */
455    _insertTemplate(token: TagToken): void {
456        const tmpl = this.treeAdapter.createElement(token.tagName, NS.HTML, token.attrs);
457        const content = this.treeAdapter.createDocumentFragment();
458
459        this.treeAdapter.setTemplateContent(tmpl, content);
460        this._attachElementToTree(tmpl, token.location);
461        this.openElements.push(tmpl, token.tagID);
462        if (this.options.sourceCodeLocationInfo) this.treeAdapter.setNodeSourceCodeLocation(content, null);
463    }
464
465    /** @protected */
466    _insertFakeRootElement(): void {
467        const element = this.treeAdapter.createElement(TN.HTML, NS.HTML, []);
468        if (this.options.sourceCodeLocationInfo) this.treeAdapter.setNodeSourceCodeLocation(element, null);
469
470        this.treeAdapter.appendChild(this.openElements.current, element);
471        this.openElements.push(element, $.HTML);
472    }
473
474    /** @protected */
475    _appendCommentNode(token: CommentToken, parent: T['parentNode']): void {
476        const commentNode = this.treeAdapter.createCommentNode(token.data);
477
478        this.treeAdapter.appendChild(parent, commentNode);
479        if (this.options.sourceCodeLocationInfo) {
480            this.treeAdapter.setNodeSourceCodeLocation(commentNode, token.location);
481        }
482    }
483
484    /** @protected */
485    _insertCharacters(token: CharacterToken): void {
486        let parent;
487        let beforeElement;
488
489        if (this._shouldFosterParentOnInsertion()) {
490            ({ parent, beforeElement } = this._findFosterParentingLocation());
491
492            if (beforeElement) {
493                this.treeAdapter.insertTextBefore(parent, token.chars, beforeElement);
494            } else {
495                this.treeAdapter.insertText(parent, token.chars);
496            }
497        } else {
498            parent = this.openElements.currentTmplContentOrNode;
499
500            this.treeAdapter.insertText(parent, token.chars);
501        }
502
503        if (!token.location) return;
504
505        const siblings = this.treeAdapter.getChildNodes(parent);
506        const textNodeIdx = beforeElement ? siblings.lastIndexOf(beforeElement) : siblings.length;
507        const textNode = siblings[textNodeIdx - 1];
508
509        //NOTE: if we have a location assigned by another token, then just update the end position
510        const tnLoc = this.treeAdapter.getNodeSourceCodeLocation(textNode);
511
512        if (tnLoc) {
513            const { endLine, endCol, endOffset } = token.location;
514            this.treeAdapter.updateNodeSourceCodeLocation(textNode, { endLine, endCol, endOffset });
515        } else if (this.options.sourceCodeLocationInfo) {
516            this.treeAdapter.setNodeSourceCodeLocation(textNode, token.location);
517        }
518    }
519
520    /** @protected */
521    _adoptNodes(donor: T['parentNode'], recipient: T['parentNode']): void {
522        for (let child = this.treeAdapter.getFirstChild(donor); child; child = this.treeAdapter.getFirstChild(donor)) {
523            this.treeAdapter.detachNode(child);
524            this.treeAdapter.appendChild(recipient, child);
525        }
526    }
527
528    /** @protected */
529    _setEndLocation(element: T['element'], closingToken: Token): void {
530        if (this.treeAdapter.getNodeSourceCodeLocation(element) && closingToken.location) {
531            const ctLoc = closingToken.location;
532            const tn = this.treeAdapter.getTagName(element);
533
534            const endLoc: Partial<ElementLocation> =
535                // NOTE: For cases like <p> <p> </p> - First 'p' closes without a closing
536                // tag and for cases like <td> <p> </td> - 'p' closes without a closing tag.
537                closingToken.type === TokenType.END_TAG && tn === closingToken.tagName
538                    ? {
539                          endTag: { ...ctLoc },
540                          endLine: ctLoc.endLine,
541                          endCol: ctLoc.endCol,
542                          endOffset: ctLoc.endOffset,
543                      }
544                    : {
545                          endLine: ctLoc.startLine,
546                          endCol: ctLoc.startCol,
547                          endOffset: ctLoc.startOffset,
548                      };
549
550            this.treeAdapter.updateNodeSourceCodeLocation(element, endLoc);
551        }
552    }
553
554    //Token processing
555    protected shouldProcessStartTagTokenInForeignContent(token: TagToken): boolean {
556        // Check that neither current === document, or ns === NS.HTML
557        if (!this.currentNotInHTML) return false;
558
559        let current: T['parentNode'];
560        let currentTagId: number;
561
562        if (this.openElements.stackTop === 0 && this.fragmentContext) {
563            current = this.fragmentContext;
564            currentTagId = this.fragmentContextID;
565        } else {
566            ({ current, currentTagId } = this.openElements);
567        }
568
569        if (
570            token.tagID === $.SVG &&
571            this.treeAdapter.getTagName(current) === TN.ANNOTATION_XML &&
572            this.treeAdapter.getNamespaceURI(current) === NS.MATHML
573        ) {
574            return false;
575        }
576
577        return (
578            // Check that `current` is not an integration point for HTML or MathML elements.
579            this.tokenizer.inForeignNode ||
580            // If it _is_ an integration point, then we might have to check that it is not an HTML
581            // integration point.
582            ((token.tagID === $.MGLYPH || token.tagID === $.MALIGNMARK) &&
583                !this._isIntegrationPoint(currentTagId, current, NS.HTML))
584        );
585    }
586
587    /** @protected */
588    _processToken(token: Token): void {
589        switch (token.type) {
590            case TokenType.CHARACTER: {
591                this.onCharacter(token);
592                break;
593            }
594            case TokenType.NULL_CHARACTER: {
595                this.onNullCharacter(token);
596                break;
597            }
598            case TokenType.COMMENT: {
599                this.onComment(token);
600                break;
601            }
602            case TokenType.DOCTYPE: {
603                this.onDoctype(token);
604                break;
605            }
606            case TokenType.START_TAG: {
607                this._processStartTag(token);
608                break;
609            }
610            case TokenType.END_TAG: {
611                this.onEndTag(token);
612                break;
613            }
614            case TokenType.EOF: {
615                this.onEof(token);
616                break;
617            }
618            case TokenType.WHITESPACE_CHARACTER: {
619                this.onWhitespaceCharacter(token);
620                break;
621            }
622        }
623    }
624
625    //Integration points
626
627    /** @protected */
628    _isIntegrationPoint(tid: $, element: T['element'], foreignNS?: NS): boolean {
629        const ns = this.treeAdapter.getNamespaceURI(element);
630        const attrs = this.treeAdapter.getAttrList(element);
631
632        return foreignContent.isIntegrationPoint(tid, ns, attrs, foreignNS);
633    }
634
635    //Active formatting elements reconstruction
636
637    /** @protected */
638    _reconstructActiveFormattingElements(): void {
639        const listLength = this.activeFormattingElements.entries.length;
640
641        if (listLength) {
642            const endIndex = this.activeFormattingElements.entries.findIndex(
643                (entry) => entry.type === EntryType.Marker || this.openElements.contains(entry.element),
644            );
645
646            const unopenIdx = endIndex < 0 ? listLength - 1 : endIndex - 1;
647
648            for (let i = unopenIdx; i >= 0; i--) {
649                const entry = this.activeFormattingElements.entries[i] as ElementEntry<T>;
650                this._insertElement(entry.token, this.treeAdapter.getNamespaceURI(entry.element));
651                entry.element = this.openElements.current;
652            }
653        }
654    }
655
656    //Close elements
657
658    /** @protected */
659    _closeTableCell(): void {
660        this.openElements.generateImpliedEndTags();
661        this.openElements.popUntilTableCellPopped();
662        this.activeFormattingElements.clearToLastMarker();
663        this.insertionMode = InsertionMode.IN_ROW;
664    }
665
666    /** @protected */
667    _closePElement(): void {
668        this.openElements.generateImpliedEndTagsWithExclusion($.P);
669        this.openElements.popUntilTagNamePopped($.P);
670    }
671
672    //Insertion modes
673
674    /** @protected */
675    _resetInsertionMode(): void {
676        for (let i = this.openElements.stackTop; i >= 0; i--) {
677            //Insertion mode reset map
678            switch (i === 0 && this.fragmentContext ? this.fragmentContextID : this.openElements.tagIDs[i]) {
679                case $.TR: {
680                    this.insertionMode = InsertionMode.IN_ROW;
681                    return;
682                }
683                case $.TBODY:
684                case $.THEAD:
685                case $.TFOOT: {
686                    this.insertionMode = InsertionMode.IN_TABLE_BODY;
687                    return;
688                }
689                case $.CAPTION: {
690                    this.insertionMode = InsertionMode.IN_CAPTION;
691                    return;
692                }
693                case $.COLGROUP: {
694                    this.insertionMode = InsertionMode.IN_COLUMN_GROUP;
695                    return;
696                }
697                case $.TABLE: {
698                    this.insertionMode = InsertionMode.IN_TABLE;
699                    return;
700                }
701                case $.BODY: {
702                    this.insertionMode = InsertionMode.IN_BODY;
703                    return;
704                }
705                case $.FRAMESET: {
706                    this.insertionMode = InsertionMode.IN_FRAMESET;
707                    return;
708                }
709                case $.SELECT: {
710                    this._resetInsertionModeForSelect(i);
711                    return;
712                }
713                case $.TEMPLATE: {
714                    this.insertionMode = this.tmplInsertionModeStack[0];
715                    return;
716                }
717                case $.HTML: {
718                    this.insertionMode = this.headElement ? InsertionMode.AFTER_HEAD : InsertionMode.BEFORE_HEAD;
719                    return;
720                }
721                case $.TD:
722                case $.TH: {
723                    if (i > 0) {
724                        this.insertionMode = InsertionMode.IN_CELL;
725                        return;
726                    }
727                    break;
728                }
729                case $.HEAD: {
730                    if (i > 0) {
731                        this.insertionMode = InsertionMode.IN_HEAD;
732                        return;
733                    }
734                    break;
735                }
736            }
737        }
738
739        this.insertionMode = InsertionMode.IN_BODY;
740    }
741
742    /** @protected */
743    _resetInsertionModeForSelect(selectIdx: number): void {
744        if (selectIdx > 0) {
745            for (let i = selectIdx - 1; i > 0; i--) {
746                const tn = this.openElements.tagIDs[i];
747
748                if (tn === $.TEMPLATE) {
749                    break;
750                } else if (tn === $.TABLE) {
751                    this.insertionMode = InsertionMode.IN_SELECT_IN_TABLE;
752                    return;
753                }
754            }
755        }
756
757        this.insertionMode = InsertionMode.IN_SELECT;
758    }
759
760    //Foster parenting
761
762    /** @protected */
763    _isElementCausesFosterParenting(tn: $): boolean {
764        return TABLE_STRUCTURE_TAGS.has(tn);
765    }
766
767    /** @protected */
768    _shouldFosterParentOnInsertion(): boolean {
769        return this.fosterParentingEnabled && this._isElementCausesFosterParenting(this.openElements.currentTagId);
770    }
771
772    /** @protected */
773    _findFosterParentingLocation(): { parent: T['parentNode']; beforeElement: T['element'] | null } {
774        for (let i = this.openElements.stackTop; i >= 0; i--) {
775            const openElement = this.openElements.items[i];
776
777            switch (this.openElements.tagIDs[i]) {
778                case $.TEMPLATE: {
779                    if (this.treeAdapter.getNamespaceURI(openElement) === NS.HTML) {
780                        return { parent: this.treeAdapter.getTemplateContent(openElement), beforeElement: null };
781                    }
782                    break;
783                }
784                case $.TABLE: {
785                    const parent = this.treeAdapter.getParentNode(openElement);
786
787                    if (parent) {
788                        return { parent, beforeElement: openElement };
789                    }
790
791                    return { parent: this.openElements.items[i - 1], beforeElement: null };
792                }
793                default:
794                // Do nothing
795            }
796        }
797
798        return { parent: this.openElements.items[0], beforeElement: null };
799    }
800
801    /** @protected */
802    _fosterParentElement(element: T['element']): void {
803        const location = this._findFosterParentingLocation();
804
805        if (location.beforeElement) {
806            this.treeAdapter.insertBefore(location.parent, element, location.beforeElement);
807        } else {
808            this.treeAdapter.appendChild(location.parent, element);
809        }
810    }
811
812    //Special elements
813
814    /** @protected */
815    _isSpecialElement(element: T['element'], id: $): boolean {
816        const ns = this.treeAdapter.getNamespaceURI(element);
817
818        return SPECIAL_ELEMENTS[ns].has(id);
819    }
820
821    /** @internal */
822    onCharacter(token: CharacterToken): void {
823        this.skipNextNewLine = false;
824
825        if (this.tokenizer.inForeignNode) {
826            characterInForeignContent(this, token);
827            return;
828        }
829
830        switch (this.insertionMode) {
831            case InsertionMode.INITIAL: {
832                tokenInInitialMode(this, token);
833                break;
834            }
835            case InsertionMode.BEFORE_HTML: {
836                tokenBeforeHtml(this, token);
837                break;
838            }
839            case InsertionMode.BEFORE_HEAD: {
840                tokenBeforeHead(this, token);
841                break;
842            }
843            case InsertionMode.IN_HEAD: {
844                tokenInHead(this, token);
845                break;
846            }
847            case InsertionMode.IN_HEAD_NO_SCRIPT: {
848                tokenInHeadNoScript(this, token);
849                break;
850            }
851            case InsertionMode.AFTER_HEAD: {
852                tokenAfterHead(this, token);
853                break;
854            }
855            case InsertionMode.IN_BODY:
856            case InsertionMode.IN_CAPTION:
857            case InsertionMode.IN_CELL:
858            case InsertionMode.IN_TEMPLATE: {
859                characterInBody(this, token);
860                break;
861            }
862            case InsertionMode.TEXT:
863            case InsertionMode.IN_SELECT:
864            case InsertionMode.IN_SELECT_IN_TABLE: {
865                this._insertCharacters(token);
866                break;
867            }
868            case InsertionMode.IN_TABLE:
869            case InsertionMode.IN_TABLE_BODY:
870            case InsertionMode.IN_ROW: {
871                characterInTable(this, token);
872                break;
873            }
874            case InsertionMode.IN_TABLE_TEXT: {
875                characterInTableText(this, token);
876                break;
877            }
878            case InsertionMode.IN_COLUMN_GROUP: {
879                tokenInColumnGroup(this, token);
880                break;
881            }
882            case InsertionMode.AFTER_BODY: {
883                tokenAfterBody(this, token);
884                break;
885            }
886            case InsertionMode.AFTER_AFTER_BODY: {
887                tokenAfterAfterBody(this, token);
888                break;
889            }
890            default:
891            // Do nothing
892        }
893    }
894    /** @internal */
895    onNullCharacter(token: CharacterToken): void {
896        this.skipNextNewLine = false;
897
898        if (this.tokenizer.inForeignNode) {
899            nullCharacterInForeignContent(this, token);
900            return;
901        }
902
903        switch (this.insertionMode) {
904            case InsertionMode.INITIAL: {
905                tokenInInitialMode(this, token);
906                break;
907            }
908            case InsertionMode.BEFORE_HTML: {
909                tokenBeforeHtml(this, token);
910                break;
911            }
912            case InsertionMode.BEFORE_HEAD: {
913                tokenBeforeHead(this, token);
914                break;
915            }
916            case InsertionMode.IN_HEAD: {
917                tokenInHead(this, token);
918                break;
919            }
920            case InsertionMode.IN_HEAD_NO_SCRIPT: {
921                tokenInHeadNoScript(this, token);
922                break;
923            }
924            case InsertionMode.AFTER_HEAD: {
925                tokenAfterHead(this, token);
926                break;
927            }
928            case InsertionMode.TEXT: {
929                this._insertCharacters(token);
930                break;
931            }
932            case InsertionMode.IN_TABLE:
933            case InsertionMode.IN_TABLE_BODY:
934            case InsertionMode.IN_ROW: {
935                characterInTable(this, token);
936                break;
937            }
938            case InsertionMode.IN_COLUMN_GROUP: {
939                tokenInColumnGroup(this, token);
940                break;
941            }
942            case InsertionMode.AFTER_BODY: {
943                tokenAfterBody(this, token);
944                break;
945            }
946            case InsertionMode.AFTER_AFTER_BODY: {
947                tokenAfterAfterBody(this, token);
948                break;
949            }
950            default:
951            // Do nothing
952        }
953    }
954    /** @internal */
955    onComment(token: CommentToken): void {
956        this.skipNextNewLine = false;
957
958        if (this.currentNotInHTML) {
959            appendComment(this, token);
960            return;
961        }
962
963        switch (this.insertionMode) {
964            case InsertionMode.INITIAL:
965            case InsertionMode.BEFORE_HTML:
966            case InsertionMode.BEFORE_HEAD:
967            case InsertionMode.IN_HEAD:
968            case InsertionMode.IN_HEAD_NO_SCRIPT:
969            case InsertionMode.AFTER_HEAD:
970            case InsertionMode.IN_BODY:
971            case InsertionMode.IN_TABLE:
972            case InsertionMode.IN_CAPTION:
973            case InsertionMode.IN_COLUMN_GROUP:
974            case InsertionMode.IN_TABLE_BODY:
975            case InsertionMode.IN_ROW:
976            case InsertionMode.IN_CELL:
977            case InsertionMode.IN_SELECT:
978            case InsertionMode.IN_SELECT_IN_TABLE:
979            case InsertionMode.IN_TEMPLATE:
980            case InsertionMode.IN_FRAMESET:
981            case InsertionMode.AFTER_FRAMESET: {
982                appendComment(this, token);
983                break;
984            }
985            case InsertionMode.IN_TABLE_TEXT: {
986                tokenInTableText(this, token);
987                break;
988            }
989            case InsertionMode.AFTER_BODY: {
990                appendCommentToRootHtmlElement(this, token);
991                break;
992            }
993            case InsertionMode.AFTER_AFTER_BODY:
994            case InsertionMode.AFTER_AFTER_FRAMESET: {
995                appendCommentToDocument(this, token);
996                break;
997            }
998            default:
999            // Do nothing
1000        }
1001    }
1002    /** @internal */
1003    onDoctype(token: DoctypeToken): void {
1004        this.skipNextNewLine = false;
1005        switch (this.insertionMode) {
1006            case InsertionMode.INITIAL: {
1007                doctypeInInitialMode(this, token);
1008                break;
1009            }
1010            case InsertionMode.BEFORE_HEAD:
1011            case InsertionMode.IN_HEAD:
1012            case InsertionMode.IN_HEAD_NO_SCRIPT:
1013            case InsertionMode.AFTER_HEAD: {
1014                this._err(token, ERR.misplacedDoctype);
1015                break;
1016            }
1017            case InsertionMode.IN_TABLE_TEXT: {
1018                tokenInTableText(this, token);
1019                break;
1020            }
1021            default:
1022            // Do nothing
1023        }
1024    }
1025    /** @internal */
1026    onStartTag(token: TagToken): void {
1027        this.skipNextNewLine = false;
1028        this.currentToken = token;
1029
1030        this._processStartTag(token);
1031
1032        if (token.selfClosing && !token.ackSelfClosing) {
1033            this._err(token, ERR.nonVoidHtmlElementStartTagWithTrailingSolidus);
1034        }
1035    }
1036    /**
1037     * Processes a given start tag.
1038     *
1039     * `onStartTag` checks if a self-closing tag was recognized. When a token
1040     * is moved inbetween multiple insertion modes, this check for self-closing
1041     * could lead to false positives. To avoid this, `_processStartTag` is used
1042     * for nested calls.
1043     *
1044     * @param token The token to process.
1045     * @protected
1046     */
1047    _processStartTag(token: TagToken): void {
1048        if (this.shouldProcessStartTagTokenInForeignContent(token)) {
1049            startTagInForeignContent(this, token);
1050        } else {
1051            this._startTagOutsideForeignContent(token);
1052        }
1053    }
1054    /** @protected */
1055    _startTagOutsideForeignContent(token: TagToken): void {
1056        switch (this.insertionMode) {
1057            case InsertionMode.INITIAL: {
1058                tokenInInitialMode(this, token);
1059                break;
1060            }
1061            case InsertionMode.BEFORE_HTML: {
1062                startTagBeforeHtml(this, token);
1063                break;
1064            }
1065            case InsertionMode.BEFORE_HEAD: {
1066                startTagBeforeHead(this, token);
1067                break;
1068            }
1069            case InsertionMode.IN_HEAD: {
1070                startTagInHead(this, token);
1071                break;
1072            }
1073            case InsertionMode.IN_HEAD_NO_SCRIPT: {
1074                startTagInHeadNoScript(this, token);
1075                break;
1076            }
1077            case InsertionMode.AFTER_HEAD: {
1078                startTagAfterHead(this, token);
1079                break;
1080            }
1081            case InsertionMode.IN_BODY: {
1082                startTagInBody(this, token);
1083                break;
1084            }
1085            case InsertionMode.IN_TABLE: {
1086                startTagInTable(this, token);
1087                break;
1088            }
1089            case InsertionMode.IN_TABLE_TEXT: {
1090                tokenInTableText(this, token);
1091                break;
1092            }
1093            case InsertionMode.IN_CAPTION: {
1094                startTagInCaption(this, token);
1095                break;
1096            }
1097            case InsertionMode.IN_COLUMN_GROUP: {
1098                startTagInColumnGroup(this, token);
1099                break;
1100            }
1101            case InsertionMode.IN_TABLE_BODY: {
1102                startTagInTableBody(this, token);
1103                break;
1104            }
1105            case InsertionMode.IN_ROW: {
1106                startTagInRow(this, token);
1107                break;
1108            }
1109            case InsertionMode.IN_CELL: {
1110                startTagInCell(this, token);
1111                break;
1112            }
1113            case InsertionMode.IN_SELECT: {
1114                startTagInSelect(this, token);
1115                break;
1116            }
1117            case InsertionMode.IN_SELECT_IN_TABLE: {
1118                startTagInSelectInTable(this, token);
1119                break;
1120            }
1121            case InsertionMode.IN_TEMPLATE: {
1122                startTagInTemplate(this, token);
1123                break;
1124            }
1125            case InsertionMode.AFTER_BODY: {
1126                startTagAfterBody(this, token);
1127                break;
1128            }
1129            case InsertionMode.IN_FRAMESET: {
1130                startTagInFrameset(this, token);
1131                break;
1132            }
1133            case InsertionMode.AFTER_FRAMESET: {
1134                startTagAfterFrameset(this, token);
1135                break;
1136            }
1137            case InsertionMode.AFTER_AFTER_BODY: {
1138                startTagAfterAfterBody(this, token);
1139                break;
1140            }
1141            case InsertionMode.AFTER_AFTER_FRAMESET: {
1142                startTagAfterAfterFrameset(this, token);
1143                break;
1144            }
1145            default:
1146            // Do nothing
1147        }
1148    }
1149    /** @internal */
1150    onEndTag(token: TagToken): void {
1151        this.skipNextNewLine = false;
1152        this.currentToken = token;
1153
1154        if (this.currentNotInHTML) {
1155            endTagInForeignContent(this, token);
1156        } else {
1157            this._endTagOutsideForeignContent(token);
1158        }
1159    }
1160    /** @protected */
1161    _endTagOutsideForeignContent(token: TagToken): void {
1162        switch (this.insertionMode) {
1163            case InsertionMode.INITIAL: {
1164                tokenInInitialMode(this, token);
1165                break;
1166            }
1167            case InsertionMode.BEFORE_HTML: {
1168                endTagBeforeHtml(this, token);
1169                break;
1170            }
1171            case InsertionMode.BEFORE_HEAD: {
1172                endTagBeforeHead(this, token);
1173                break;
1174            }
1175            case InsertionMode.IN_HEAD: {
1176                endTagInHead(this, token);
1177                break;
1178            }
1179            case InsertionMode.IN_HEAD_NO_SCRIPT: {
1180                endTagInHeadNoScript(this, token);
1181                break;
1182            }
1183            case InsertionMode.AFTER_HEAD: {
1184                endTagAfterHead(this, token);
1185                break;
1186            }
1187            case InsertionMode.IN_BODY: {
1188                endTagInBody(this, token);
1189                break;
1190            }
1191            case InsertionMode.TEXT: {
1192                endTagInText(this, token);
1193                break;
1194            }
1195            case InsertionMode.IN_TABLE: {
1196                endTagInTable(this, token);
1197                break;
1198            }
1199            case InsertionMode.IN_TABLE_TEXT: {
1200                tokenInTableText(this, token);
1201                break;
1202            }
1203            case InsertionMode.IN_CAPTION: {
1204                endTagInCaption(this, token);
1205                break;
1206            }
1207            case InsertionMode.IN_COLUMN_GROUP: {
1208                endTagInColumnGroup(this, token);
1209                break;
1210            }
1211            case InsertionMode.IN_TABLE_BODY: {
1212                endTagInTableBody(this, token);
1213                break;
1214            }
1215            case InsertionMode.IN_ROW: {
1216                endTagInRow(this, token);
1217                break;
1218            }
1219            case InsertionMode.IN_CELL: {
1220                endTagInCell(this, token);
1221                break;
1222            }
1223            case InsertionMode.IN_SELECT: {
1224                endTagInSelect(this, token);
1225                break;
1226            }
1227            case InsertionMode.IN_SELECT_IN_TABLE: {
1228                endTagInSelectInTable(this, token);
1229                break;
1230            }
1231            case InsertionMode.IN_TEMPLATE: {
1232                endTagInTemplate(this, token);
1233                break;
1234            }
1235            case InsertionMode.AFTER_BODY: {
1236                endTagAfterBody(this, token);
1237                break;
1238            }
1239            case InsertionMode.IN_FRAMESET: {
1240                endTagInFrameset(this, token);
1241                break;
1242            }
1243            case InsertionMode.AFTER_FRAMESET: {
1244                endTagAfterFrameset(this, token);
1245                break;
1246            }
1247            case InsertionMode.AFTER_AFTER_BODY: {
1248                tokenAfterAfterBody(this, token);
1249                break;
1250            }
1251            default:
1252            // Do nothing
1253        }
1254    }
1255    /** @internal */
1256    onEof(token: EOFToken): void {
1257        switch (this.insertionMode) {
1258            case InsertionMode.INITIAL: {
1259                tokenInInitialMode(this, token);
1260                break;
1261            }
1262            case InsertionMode.BEFORE_HTML: {
1263                tokenBeforeHtml(this, token);
1264                break;
1265            }
1266            case InsertionMode.BEFORE_HEAD: {
1267                tokenBeforeHead(this, token);
1268                break;
1269            }
1270            case InsertionMode.IN_HEAD: {
1271                tokenInHead(this, token);
1272                break;
1273            }
1274            case InsertionMode.IN_HEAD_NO_SCRIPT: {
1275                tokenInHeadNoScript(this, token);
1276                break;
1277            }
1278            case InsertionMode.AFTER_HEAD: {
1279                tokenAfterHead(this, token);
1280                break;
1281            }
1282            case InsertionMode.IN_BODY:
1283            case InsertionMode.IN_TABLE:
1284            case InsertionMode.IN_CAPTION:
1285            case InsertionMode.IN_COLUMN_GROUP:
1286            case InsertionMode.IN_TABLE_BODY:
1287            case InsertionMode.IN_ROW:
1288            case InsertionMode.IN_CELL:
1289            case InsertionMode.IN_SELECT:
1290            case InsertionMode.IN_SELECT_IN_TABLE: {
1291                eofInBody(this, token);
1292                break;
1293            }
1294            case InsertionMode.TEXT: {
1295                eofInText(this, token);
1296                break;
1297            }
1298            case InsertionMode.IN_TABLE_TEXT: {
1299                tokenInTableText(this, token);
1300                break;
1301            }
1302            case InsertionMode.IN_TEMPLATE: {
1303                eofInTemplate(this, token);
1304                break;
1305            }
1306            case InsertionMode.AFTER_BODY:
1307            case InsertionMode.IN_FRAMESET:
1308            case InsertionMode.AFTER_FRAMESET:
1309            case InsertionMode.AFTER_AFTER_BODY:
1310            case InsertionMode.AFTER_AFTER_FRAMESET: {
1311                stopParsing(this, token);
1312                break;
1313            }
1314            default:
1315            // Do nothing
1316        }
1317    }
1318    /** @internal */
1319    onWhitespaceCharacter(token: CharacterToken): void {
1320        if (this.skipNextNewLine) {
1321            this.skipNextNewLine = false;
1322
1323            if (token.chars.charCodeAt(0) === unicode.CODE_POINTS.LINE_FEED) {
1324                if (token.chars.length === 1) {
1325                    return;
1326                }
1327
1328                token.chars = token.chars.substr(1);
1329            }
1330        }
1331
1332        if (this.tokenizer.inForeignNode) {
1333            this._insertCharacters(token);
1334            return;
1335        }
1336
1337        switch (this.insertionMode) {
1338            case InsertionMode.IN_HEAD:
1339            case InsertionMode.IN_HEAD_NO_SCRIPT:
1340            case InsertionMode.AFTER_HEAD:
1341            case InsertionMode.TEXT:
1342            case InsertionMode.IN_COLUMN_GROUP:
1343            case InsertionMode.IN_SELECT:
1344            case InsertionMode.IN_SELECT_IN_TABLE:
1345            case InsertionMode.IN_FRAMESET:
1346            case InsertionMode.AFTER_FRAMESET: {
1347                this._insertCharacters(token);
1348                break;
1349            }
1350            case InsertionMode.IN_BODY:
1351            case InsertionMode.IN_CAPTION:
1352            case InsertionMode.IN_CELL:
1353            case InsertionMode.IN_TEMPLATE:
1354            case InsertionMode.AFTER_BODY:
1355            case InsertionMode.AFTER_AFTER_BODY:
1356            case InsertionMode.AFTER_AFTER_FRAMESET: {
1357                whitespaceCharacterInBody(this, token);
1358                break;
1359            }
1360            case InsertionMode.IN_TABLE:
1361            case InsertionMode.IN_TABLE_BODY:
1362            case InsertionMode.IN_ROW: {
1363                characterInTable(this, token);
1364                break;
1365            }
1366            case InsertionMode.IN_TABLE_TEXT: {
1367                whitespaceCharacterInTableText(this, token);
1368                break;
1369            }
1370            default:
1371            // Do nothing
1372        }
1373    }
1374}
1375
1376//Adoption agency algorithm
1377//(see: http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html#adoptionAgency)
1378//------------------------------------------------------------------
1379
1380//Steps 5-8 of the algorithm
1381function aaObtainFormattingElementEntry<T extends TreeAdapterTypeMap>(
1382    p: Parser<T>,
1383    token: TagToken,
1384): ElementEntry<T> | null {
1385    let formattingElementEntry = p.activeFormattingElements.getElementEntryInScopeWithTagName(token.tagName);
1386
1387    if (formattingElementEntry) {
1388        if (!p.openElements.contains(formattingElementEntry.element)) {
1389            p.activeFormattingElements.removeEntry(formattingElementEntry);
1390            formattingElementEntry = null;
1391        } else if (!p.openElements.hasInScope(token.tagID)) {
1392            formattingElementEntry = null;
1393        }
1394    } else {
1395        genericEndTagInBody(p, token);
1396    }
1397
1398    return formattingElementEntry;
1399}
1400
1401//Steps 9 and 10 of the algorithm
1402function aaObtainFurthestBlock<T extends TreeAdapterTypeMap>(
1403    p: Parser<T>,
1404    formattingElementEntry: ElementEntry<T>,
1405): T['parentNode'] | null {
1406    let furthestBlock = null;
1407    let idx = p.openElements.stackTop;
1408
1409    for (; idx >= 0; idx--) {
1410        const element = p.openElements.items[idx];
1411
1412        if (element === formattingElementEntry.element) {
1413            break;
1414        }
1415
1416        if (p._isSpecialElement(element, p.openElements.tagIDs[idx])) {
1417            furthestBlock = element;
1418        }
1419    }
1420
1421    if (!furthestBlock) {
1422        p.openElements.shortenToLength(idx < 0 ? 0 : idx);
1423        p.activeFormattingElements.removeEntry(formattingElementEntry);
1424    }
1425
1426    return furthestBlock;
1427}
1428
1429//Step 13 of the algorithm
1430function aaInnerLoop<T extends TreeAdapterTypeMap>(
1431    p: Parser<T>,
1432    furthestBlock: T['element'],
1433    formattingElement: T['element'],
1434): T['element'] {
1435    let lastElement = furthestBlock;
1436    let nextElement = p.openElements.getCommonAncestor(furthestBlock) as T['element'];
1437
1438    for (let i = 0, element = nextElement; element !== formattingElement; i++, element = nextElement) {
1439        //NOTE: store the next element for the next loop iteration (it may be deleted from the stack by step 9.5)
1440        nextElement = p.openElements.getCommonAncestor(element) as T['element'];
1441
1442        const elementEntry = p.activeFormattingElements.getElementEntry(element);
1443        const counterOverflow = elementEntry && i >= AA_INNER_LOOP_ITER;
1444        const shouldRemoveFromOpenElements = !elementEntry || counterOverflow;
1445
1446        if (shouldRemoveFromOpenElements) {
1447            if (counterOverflow) {
1448                p.activeFormattingElements.removeEntry(elementEntry);
1449            }
1450
1451            p.openElements.remove(element);
1452        } else {
1453            element = aaRecreateElementFromEntry(p, elementEntry);
1454
1455            if (lastElement === furthestBlock) {
1456                p.activeFormattingElements.bookmark = elementEntry;
1457            }
1458
1459            p.treeAdapter.detachNode(lastElement);
1460            p.treeAdapter.appendChild(element, lastElement);
1461            lastElement = element;
1462        }
1463    }
1464
1465    return lastElement;
1466}
1467
1468//Step 13.7 of the algorithm
1469function aaRecreateElementFromEntry<T extends TreeAdapterTypeMap>(
1470    p: Parser<T>,
1471    elementEntry: ElementEntry<T>,
1472): T['element'] {
1473    const ns = p.treeAdapter.getNamespaceURI(elementEntry.element);
1474    const newElement = p.treeAdapter.createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs);
1475
1476    p.openElements.replace(elementEntry.element, newElement);
1477    elementEntry.element = newElement;
1478
1479    return newElement;
1480}
1481
1482//Step 14 of the algorithm
1483function aaInsertLastNodeInCommonAncestor<T extends TreeAdapterTypeMap>(
1484    p: Parser<T>,
1485    commonAncestor: T['parentNode'],
1486    lastElement: T['element'],
1487): void {
1488    const tn = p.treeAdapter.getTagName(commonAncestor);
1489    const tid = getTagID(tn);
1490
1491    if (p._isElementCausesFosterParenting(tid)) {
1492        p._fosterParentElement(lastElement);
1493    } else {
1494        const ns = p.treeAdapter.getNamespaceURI(commonAncestor);
1495
1496        if (tid === $.TEMPLATE && ns === NS.HTML) {
1497            commonAncestor = p.treeAdapter.getTemplateContent(commonAncestor);
1498        }
1499
1500        p.treeAdapter.appendChild(commonAncestor, lastElement);
1501    }
1502}
1503
1504//Steps 15-19 of the algorithm
1505function aaReplaceFormattingElement<T extends TreeAdapterTypeMap>(
1506    p: Parser<T>,
1507    furthestBlock: T['parentNode'],
1508    formattingElementEntry: ElementEntry<T>,
1509): void {
1510    const ns = p.treeAdapter.getNamespaceURI(formattingElementEntry.element);
1511    const { token } = formattingElementEntry;
1512    const newElement = p.treeAdapter.createElement(token.tagName, ns, token.attrs);
1513
1514    p._adoptNodes(furthestBlock, newElement);
1515    p.treeAdapter.appendChild(furthestBlock, newElement);
1516
1517    p.activeFormattingElements.insertElementAfterBookmark(newElement, token);
1518    p.activeFormattingElements.removeEntry(formattingElementEntry);
1519
1520    p.openElements.remove(formattingElementEntry.element);
1521    p.openElements.insertAfter(furthestBlock, newElement, token.tagID);
1522}
1523
1524//Algorithm entry point
1525function callAdoptionAgency<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1526    for (let i = 0; i < AA_OUTER_LOOP_ITER; i++) {
1527        const formattingElementEntry = aaObtainFormattingElementEntry(p, token);
1528
1529        if (!formattingElementEntry) {
1530            break;
1531        }
1532
1533        const furthestBlock = aaObtainFurthestBlock(p, formattingElementEntry);
1534
1535        if (!furthestBlock) {
1536            break;
1537        }
1538
1539        p.activeFormattingElements.bookmark = formattingElementEntry;
1540
1541        const lastElement = aaInnerLoop(p, furthestBlock, formattingElementEntry.element);
1542        const commonAncestor = p.openElements.getCommonAncestor(formattingElementEntry.element);
1543
1544        p.treeAdapter.detachNode(lastElement);
1545        if (commonAncestor) aaInsertLastNodeInCommonAncestor(p, commonAncestor, lastElement);
1546        aaReplaceFormattingElement(p, furthestBlock, formattingElementEntry);
1547    }
1548}
1549
1550//Generic token handlers
1551//------------------------------------------------------------------
1552function appendComment<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CommentToken): void {
1553    p._appendCommentNode(token, p.openElements.currentTmplContentOrNode);
1554}
1555
1556function appendCommentToRootHtmlElement<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CommentToken): void {
1557    p._appendCommentNode(token, p.openElements.items[0]);
1558}
1559
1560function appendCommentToDocument<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CommentToken): void {
1561    p._appendCommentNode(token, p.document);
1562}
1563
1564function stopParsing<T extends TreeAdapterTypeMap>(p: Parser<T>, token: EOFToken): void {
1565    p.stopped = true;
1566
1567    // NOTE: Set end locations for elements that remain on the open element stack.
1568    if (token.location) {
1569        // NOTE: If we are not in a fragment, `html` and `body` will stay on the stack.
1570        // This is a problem, as we might overwrite their end position here.
1571        const target = p.fragmentContext ? 0 : 2;
1572        for (let i = p.openElements.stackTop; i >= target; i--) {
1573            p._setEndLocation(p.openElements.items[i], token);
1574        }
1575
1576        // Handle `html` and `body`
1577        if (!p.fragmentContext && p.openElements.stackTop >= 0) {
1578            const htmlElement = p.openElements.items[0];
1579            const htmlLocation = p.treeAdapter.getNodeSourceCodeLocation(htmlElement);
1580            if (htmlLocation && !htmlLocation.endTag) {
1581                p._setEndLocation(htmlElement, token);
1582
1583                if (p.openElements.stackTop >= 1) {
1584                    const bodyElement = p.openElements.items[1];
1585                    const bodyLocation = p.treeAdapter.getNodeSourceCodeLocation(bodyElement);
1586                    if (bodyLocation && !bodyLocation.endTag) {
1587                        p._setEndLocation(bodyElement, token);
1588                    }
1589                }
1590            }
1591        }
1592    }
1593}
1594
1595// The "initial" insertion mode
1596//------------------------------------------------------------------
1597function doctypeInInitialMode<T extends TreeAdapterTypeMap>(p: Parser<T>, token: DoctypeToken): void {
1598    p._setDocumentType(token);
1599
1600    const mode = token.forceQuirks ? DOCUMENT_MODE.QUIRKS : doctype.getDocumentMode(token);
1601
1602    if (!doctype.isConforming(token)) {
1603        p._err(token, ERR.nonConformingDoctype);
1604    }
1605
1606    p.treeAdapter.setDocumentMode(p.document, mode);
1607
1608    p.insertionMode = InsertionMode.BEFORE_HTML;
1609}
1610
1611function tokenInInitialMode<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1612    p._err(token, ERR.missingDoctype, true);
1613    p.treeAdapter.setDocumentMode(p.document, DOCUMENT_MODE.QUIRKS);
1614    p.insertionMode = InsertionMode.BEFORE_HTML;
1615    p._processToken(token);
1616}
1617
1618// The "before html" insertion mode
1619//------------------------------------------------------------------
1620function startTagBeforeHtml<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1621    if (token.tagID === $.HTML) {
1622        p._insertElement(token, NS.HTML);
1623        p.insertionMode = InsertionMode.BEFORE_HEAD;
1624    } else {
1625        tokenBeforeHtml(p, token);
1626    }
1627}
1628
1629function endTagBeforeHtml<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1630    const tn = token.tagID;
1631
1632    if (tn === $.HTML || tn === $.HEAD || tn === $.BODY || tn === $.BR) {
1633        tokenBeforeHtml(p, token);
1634    }
1635}
1636
1637function tokenBeforeHtml<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1638    p._insertFakeRootElement();
1639    p.insertionMode = InsertionMode.BEFORE_HEAD;
1640    p._processToken(token);
1641}
1642
1643// The "before head" insertion mode
1644//------------------------------------------------------------------
1645function startTagBeforeHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1646    switch (token.tagID) {
1647        case $.HTML: {
1648            startTagInBody(p, token);
1649            break;
1650        }
1651        case $.HEAD: {
1652            p._insertElement(token, NS.HTML);
1653            p.headElement = p.openElements.current;
1654            p.insertionMode = InsertionMode.IN_HEAD;
1655            break;
1656        }
1657        default: {
1658            tokenBeforeHead(p, token);
1659        }
1660    }
1661}
1662
1663function endTagBeforeHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1664    const tn = token.tagID;
1665
1666    if (tn === $.HEAD || tn === $.BODY || tn === $.HTML || tn === $.BR) {
1667        tokenBeforeHead(p, token);
1668    } else {
1669        p._err(token, ERR.endTagWithoutMatchingOpenElement);
1670    }
1671}
1672
1673function tokenBeforeHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1674    p._insertFakeElement(TN.HEAD, $.HEAD);
1675    p.headElement = p.openElements.current;
1676    p.insertionMode = InsertionMode.IN_HEAD;
1677    p._processToken(token);
1678}
1679
1680// The "in head" insertion mode
1681//------------------------------------------------------------------
1682function startTagInHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1683    switch (token.tagID) {
1684        case $.HTML: {
1685            startTagInBody(p, token);
1686            break;
1687        }
1688        case $.BASE:
1689        case $.BASEFONT:
1690        case $.BGSOUND:
1691        case $.LINK:
1692        case $.META: {
1693            p._appendElement(token, NS.HTML);
1694            token.ackSelfClosing = true;
1695            break;
1696        }
1697        case $.TITLE: {
1698            p._switchToTextParsing(token, TokenizerMode.RCDATA);
1699            break;
1700        }
1701        case $.NOSCRIPT: {
1702            if (p.options.scriptingEnabled) {
1703                p._switchToTextParsing(token, TokenizerMode.RAWTEXT);
1704            } else {
1705                p._insertElement(token, NS.HTML);
1706                p.insertionMode = InsertionMode.IN_HEAD_NO_SCRIPT;
1707            }
1708            break;
1709        }
1710        case $.NOFRAMES:
1711        case $.STYLE: {
1712            p._switchToTextParsing(token, TokenizerMode.RAWTEXT);
1713            break;
1714        }
1715        case $.SCRIPT: {
1716            p._switchToTextParsing(token, TokenizerMode.SCRIPT_DATA);
1717            break;
1718        }
1719        case $.TEMPLATE: {
1720            p._insertTemplate(token);
1721            p.activeFormattingElements.insertMarker();
1722            p.framesetOk = false;
1723            p.insertionMode = InsertionMode.IN_TEMPLATE;
1724            p.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE);
1725            break;
1726        }
1727        case $.HEAD: {
1728            p._err(token, ERR.misplacedStartTagForHeadElement);
1729            break;
1730        }
1731        default: {
1732            tokenInHead(p, token);
1733        }
1734    }
1735}
1736
1737function endTagInHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1738    switch (token.tagID) {
1739        case $.HEAD: {
1740            p.openElements.pop();
1741            p.insertionMode = InsertionMode.AFTER_HEAD;
1742            break;
1743        }
1744        case $.BODY:
1745        case $.BR:
1746        case $.HTML: {
1747            tokenInHead(p, token);
1748            break;
1749        }
1750        case $.TEMPLATE: {
1751            templateEndTagInHead<T>(p, token);
1752            break;
1753        }
1754        default: {
1755            p._err(token, ERR.endTagWithoutMatchingOpenElement);
1756        }
1757    }
1758}
1759
1760function templateEndTagInHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1761    if (p.openElements.tmplCount > 0) {
1762        p.openElements.generateImpliedEndTagsThoroughly();
1763
1764        if (p.openElements.currentTagId !== $.TEMPLATE) {
1765            p._err(token, ERR.closingOfElementWithOpenChildElements);
1766        }
1767
1768        p.openElements.popUntilTagNamePopped($.TEMPLATE);
1769        p.activeFormattingElements.clearToLastMarker();
1770        p.tmplInsertionModeStack.shift();
1771        p._resetInsertionMode();
1772    } else {
1773        p._err(token, ERR.endTagWithoutMatchingOpenElement);
1774    }
1775}
1776
1777function tokenInHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1778    p.openElements.pop();
1779    p.insertionMode = InsertionMode.AFTER_HEAD;
1780    p._processToken(token);
1781}
1782
1783// The "in head no script" insertion mode
1784//------------------------------------------------------------------
1785function startTagInHeadNoScript<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1786    switch (token.tagID) {
1787        case $.HTML: {
1788            startTagInBody(p, token);
1789            break;
1790        }
1791        case $.BASEFONT:
1792        case $.BGSOUND:
1793        case $.HEAD:
1794        case $.LINK:
1795        case $.META:
1796        case $.NOFRAMES:
1797        case $.STYLE: {
1798            startTagInHead(p, token);
1799            break;
1800        }
1801        case $.NOSCRIPT: {
1802            p._err(token, ERR.nestedNoscriptInHead);
1803            break;
1804        }
1805        default: {
1806            tokenInHeadNoScript(p, token);
1807        }
1808    }
1809}
1810
1811function endTagInHeadNoScript<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1812    switch (token.tagID) {
1813        case $.NOSCRIPT: {
1814            p.openElements.pop();
1815            p.insertionMode = InsertionMode.IN_HEAD;
1816            break;
1817        }
1818        case $.BR: {
1819            tokenInHeadNoScript(p, token);
1820            break;
1821        }
1822        default: {
1823            p._err(token, ERR.endTagWithoutMatchingOpenElement);
1824        }
1825    }
1826}
1827
1828function tokenInHeadNoScript<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1829    const errCode = token.type === TokenType.EOF ? ERR.openElementsLeftAfterEof : ERR.disallowedContentInNoscriptInHead;
1830
1831    p._err(token, errCode);
1832    p.openElements.pop();
1833    p.insertionMode = InsertionMode.IN_HEAD;
1834    p._processToken(token);
1835}
1836
1837// The "after head" insertion mode
1838//------------------------------------------------------------------
1839function startTagAfterHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1840    switch (token.tagID) {
1841        case $.HTML: {
1842            startTagInBody(p, token);
1843            break;
1844        }
1845        case $.BODY: {
1846            p._insertElement(token, NS.HTML);
1847            p.framesetOk = false;
1848            p.insertionMode = InsertionMode.IN_BODY;
1849            break;
1850        }
1851        case $.FRAMESET: {
1852            p._insertElement(token, NS.HTML);
1853            p.insertionMode = InsertionMode.IN_FRAMESET;
1854            break;
1855        }
1856        case $.BASE:
1857        case $.BASEFONT:
1858        case $.BGSOUND:
1859        case $.LINK:
1860        case $.META:
1861        case $.NOFRAMES:
1862        case $.SCRIPT:
1863        case $.STYLE:
1864        case $.TEMPLATE:
1865        case $.TITLE: {
1866            p._err(token, ERR.abandonedHeadElementChild);
1867            p.openElements.push(p.headElement!, $.HEAD);
1868            startTagInHead(p, token);
1869            p.openElements.remove(p.headElement!);
1870            break;
1871        }
1872        case $.HEAD: {
1873            p._err(token, ERR.misplacedStartTagForHeadElement);
1874            break;
1875        }
1876        default: {
1877            tokenAfterHead(p, token);
1878        }
1879    }
1880}
1881
1882function endTagAfterHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1883    switch (token.tagID) {
1884        case $.BODY:
1885        case $.HTML:
1886        case $.BR: {
1887            tokenAfterHead(p, token);
1888            break;
1889        }
1890        case $.TEMPLATE: {
1891            templateEndTagInHead(p, token);
1892            break;
1893        }
1894        default: {
1895            p._err(token, ERR.endTagWithoutMatchingOpenElement);
1896        }
1897    }
1898}
1899
1900function tokenAfterHead<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1901    p._insertFakeElement(TN.BODY, $.BODY);
1902    p.insertionMode = InsertionMode.IN_BODY;
1903    modeInBody(p, token);
1904}
1905
1906// The "in body" insertion mode
1907//------------------------------------------------------------------
1908function modeInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
1909    switch (token.type) {
1910        case TokenType.CHARACTER: {
1911            characterInBody(p, token);
1912            break;
1913        }
1914        case TokenType.WHITESPACE_CHARACTER: {
1915            whitespaceCharacterInBody(p, token);
1916            break;
1917        }
1918        case TokenType.COMMENT: {
1919            appendComment(p, token);
1920            break;
1921        }
1922        case TokenType.START_TAG: {
1923            startTagInBody(p, token);
1924            break;
1925        }
1926        case TokenType.END_TAG: {
1927            endTagInBody(p, token);
1928            break;
1929        }
1930        case TokenType.EOF: {
1931            eofInBody(p, token);
1932            break;
1933        }
1934        default:
1935        // Do nothing
1936    }
1937}
1938
1939function whitespaceCharacterInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
1940    p._reconstructActiveFormattingElements();
1941    p._insertCharacters(token);
1942}
1943
1944function characterInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
1945    p._reconstructActiveFormattingElements();
1946    p._insertCharacters(token);
1947    p.framesetOk = false;
1948}
1949
1950function htmlStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1951    if (p.openElements.tmplCount === 0) {
1952        p.treeAdapter.adoptAttributes(p.openElements.items[0], token.attrs);
1953    }
1954}
1955
1956function bodyStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1957    const bodyElement = p.openElements.tryPeekProperlyNestedBodyElement();
1958
1959    if (bodyElement && p.openElements.tmplCount === 0) {
1960        p.framesetOk = false;
1961        p.treeAdapter.adoptAttributes(bodyElement, token.attrs);
1962    }
1963}
1964
1965function framesetStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1966    const bodyElement = p.openElements.tryPeekProperlyNestedBodyElement();
1967
1968    if (p.framesetOk && bodyElement) {
1969        p.treeAdapter.detachNode(bodyElement);
1970        p.openElements.popAllUpToHtmlElement();
1971        p._insertElement(token, NS.HTML);
1972        p.insertionMode = InsertionMode.IN_FRAMESET;
1973    }
1974}
1975
1976function addressStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1977    if (p.openElements.hasInButtonScope($.P)) {
1978        p._closePElement();
1979    }
1980
1981    p._insertElement(token, NS.HTML);
1982}
1983
1984function numberedHeaderStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1985    if (p.openElements.hasInButtonScope($.P)) {
1986        p._closePElement();
1987    }
1988
1989    if (NUMBERED_HEADERS.has(p.openElements.currentTagId)) {
1990        p.openElements.pop();
1991    }
1992
1993    p._insertElement(token, NS.HTML);
1994}
1995
1996function preStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
1997    if (p.openElements.hasInButtonScope($.P)) {
1998        p._closePElement();
1999    }
2000
2001    p._insertElement(token, NS.HTML);
2002    //NOTE: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move
2003    //on to the next one. (Newlines at the start of pre blocks are ignored as an authoring convenience.)
2004    p.skipNextNewLine = true;
2005    p.framesetOk = false;
2006}
2007
2008function formStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2009    const inTemplate = p.openElements.tmplCount > 0;
2010
2011    if (!p.formElement || inTemplate) {
2012        if (p.openElements.hasInButtonScope($.P)) {
2013            p._closePElement();
2014        }
2015
2016        p._insertElement(token, NS.HTML);
2017
2018        if (!inTemplate) {
2019            p.formElement = p.openElements.current;
2020        }
2021    }
2022}
2023
2024function listItemStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2025    p.framesetOk = false;
2026
2027    const tn = token.tagID;
2028
2029    for (let i = p.openElements.stackTop; i >= 0; i--) {
2030        const elementId = p.openElements.tagIDs[i];
2031
2032        if (
2033            (tn === $.LI && elementId === $.LI) ||
2034            ((tn === $.DD || tn === $.DT) && (elementId === $.DD || elementId === $.DT))
2035        ) {
2036            p.openElements.generateImpliedEndTagsWithExclusion(elementId);
2037            p.openElements.popUntilTagNamePopped(elementId);
2038            break;
2039        }
2040
2041        if (
2042            elementId !== $.ADDRESS &&
2043            elementId !== $.DIV &&
2044            elementId !== $.P &&
2045            p._isSpecialElement(p.openElements.items[i], elementId)
2046        ) {
2047            break;
2048        }
2049    }
2050
2051    if (p.openElements.hasInButtonScope($.P)) {
2052        p._closePElement();
2053    }
2054
2055    p._insertElement(token, NS.HTML);
2056}
2057
2058function plaintextStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2059    if (p.openElements.hasInButtonScope($.P)) {
2060        p._closePElement();
2061    }
2062
2063    p._insertElement(token, NS.HTML);
2064    p.tokenizer.state = TokenizerMode.PLAINTEXT;
2065}
2066
2067function buttonStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2068    if (p.openElements.hasInScope($.BUTTON)) {
2069        p.openElements.generateImpliedEndTags();
2070        p.openElements.popUntilTagNamePopped($.BUTTON);
2071    }
2072
2073    p._reconstructActiveFormattingElements();
2074    p._insertElement(token, NS.HTML);
2075    p.framesetOk = false;
2076}
2077
2078function aStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2079    const activeElementEntry = p.activeFormattingElements.getElementEntryInScopeWithTagName(TN.A);
2080
2081    if (activeElementEntry) {
2082        callAdoptionAgency(p, token);
2083        p.openElements.remove(activeElementEntry.element);
2084        p.activeFormattingElements.removeEntry(activeElementEntry);
2085    }
2086
2087    p._reconstructActiveFormattingElements();
2088    p._insertElement(token, NS.HTML);
2089    p.activeFormattingElements.pushElement(p.openElements.current, token);
2090}
2091
2092function bStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2093    p._reconstructActiveFormattingElements();
2094    p._insertElement(token, NS.HTML);
2095    p.activeFormattingElements.pushElement(p.openElements.current, token);
2096}
2097
2098function nobrStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2099    p._reconstructActiveFormattingElements();
2100
2101    if (p.openElements.hasInScope($.NOBR)) {
2102        callAdoptionAgency(p, token);
2103        p._reconstructActiveFormattingElements();
2104    }
2105
2106    p._insertElement(token, NS.HTML);
2107    p.activeFormattingElements.pushElement(p.openElements.current, token);
2108}
2109
2110function appletStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2111    p._reconstructActiveFormattingElements();
2112    p._insertElement(token, NS.HTML);
2113    p.activeFormattingElements.insertMarker();
2114    p.framesetOk = false;
2115}
2116
2117function tableStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2118    if (p.treeAdapter.getDocumentMode(p.document) !== DOCUMENT_MODE.QUIRKS && p.openElements.hasInButtonScope($.P)) {
2119        p._closePElement();
2120    }
2121
2122    p._insertElement(token, NS.HTML);
2123    p.framesetOk = false;
2124    p.insertionMode = InsertionMode.IN_TABLE;
2125}
2126
2127function areaStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2128    p._reconstructActiveFormattingElements();
2129    p._appendElement(token, NS.HTML);
2130    p.framesetOk = false;
2131    token.ackSelfClosing = true;
2132}
2133
2134function isHiddenInput(token: TagToken): boolean {
2135    const inputType = getTokenAttr(token, ATTRS.TYPE);
2136
2137    return inputType != null && inputType.toLowerCase() === HIDDEN_INPUT_TYPE;
2138}
2139
2140function inputStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2141    p._reconstructActiveFormattingElements();
2142    p._appendElement(token, NS.HTML);
2143
2144    if (!isHiddenInput(token)) {
2145        p.framesetOk = false;
2146    }
2147
2148    token.ackSelfClosing = true;
2149}
2150
2151function paramStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2152    p._appendElement(token, NS.HTML);
2153    token.ackSelfClosing = true;
2154}
2155
2156function hrStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2157    if (p.openElements.hasInButtonScope($.P)) {
2158        p._closePElement();
2159    }
2160
2161    p._appendElement(token, NS.HTML);
2162    p.framesetOk = false;
2163    token.ackSelfClosing = true;
2164}
2165
2166function imageStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2167    token.tagName = TN.IMG;
2168    token.tagID = $.IMG;
2169    areaStartTagInBody(p, token);
2170}
2171
2172function textareaStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2173    p._insertElement(token, NS.HTML);
2174    //NOTE: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move
2175    //on to the next one. (Newlines at the start of textarea elements are ignored as an authoring convenience.)
2176    p.skipNextNewLine = true;
2177    p.tokenizer.state = TokenizerMode.RCDATA;
2178    p.originalInsertionMode = p.insertionMode;
2179    p.framesetOk = false;
2180    p.insertionMode = InsertionMode.TEXT;
2181}
2182
2183function xmpStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2184    if (p.openElements.hasInButtonScope($.P)) {
2185        p._closePElement();
2186    }
2187
2188    p._reconstructActiveFormattingElements();
2189    p.framesetOk = false;
2190    p._switchToTextParsing(token, TokenizerMode.RAWTEXT);
2191}
2192
2193function iframeStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2194    p.framesetOk = false;
2195    p._switchToTextParsing(token, TokenizerMode.RAWTEXT);
2196}
2197
2198//NOTE: here we assume that we always act as a user agent with enabled plugins/frames, so we parse
2199//<noembed>/<noframes> as rawtext.
2200function rawTextStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2201    p._switchToTextParsing(token, TokenizerMode.RAWTEXT);
2202}
2203
2204function selectStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2205    p._reconstructActiveFormattingElements();
2206    p._insertElement(token, NS.HTML);
2207    p.framesetOk = false;
2208
2209    p.insertionMode =
2210        p.insertionMode === InsertionMode.IN_TABLE ||
2211        p.insertionMode === InsertionMode.IN_CAPTION ||
2212        p.insertionMode === InsertionMode.IN_TABLE_BODY ||
2213        p.insertionMode === InsertionMode.IN_ROW ||
2214        p.insertionMode === InsertionMode.IN_CELL
2215            ? InsertionMode.IN_SELECT_IN_TABLE
2216            : InsertionMode.IN_SELECT;
2217}
2218
2219function optgroupStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2220    if (p.openElements.currentTagId === $.OPTION) {
2221        p.openElements.pop();
2222    }
2223
2224    p._reconstructActiveFormattingElements();
2225    p._insertElement(token, NS.HTML);
2226}
2227
2228function rbStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2229    if (p.openElements.hasInScope($.RUBY)) {
2230        p.openElements.generateImpliedEndTags();
2231    }
2232
2233    p._insertElement(token, NS.HTML);
2234}
2235
2236function rtStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2237    if (p.openElements.hasInScope($.RUBY)) {
2238        p.openElements.generateImpliedEndTagsWithExclusion($.RTC);
2239    }
2240
2241    p._insertElement(token, NS.HTML);
2242}
2243
2244function mathStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2245    p._reconstructActiveFormattingElements();
2246
2247    if (token.selfClosing) {
2248        p._appendElement(token, NS.MATHML);
2249    } else {
2250        p._insertElement(token, NS.MATHML);
2251    }
2252
2253    token.ackSelfClosing = true;
2254}
2255
2256function svgStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2257    p._reconstructActiveFormattingElements();
2258
2259    if (token.selfClosing) {
2260        p._appendElement(token, NS.SVG);
2261    } else {
2262        p._insertElement(token, NS.SVG);
2263    }
2264
2265    token.ackSelfClosing = true;
2266}
2267
2268function genericStartTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2269    p._reconstructActiveFormattingElements();
2270    p._insertElement(token, NS.HTML);
2271}
2272
2273function startTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2274    switch (token.tagID) {
2275        case $.I:
2276        case $.S:
2277        case $.B:
2278        case $.U:
2279        case $.EM:
2280        case $.TT:
2281        case $.BIG:
2282        case $.CODE:
2283        case $.FONT:
2284        case $.SMALL:
2285        case $.STRIKE:
2286        case $.STRONG: {
2287            bStartTagInBody(p, token);
2288            break;
2289        }
2290        case $.A: {
2291            aStartTagInBody(p, token);
2292            break;
2293        }
2294        case $.H1:
2295        case $.H2:
2296        case $.H3:
2297        case $.H4:
2298        case $.H5:
2299        case $.H6: {
2300            numberedHeaderStartTagInBody(p, token);
2301            break;
2302        }
2303        case $.P:
2304        case $.DL:
2305        case $.OL:
2306        case $.UL:
2307        case $.DIV:
2308        case $.DIR:
2309        case $.NAV:
2310        case $.MAIN:
2311        case $.MENU:
2312        case $.ASIDE:
2313        case $.CENTER:
2314        case $.FIGURE:
2315        case $.FOOTER:
2316        case $.HEADER:
2317        case $.HGROUP:
2318        case $.DIALOG:
2319        case $.DETAILS:
2320        case $.ADDRESS:
2321        case $.ARTICLE:
2322        case $.SEARCH:
2323        case $.SECTION:
2324        case $.SUMMARY:
2325        case $.FIELDSET:
2326        case $.BLOCKQUOTE:
2327        case $.FIGCAPTION: {
2328            addressStartTagInBody(p, token);
2329            break;
2330        }
2331        case $.LI:
2332        case $.DD:
2333        case $.DT: {
2334            listItemStartTagInBody(p, token);
2335            break;
2336        }
2337        case $.BR:
2338        case $.IMG:
2339        case $.WBR:
2340        case $.AREA:
2341        case $.EMBED:
2342        case $.WEB:
2343        case $.XCOMPONENT:
2344        case $.RATING:
2345        case $.CANVAS:
2346        case $.CAMERA:
2347        case $.AUDIO:
2348        case $.VIDEO:
2349        case $.SWITCH:
2350        case $.QRCODE:
2351        case $.PICKER_VIEW:
2352        case $.PICKER:
2353        case $.PROGRESS:
2354        case $.SLIDER:
2355        case $.CHART:
2356        case $.CALENDAR:
2357        case $.DIVIDER:
2358        case $.IMAGE_ANIMATOR:
2359        case $.KEYGEN: {
2360            areaStartTagInBody(p, token);
2361            break;
2362        }
2363        case $.HR: {
2364            hrStartTagInBody(p, token);
2365            break;
2366        }
2367        case $.RB:
2368        case $.RTC: {
2369            rbStartTagInBody(p, token);
2370            break;
2371        }
2372        case $.RT:
2373        case $.RP: {
2374            rtStartTagInBody(p, token);
2375            break;
2376        }
2377        case $.PRE:
2378        case $.LISTING: {
2379            preStartTagInBody(p, token);
2380            break;
2381        }
2382        case $.XMP: {
2383            xmpStartTagInBody(p, token);
2384            break;
2385        }
2386        case $.SVG: {
2387            svgStartTagInBody(p, token);
2388            break;
2389        }
2390        case $.HTML: {
2391            htmlStartTagInBody(p, token);
2392            break;
2393        }
2394        case $.BASE:
2395        case $.LINK:
2396        case $.META:
2397        case $.STYLE:
2398        case $.TITLE:
2399        case $.SCRIPT:
2400        case $.BGSOUND:
2401        case $.BASEFONT:
2402        case $.TEMPLATE: {
2403            startTagInHead(p, token);
2404            break;
2405        }
2406        case $.BODY: {
2407            bodyStartTagInBody(p, token);
2408            break;
2409        }
2410        case $.FORM: {
2411            formStartTagInBody(p, token);
2412            break;
2413        }
2414        case $.NOBR: {
2415            nobrStartTagInBody(p, token);
2416            break;
2417        }
2418        case $.MATH: {
2419            mathStartTagInBody(p, token);
2420            break;
2421        }
2422        case $.TABLE: {
2423            tableStartTagInBody(p, token);
2424            break;
2425        }
2426        case $.INPUT: {
2427            inputStartTagInBody(p, token);
2428            break;
2429        }
2430        case $.PARAM:
2431        case $.TRACK:
2432        case $.SOURCE: {
2433            paramStartTagInBody(p, token);
2434            break;
2435        }
2436        case $.IMAGE: {
2437            imageStartTagInBody(p, token);
2438            break;
2439        }
2440        case $.BUTTON: {
2441            buttonStartTagInBody(p, token);
2442            break;
2443        }
2444        case $.APPLET:
2445        case $.OBJECT:
2446        case $.MARQUEE: {
2447            appletStartTagInBody(p, token);
2448            break;
2449        }
2450        case $.IFRAME: {
2451            iframeStartTagInBody(p, token);
2452            break;
2453        }
2454        case $.SELECT: {
2455            selectStartTagInBody(p, token);
2456            break;
2457        }
2458        case $.OPTION:
2459        case $.OPTGROUP: {
2460            optgroupStartTagInBody(p, token);
2461            break;
2462        }
2463        case $.NOEMBED:
2464        case $.NOFRAMES: {
2465            rawTextStartTagInBody(p, token);
2466            break;
2467        }
2468        case $.FRAMESET: {
2469            framesetStartTagInBody(p, token);
2470            break;
2471        }
2472        case $.TEXTAREA: {
2473            textareaStartTagInBody(p, token);
2474            break;
2475        }
2476        case $.NOSCRIPT: {
2477            if (p.options.scriptingEnabled) {
2478                rawTextStartTagInBody(p, token);
2479            } else {
2480                genericStartTagInBody(p, token);
2481            }
2482            break;
2483        }
2484        case $.PLAINTEXT: {
2485            plaintextStartTagInBody(p, token);
2486            break;
2487        }
2488
2489        case $.COL:
2490        case $.TH:
2491        case $.TD:
2492        case $.TR:
2493        case $.HEAD:
2494        case $.FRAME:
2495        case $.TBODY:
2496        case $.TFOOT:
2497        case $.THEAD:
2498        case $.CAPTION:
2499        case $.COLGROUP: {
2500            // Ignore token
2501            break;
2502        }
2503        default: {
2504            genericStartTagInBody(p, token);
2505        }
2506    }
2507}
2508
2509function bodyEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2510    if (p.openElements.hasInScope($.BODY)) {
2511        p.insertionMode = InsertionMode.AFTER_BODY;
2512
2513        //NOTE: <body> is never popped from the stack, so we need to updated
2514        //the end location explicitly.
2515        if (p.options.sourceCodeLocationInfo) {
2516            const bodyElement = p.openElements.tryPeekProperlyNestedBodyElement();
2517            if (bodyElement) {
2518                p._setEndLocation(bodyElement, token);
2519            }
2520        }
2521    }
2522}
2523
2524function htmlEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2525    if (p.openElements.hasInScope($.BODY)) {
2526        p.insertionMode = InsertionMode.AFTER_BODY;
2527        endTagAfterBody(p, token);
2528    }
2529}
2530
2531function addressEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2532    const tn = token.tagID;
2533
2534    if (p.openElements.hasInScope(tn)) {
2535        p.openElements.generateImpliedEndTags();
2536        p.openElements.popUntilTagNamePopped(tn);
2537    }
2538}
2539
2540function formEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>): void {
2541    const inTemplate = p.openElements.tmplCount > 0;
2542    const { formElement } = p;
2543
2544    if (!inTemplate) {
2545        p.formElement = null;
2546    }
2547
2548    if ((formElement || inTemplate) && p.openElements.hasInScope($.FORM)) {
2549        p.openElements.generateImpliedEndTags();
2550
2551        if (inTemplate) {
2552            p.openElements.popUntilTagNamePopped($.FORM);
2553        } else if (formElement) {
2554            p.openElements.remove(formElement);
2555        }
2556    }
2557}
2558
2559function pEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>): void {
2560    if (!p.openElements.hasInButtonScope($.P)) {
2561        p._insertFakeElement(TN.P, $.P);
2562    }
2563
2564    p._closePElement();
2565}
2566
2567function liEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>): void {
2568    if (p.openElements.hasInListItemScope($.LI)) {
2569        p.openElements.generateImpliedEndTagsWithExclusion($.LI);
2570        p.openElements.popUntilTagNamePopped($.LI);
2571    }
2572}
2573
2574function ddEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2575    const tn = token.tagID;
2576
2577    if (p.openElements.hasInScope(tn)) {
2578        p.openElements.generateImpliedEndTagsWithExclusion(tn);
2579        p.openElements.popUntilTagNamePopped(tn);
2580    }
2581}
2582
2583function numberedHeaderEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>): void {
2584    if (p.openElements.hasNumberedHeaderInScope()) {
2585        p.openElements.generateImpliedEndTags();
2586        p.openElements.popUntilNumberedHeaderPopped();
2587    }
2588}
2589
2590function appletEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2591    const tn = token.tagID;
2592
2593    if (p.openElements.hasInScope(tn)) {
2594        p.openElements.generateImpliedEndTags();
2595        p.openElements.popUntilTagNamePopped(tn);
2596        p.activeFormattingElements.clearToLastMarker();
2597    }
2598}
2599
2600function brEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>): void {
2601    p._reconstructActiveFormattingElements();
2602    p._insertFakeElement(TN.BR, $.BR);
2603    p.openElements.pop();
2604    p.framesetOk = false;
2605}
2606
2607function genericEndTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2608    const tn = token.tagName;
2609    const tid = token.tagID;
2610
2611    for (let i = p.openElements.stackTop; i > 0; i--) {
2612        const element = p.openElements.items[i];
2613        const elementId = p.openElements.tagIDs[i];
2614
2615        // Compare the tag name here, as the tag might not be a known tag with an ID.
2616        if (tid === elementId && (tid !== $.UNKNOWN || p.treeAdapter.getTagName(element) === tn)) {
2617            p.openElements.generateImpliedEndTagsWithExclusion(tid);
2618            if (p.openElements.stackTop >= i) p.openElements.shortenToLength(i);
2619            break;
2620        }
2621
2622        if (p._isSpecialElement(element, elementId)) {
2623            break;
2624        }
2625    }
2626}
2627
2628function endTagInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2629    switch (token.tagID) {
2630        case $.A:
2631        case $.B:
2632        case $.I:
2633        case $.S:
2634        case $.U:
2635        case $.EM:
2636        case $.TT:
2637        case $.BIG:
2638        case $.CODE:
2639        case $.FONT:
2640        case $.NOBR:
2641        case $.SMALL:
2642        case $.STRIKE:
2643        case $.STRONG: {
2644            callAdoptionAgency(p, token);
2645            break;
2646        }
2647        case $.P: {
2648            pEndTagInBody(p);
2649            break;
2650        }
2651        case $.DL:
2652        case $.UL:
2653        case $.OL:
2654        case $.DIR:
2655        case $.DIV:
2656        case $.NAV:
2657        case $.PRE:
2658        case $.MAIN:
2659        case $.MENU:
2660        case $.ASIDE:
2661        case $.BUTTON:
2662        case $.CENTER:
2663        case $.FIGURE:
2664        case $.FOOTER:
2665        case $.HEADER:
2666        case $.HGROUP:
2667        case $.DIALOG:
2668        case $.ADDRESS:
2669        case $.ARTICLE:
2670        case $.DETAILS:
2671        case $.SEARCH:
2672        case $.SECTION:
2673        case $.SUMMARY:
2674        case $.LISTING:
2675        case $.FIELDSET:
2676        case $.BLOCKQUOTE:
2677        case $.FIGCAPTION: {
2678            addressEndTagInBody(p, token);
2679            break;
2680        }
2681        case $.LI: {
2682            liEndTagInBody(p);
2683            break;
2684        }
2685        case $.DD:
2686        case $.DT: {
2687            ddEndTagInBody(p, token);
2688            break;
2689        }
2690        case $.H1:
2691        case $.H2:
2692        case $.H3:
2693        case $.H4:
2694        case $.H5:
2695        case $.H6: {
2696            numberedHeaderEndTagInBody(p);
2697            break;
2698        }
2699        case $.BR: {
2700            brEndTagInBody(p);
2701            break;
2702        }
2703        case $.BODY: {
2704            bodyEndTagInBody(p, token);
2705            break;
2706        }
2707        case $.HTML: {
2708            htmlEndTagInBody(p, token);
2709            break;
2710        }
2711        case $.FORM: {
2712            formEndTagInBody(p);
2713            break;
2714        }
2715        case $.APPLET:
2716        case $.OBJECT:
2717        case $.MARQUEE: {
2718            appletEndTagInBody(p, token);
2719            break;
2720        }
2721        case $.TEMPLATE: {
2722            templateEndTagInHead(p, token);
2723            break;
2724        }
2725        default: {
2726            genericEndTagInBody(p, token);
2727        }
2728    }
2729}
2730
2731function eofInBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: EOFToken): void {
2732    if (p.tmplInsertionModeStack.length > 0) {
2733        eofInTemplate(p, token);
2734    } else {
2735        stopParsing(p, token);
2736    }
2737}
2738
2739// The "text" insertion mode
2740//------------------------------------------------------------------
2741function endTagInText<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2742    if (token.tagID === $.SCRIPT) {
2743        p.scriptHandler?.(p.openElements.current);
2744    }
2745
2746    p.openElements.pop();
2747    p.insertionMode = p.originalInsertionMode;
2748}
2749
2750function eofInText<T extends TreeAdapterTypeMap>(p: Parser<T>, token: EOFToken): void {
2751    p._err(token, ERR.eofInElementThatCanContainOnlyText);
2752    p.openElements.pop();
2753    p.insertionMode = p.originalInsertionMode;
2754    p.onEof(token);
2755}
2756
2757// The "in table" insertion mode
2758//------------------------------------------------------------------
2759function characterInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
2760    if (TABLE_STRUCTURE_TAGS.has(p.openElements.currentTagId)) {
2761        p.pendingCharacterTokens.length = 0;
2762        p.hasNonWhitespacePendingCharacterToken = false;
2763        p.originalInsertionMode = p.insertionMode;
2764        p.insertionMode = InsertionMode.IN_TABLE_TEXT;
2765
2766        switch (token.type) {
2767            case TokenType.CHARACTER: {
2768                characterInTableText(p, token);
2769                break;
2770            }
2771            case TokenType.WHITESPACE_CHARACTER: {
2772                whitespaceCharacterInTableText(p, token);
2773                break;
2774            }
2775            // Ignore null
2776        }
2777    } else {
2778        tokenInTable(p, token);
2779    }
2780}
2781
2782function captionStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2783    p.openElements.clearBackToTableContext();
2784    p.activeFormattingElements.insertMarker();
2785    p._insertElement(token, NS.HTML);
2786    p.insertionMode = InsertionMode.IN_CAPTION;
2787}
2788
2789function colgroupStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2790    p.openElements.clearBackToTableContext();
2791    p._insertElement(token, NS.HTML);
2792    p.insertionMode = InsertionMode.IN_COLUMN_GROUP;
2793}
2794
2795function colStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2796    p.openElements.clearBackToTableContext();
2797    p._insertFakeElement(TN.COLGROUP, $.COLGROUP);
2798    p.insertionMode = InsertionMode.IN_COLUMN_GROUP;
2799    startTagInColumnGroup(p, token);
2800}
2801
2802function tbodyStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2803    p.openElements.clearBackToTableContext();
2804    p._insertElement(token, NS.HTML);
2805    p.insertionMode = InsertionMode.IN_TABLE_BODY;
2806}
2807
2808function tdStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2809    p.openElements.clearBackToTableContext();
2810    p._insertFakeElement(TN.TBODY, $.TBODY);
2811    p.insertionMode = InsertionMode.IN_TABLE_BODY;
2812    startTagInTableBody(p, token);
2813}
2814
2815function tableStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2816    if (p.openElements.hasInTableScope($.TABLE)) {
2817        p.openElements.popUntilTagNamePopped($.TABLE);
2818        p._resetInsertionMode();
2819        p._processStartTag(token);
2820    }
2821}
2822
2823function inputStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2824    if (isHiddenInput(token)) {
2825        p._appendElement(token, NS.HTML);
2826    } else {
2827        tokenInTable(p, token);
2828    }
2829
2830    token.ackSelfClosing = true;
2831}
2832
2833function formStartTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2834    if (!p.formElement && p.openElements.tmplCount === 0) {
2835        p._insertElement(token, NS.HTML);
2836        p.formElement = p.openElements.current;
2837        p.openElements.pop();
2838    }
2839}
2840
2841function startTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2842    switch (token.tagID) {
2843        case $.TD:
2844        case $.TH:
2845        case $.TR: {
2846            tdStartTagInTable(p, token);
2847            break;
2848        }
2849        case $.STYLE:
2850        case $.SCRIPT:
2851        case $.TEMPLATE: {
2852            startTagInHead(p, token);
2853            break;
2854        }
2855        case $.COL: {
2856            colStartTagInTable(p, token);
2857            break;
2858        }
2859        case $.FORM: {
2860            formStartTagInTable(p, token);
2861            break;
2862        }
2863        case $.TABLE: {
2864            tableStartTagInTable(p, token);
2865            break;
2866        }
2867        case $.TBODY:
2868        case $.TFOOT:
2869        case $.THEAD: {
2870            tbodyStartTagInTable(p, token);
2871            break;
2872        }
2873        case $.INPUT: {
2874            inputStartTagInTable(p, token);
2875            break;
2876        }
2877        case $.CAPTION: {
2878            captionStartTagInTable(p, token);
2879            break;
2880        }
2881        case $.COLGROUP: {
2882            colgroupStartTagInTable(p, token);
2883            break;
2884        }
2885        default: {
2886            tokenInTable(p, token);
2887        }
2888    }
2889}
2890
2891function endTagInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2892    switch (token.tagID) {
2893        case $.TABLE: {
2894            if (p.openElements.hasInTableScope($.TABLE)) {
2895                p.openElements.popUntilTagNamePopped($.TABLE);
2896                p._resetInsertionMode();
2897            }
2898            break;
2899        }
2900        case $.TEMPLATE: {
2901            templateEndTagInHead(p, token);
2902            break;
2903        }
2904        case $.BODY:
2905        case $.CAPTION:
2906        case $.COL:
2907        case $.COLGROUP:
2908        case $.HTML:
2909        case $.TBODY:
2910        case $.TD:
2911        case $.TFOOT:
2912        case $.TH:
2913        case $.THEAD:
2914        case $.TR: {
2915            // Ignore token
2916            break;
2917        }
2918        default: {
2919            tokenInTable(p, token);
2920        }
2921    }
2922}
2923
2924function tokenInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
2925    const savedFosterParentingState = p.fosterParentingEnabled;
2926
2927    p.fosterParentingEnabled = true;
2928    // Process token in `In Body` mode
2929    modeInBody(p, token);
2930    p.fosterParentingEnabled = savedFosterParentingState;
2931}
2932
2933// The "in table text" insertion mode
2934//------------------------------------------------------------------
2935function whitespaceCharacterInTableText<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
2936    p.pendingCharacterTokens.push(token);
2937}
2938
2939function characterInTableText<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
2940    p.pendingCharacterTokens.push(token);
2941    p.hasNonWhitespacePendingCharacterToken = true;
2942}
2943
2944function tokenInTableText<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
2945    let i = 0;
2946
2947    if (p.hasNonWhitespacePendingCharacterToken) {
2948        for (; i < p.pendingCharacterTokens.length; i++) {
2949            tokenInTable(p, p.pendingCharacterTokens[i]);
2950        }
2951    } else {
2952        for (; i < p.pendingCharacterTokens.length; i++) {
2953            p._insertCharacters(p.pendingCharacterTokens[i]);
2954        }
2955    }
2956
2957    p.insertionMode = p.originalInsertionMode;
2958    p._processToken(token);
2959}
2960
2961// The "in caption" insertion mode
2962//------------------------------------------------------------------
2963const TABLE_VOID_ELEMENTS = new Set([$.CAPTION, $.COL, $.COLGROUP, $.TBODY, $.TD, $.TFOOT, $.TH, $.THEAD, $.TR]);
2964
2965function startTagInCaption<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2966    const tn = token.tagID;
2967
2968    if (TABLE_VOID_ELEMENTS.has(tn)) {
2969        if (p.openElements.hasInTableScope($.CAPTION)) {
2970            p.openElements.generateImpliedEndTags();
2971            p.openElements.popUntilTagNamePopped($.CAPTION);
2972            p.activeFormattingElements.clearToLastMarker();
2973            p.insertionMode = InsertionMode.IN_TABLE;
2974            startTagInTable(p, token);
2975        }
2976    } else {
2977        startTagInBody(p, token);
2978    }
2979}
2980
2981function endTagInCaption<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
2982    const tn = token.tagID;
2983
2984    switch (tn) {
2985        case $.CAPTION:
2986        case $.TABLE: {
2987            if (p.openElements.hasInTableScope($.CAPTION)) {
2988                p.openElements.generateImpliedEndTags();
2989                p.openElements.popUntilTagNamePopped($.CAPTION);
2990                p.activeFormattingElements.clearToLastMarker();
2991                p.insertionMode = InsertionMode.IN_TABLE;
2992
2993                if (tn === $.TABLE) {
2994                    endTagInTable(p, token);
2995                }
2996            }
2997            break;
2998        }
2999        case $.BODY:
3000        case $.COL:
3001        case $.COLGROUP:
3002        case $.HTML:
3003        case $.TBODY:
3004        case $.TD:
3005        case $.TFOOT:
3006        case $.TH:
3007        case $.THEAD:
3008        case $.TR: {
3009            // Ignore token
3010            break;
3011        }
3012        default: {
3013            endTagInBody(p, token);
3014        }
3015    }
3016}
3017
3018// The "in column group" insertion mode
3019//------------------------------------------------------------------
3020function startTagInColumnGroup<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3021    switch (token.tagID) {
3022        case $.HTML: {
3023            startTagInBody(p, token);
3024            break;
3025        }
3026        case $.COL: {
3027            p._appendElement(token, NS.HTML);
3028            token.ackSelfClosing = true;
3029            break;
3030        }
3031        case $.TEMPLATE: {
3032            startTagInHead(p, token);
3033            break;
3034        }
3035        default: {
3036            tokenInColumnGroup(p, token);
3037        }
3038    }
3039}
3040
3041function endTagInColumnGroup<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3042    switch (token.tagID) {
3043        case $.COLGROUP: {
3044            if (p.openElements.currentTagId === $.COLGROUP) {
3045                p.openElements.pop();
3046                p.insertionMode = InsertionMode.IN_TABLE;
3047            }
3048            break;
3049        }
3050        case $.TEMPLATE: {
3051            templateEndTagInHead(p, token);
3052            break;
3053        }
3054        case $.COL: {
3055            // Ignore token
3056            break;
3057        }
3058        default: {
3059            tokenInColumnGroup(p, token);
3060        }
3061    }
3062}
3063
3064function tokenInColumnGroup<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
3065    if (p.openElements.currentTagId === $.COLGROUP) {
3066        p.openElements.pop();
3067        p.insertionMode = InsertionMode.IN_TABLE;
3068        p._processToken(token);
3069    }
3070}
3071
3072// The "in table body" insertion mode
3073//------------------------------------------------------------------
3074function startTagInTableBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3075    switch (token.tagID) {
3076        case $.TR: {
3077            p.openElements.clearBackToTableBodyContext();
3078            p._insertElement(token, NS.HTML);
3079            p.insertionMode = InsertionMode.IN_ROW;
3080            break;
3081        }
3082        case $.TH:
3083        case $.TD: {
3084            p.openElements.clearBackToTableBodyContext();
3085            p._insertFakeElement(TN.TR, $.TR);
3086            p.insertionMode = InsertionMode.IN_ROW;
3087            startTagInRow(p, token);
3088            break;
3089        }
3090        case $.CAPTION:
3091        case $.COL:
3092        case $.COLGROUP:
3093        case $.TBODY:
3094        case $.TFOOT:
3095        case $.THEAD: {
3096            if (p.openElements.hasTableBodyContextInTableScope()) {
3097                p.openElements.clearBackToTableBodyContext();
3098                p.openElements.pop();
3099                p.insertionMode = InsertionMode.IN_TABLE;
3100                startTagInTable(p, token);
3101            }
3102            break;
3103        }
3104        default: {
3105            startTagInTable(p, token);
3106        }
3107    }
3108}
3109
3110function endTagInTableBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3111    const tn = token.tagID;
3112
3113    switch (token.tagID) {
3114        case $.TBODY:
3115        case $.TFOOT:
3116        case $.THEAD: {
3117            if (p.openElements.hasInTableScope(tn)) {
3118                p.openElements.clearBackToTableBodyContext();
3119                p.openElements.pop();
3120                p.insertionMode = InsertionMode.IN_TABLE;
3121            }
3122            break;
3123        }
3124        case $.TABLE: {
3125            if (p.openElements.hasTableBodyContextInTableScope()) {
3126                p.openElements.clearBackToTableBodyContext();
3127                p.openElements.pop();
3128                p.insertionMode = InsertionMode.IN_TABLE;
3129                endTagInTable(p, token);
3130            }
3131            break;
3132        }
3133        case $.BODY:
3134        case $.CAPTION:
3135        case $.COL:
3136        case $.COLGROUP:
3137        case $.HTML:
3138        case $.TD:
3139        case $.TH:
3140        case $.TR: {
3141            // Ignore token
3142            break;
3143        }
3144        default: {
3145            endTagInTable(p, token);
3146        }
3147    }
3148}
3149
3150// The "in row" insertion mode
3151//------------------------------------------------------------------
3152function startTagInRow<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3153    switch (token.tagID) {
3154        case $.TH:
3155        case $.TD: {
3156            p.openElements.clearBackToTableRowContext();
3157            p._insertElement(token, NS.HTML);
3158            p.insertionMode = InsertionMode.IN_CELL;
3159            p.activeFormattingElements.insertMarker();
3160            break;
3161        }
3162        case $.CAPTION:
3163        case $.COL:
3164        case $.COLGROUP:
3165        case $.TBODY:
3166        case $.TFOOT:
3167        case $.THEAD:
3168        case $.TR: {
3169            if (p.openElements.hasInTableScope($.TR)) {
3170                p.openElements.clearBackToTableRowContext();
3171                p.openElements.pop();
3172                p.insertionMode = InsertionMode.IN_TABLE_BODY;
3173                startTagInTableBody(p, token);
3174            }
3175            break;
3176        }
3177        default: {
3178            startTagInTable(p, token);
3179        }
3180    }
3181}
3182
3183function endTagInRow<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3184    switch (token.tagID) {
3185        case $.TR: {
3186            if (p.openElements.hasInTableScope($.TR)) {
3187                p.openElements.clearBackToTableRowContext();
3188                p.openElements.pop();
3189                p.insertionMode = InsertionMode.IN_TABLE_BODY;
3190            }
3191            break;
3192        }
3193        case $.TABLE: {
3194            if (p.openElements.hasInTableScope($.TR)) {
3195                p.openElements.clearBackToTableRowContext();
3196                p.openElements.pop();
3197                p.insertionMode = InsertionMode.IN_TABLE_BODY;
3198                endTagInTableBody(p, token);
3199            }
3200            break;
3201        }
3202        case $.TBODY:
3203        case $.TFOOT:
3204        case $.THEAD: {
3205            if (p.openElements.hasInTableScope(token.tagID) || p.openElements.hasInTableScope($.TR)) {
3206                p.openElements.clearBackToTableRowContext();
3207                p.openElements.pop();
3208                p.insertionMode = InsertionMode.IN_TABLE_BODY;
3209                endTagInTableBody(p, token);
3210            }
3211            break;
3212        }
3213        case $.BODY:
3214        case $.CAPTION:
3215        case $.COL:
3216        case $.COLGROUP:
3217        case $.HTML:
3218        case $.TD:
3219        case $.TH: {
3220            // Ignore end tag
3221            break;
3222        }
3223        default: {
3224            endTagInTable(p, token);
3225        }
3226    }
3227}
3228
3229// The "in cell" insertion mode
3230//------------------------------------------------------------------
3231function startTagInCell<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3232    const tn = token.tagID;
3233
3234    if (TABLE_VOID_ELEMENTS.has(tn)) {
3235        if (p.openElements.hasInTableScope($.TD) || p.openElements.hasInTableScope($.TH)) {
3236            p._closeTableCell();
3237            startTagInRow(p, token);
3238        }
3239    } else {
3240        startTagInBody(p, token);
3241    }
3242}
3243
3244function endTagInCell<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3245    const tn = token.tagID;
3246
3247    switch (tn) {
3248        case $.TD:
3249        case $.TH: {
3250            if (p.openElements.hasInTableScope(tn)) {
3251                p.openElements.generateImpliedEndTags();
3252                p.openElements.popUntilTagNamePopped(tn);
3253                p.activeFormattingElements.clearToLastMarker();
3254                p.insertionMode = InsertionMode.IN_ROW;
3255            }
3256            break;
3257        }
3258        case $.TABLE:
3259        case $.TBODY:
3260        case $.TFOOT:
3261        case $.THEAD:
3262        case $.TR: {
3263            if (p.openElements.hasInTableScope(tn)) {
3264                p._closeTableCell();
3265                endTagInRow(p, token);
3266            }
3267            break;
3268        }
3269        case $.BODY:
3270        case $.CAPTION:
3271        case $.COL:
3272        case $.COLGROUP:
3273        case $.HTML: {
3274            // Ignore token
3275            break;
3276        }
3277        default: {
3278            endTagInBody(p, token);
3279        }
3280    }
3281}
3282
3283// The "in select" insertion mode
3284//------------------------------------------------------------------
3285function startTagInSelect<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3286    switch (token.tagID) {
3287        case $.HTML: {
3288            startTagInBody(p, token);
3289            break;
3290        }
3291        case $.OPTION: {
3292            if (p.openElements.currentTagId === $.OPTION) {
3293                p.openElements.pop();
3294            }
3295
3296            p._insertElement(token, NS.HTML);
3297            break;
3298        }
3299        case $.OPTGROUP: {
3300            if (p.openElements.currentTagId === $.OPTION) {
3301                p.openElements.pop();
3302            }
3303
3304            if (p.openElements.currentTagId === $.OPTGROUP) {
3305                p.openElements.pop();
3306            }
3307
3308            p._insertElement(token, NS.HTML);
3309            break;
3310        }
3311        case $.HR: {
3312            if (p.openElements.currentTagId === $.OPTION) {
3313                p.openElements.pop();
3314            }
3315
3316            if (p.openElements.currentTagId === $.OPTGROUP) {
3317                p.openElements.pop();
3318            }
3319
3320            p._appendElement(token, NS.HTML);
3321            token.ackSelfClosing = true;
3322            break;
3323        }
3324        case $.INPUT:
3325        case $.KEYGEN:
3326        case $.TEXTAREA:
3327        case $.SELECT: {
3328            if (p.openElements.hasInSelectScope($.SELECT)) {
3329                p.openElements.popUntilTagNamePopped($.SELECT);
3330                p._resetInsertionMode();
3331
3332                if (token.tagID !== $.SELECT) {
3333                    p._processStartTag(token);
3334                }
3335            }
3336            break;
3337        }
3338        case $.SCRIPT:
3339        case $.TEMPLATE: {
3340            startTagInHead(p, token);
3341            break;
3342        }
3343        default:
3344        // Do nothing
3345    }
3346}
3347
3348function endTagInSelect<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3349    switch (token.tagID) {
3350        case $.OPTGROUP: {
3351            if (
3352                p.openElements.stackTop > 0 &&
3353                p.openElements.currentTagId === $.OPTION &&
3354                p.openElements.tagIDs[p.openElements.stackTop - 1] === $.OPTGROUP
3355            ) {
3356                p.openElements.pop();
3357            }
3358
3359            if (p.openElements.currentTagId === $.OPTGROUP) {
3360                p.openElements.pop();
3361            }
3362            break;
3363        }
3364        case $.OPTION: {
3365            if (p.openElements.currentTagId === $.OPTION) {
3366                p.openElements.pop();
3367            }
3368            break;
3369        }
3370        case $.SELECT: {
3371            if (p.openElements.hasInSelectScope($.SELECT)) {
3372                p.openElements.popUntilTagNamePopped($.SELECT);
3373                p._resetInsertionMode();
3374            }
3375            break;
3376        }
3377        case $.TEMPLATE: {
3378            templateEndTagInHead(p, token);
3379            break;
3380        }
3381        default:
3382        // Do nothing
3383    }
3384}
3385
3386// The "in select in table" insertion mode
3387//------------------------------------------------------------------
3388function startTagInSelectInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3389    const tn = token.tagID;
3390
3391    if (
3392        tn === $.CAPTION ||
3393        tn === $.TABLE ||
3394        tn === $.TBODY ||
3395        tn === $.TFOOT ||
3396        tn === $.THEAD ||
3397        tn === $.TR ||
3398        tn === $.TD ||
3399        tn === $.TH
3400    ) {
3401        p.openElements.popUntilTagNamePopped($.SELECT);
3402        p._resetInsertionMode();
3403        p._processStartTag(token);
3404    } else {
3405        startTagInSelect(p, token);
3406    }
3407}
3408
3409function endTagInSelectInTable<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3410    const tn = token.tagID;
3411
3412    if (
3413        tn === $.CAPTION ||
3414        tn === $.TABLE ||
3415        tn === $.TBODY ||
3416        tn === $.TFOOT ||
3417        tn === $.THEAD ||
3418        tn === $.TR ||
3419        tn === $.TD ||
3420        tn === $.TH
3421    ) {
3422        if (p.openElements.hasInTableScope(tn)) {
3423            p.openElements.popUntilTagNamePopped($.SELECT);
3424            p._resetInsertionMode();
3425            p.onEndTag(token);
3426        }
3427    } else {
3428        endTagInSelect(p, token);
3429    }
3430}
3431
3432// The "in template" insertion mode
3433//------------------------------------------------------------------
3434function startTagInTemplate<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3435    switch (token.tagID) {
3436        // First, handle tags that can start without a mode change
3437        case $.BASE:
3438        case $.BASEFONT:
3439        case $.BGSOUND:
3440        case $.LINK:
3441        case $.META:
3442        case $.NOFRAMES:
3443        case $.SCRIPT:
3444        case $.STYLE:
3445        case $.TEMPLATE:
3446        case $.TITLE: {
3447            startTagInHead(p, token);
3448            break;
3449        }
3450
3451        // Re-process the token in the appropriate mode
3452        case $.CAPTION:
3453        case $.COLGROUP:
3454        case $.TBODY:
3455        case $.TFOOT:
3456        case $.THEAD: {
3457            p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE;
3458            p.insertionMode = InsertionMode.IN_TABLE;
3459            startTagInTable(p, token);
3460            break;
3461        }
3462        case $.COL: {
3463            p.tmplInsertionModeStack[0] = InsertionMode.IN_COLUMN_GROUP;
3464            p.insertionMode = InsertionMode.IN_COLUMN_GROUP;
3465            startTagInColumnGroup(p, token);
3466            break;
3467        }
3468        case $.TR: {
3469            p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE_BODY;
3470            p.insertionMode = InsertionMode.IN_TABLE_BODY;
3471            startTagInTableBody(p, token);
3472            break;
3473        }
3474        case $.TD:
3475        case $.TH: {
3476            p.tmplInsertionModeStack[0] = InsertionMode.IN_ROW;
3477            p.insertionMode = InsertionMode.IN_ROW;
3478            startTagInRow(p, token);
3479            break;
3480        }
3481        default: {
3482            p.tmplInsertionModeStack[0] = InsertionMode.IN_BODY;
3483            p.insertionMode = InsertionMode.IN_BODY;
3484            startTagInBody(p, token);
3485        }
3486    }
3487}
3488
3489function endTagInTemplate<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3490    if (token.tagID === $.TEMPLATE) {
3491        templateEndTagInHead(p, token);
3492    }
3493}
3494
3495function eofInTemplate<T extends TreeAdapterTypeMap>(p: Parser<T>, token: EOFToken): void {
3496    if (p.openElements.tmplCount > 0) {
3497        p.openElements.popUntilTagNamePopped($.TEMPLATE);
3498        p.activeFormattingElements.clearToLastMarker();
3499        p.tmplInsertionModeStack.shift();
3500        p._resetInsertionMode();
3501        p.onEof(token);
3502    } else {
3503        stopParsing(p, token);
3504    }
3505}
3506
3507// The "after body" insertion mode
3508//------------------------------------------------------------------
3509function startTagAfterBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3510    if (token.tagID === $.HTML) {
3511        startTagInBody(p, token);
3512    } else {
3513        tokenAfterBody(p, token);
3514    }
3515}
3516
3517function endTagAfterBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3518    if (token.tagID === $.HTML) {
3519        if (!p.fragmentContext) {
3520            p.insertionMode = InsertionMode.AFTER_AFTER_BODY;
3521        }
3522
3523        //NOTE: <html> is never popped from the stack, so we need to updated
3524        //the end location explicitly.
3525        if (p.options.sourceCodeLocationInfo && p.openElements.tagIDs[0] === $.HTML) {
3526            p._setEndLocation(p.openElements.items[0], token);
3527
3528            // Update the body element, if it doesn't have an end tag
3529            const bodyElement = p.openElements.items[1];
3530            if (bodyElement && !p.treeAdapter.getNodeSourceCodeLocation(bodyElement)?.endTag) {
3531                p._setEndLocation(bodyElement, token);
3532            }
3533        }
3534    } else {
3535        tokenAfterBody(p, token);
3536    }
3537}
3538
3539function tokenAfterBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
3540    p.insertionMode = InsertionMode.IN_BODY;
3541    modeInBody(p, token);
3542}
3543
3544// The "in frameset" insertion mode
3545//------------------------------------------------------------------
3546function startTagInFrameset<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3547    switch (token.tagID) {
3548        case $.HTML: {
3549            startTagInBody(p, token);
3550            break;
3551        }
3552        case $.FRAMESET: {
3553            p._insertElement(token, NS.HTML);
3554            break;
3555        }
3556        case $.FRAME: {
3557            p._appendElement(token, NS.HTML);
3558            token.ackSelfClosing = true;
3559            break;
3560        }
3561        case $.NOFRAMES: {
3562            startTagInHead(p, token);
3563            break;
3564        }
3565        default:
3566        // Do nothing
3567    }
3568}
3569
3570function endTagInFrameset<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3571    if (token.tagID === $.FRAMESET && !p.openElements.isRootHtmlElementCurrent()) {
3572        p.openElements.pop();
3573
3574        if (!p.fragmentContext && p.openElements.currentTagId !== $.FRAMESET) {
3575            p.insertionMode = InsertionMode.AFTER_FRAMESET;
3576        }
3577    }
3578}
3579
3580// The "after frameset" insertion mode
3581//------------------------------------------------------------------
3582function startTagAfterFrameset<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3583    switch (token.tagID) {
3584        case $.HTML: {
3585            startTagInBody(p, token);
3586            break;
3587        }
3588        case $.NOFRAMES: {
3589            startTagInHead(p, token);
3590            break;
3591        }
3592        default:
3593        // Do nothing
3594    }
3595}
3596
3597function endTagAfterFrameset<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3598    if (token.tagID === $.HTML) {
3599        p.insertionMode = InsertionMode.AFTER_AFTER_FRAMESET;
3600    }
3601}
3602
3603// The "after after body" insertion mode
3604//------------------------------------------------------------------
3605function startTagAfterAfterBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3606    if (token.tagID === $.HTML) {
3607        startTagInBody(p, token);
3608    } else {
3609        tokenAfterAfterBody(p, token);
3610    }
3611}
3612
3613function tokenAfterAfterBody<T extends TreeAdapterTypeMap>(p: Parser<T>, token: Token): void {
3614    p.insertionMode = InsertionMode.IN_BODY;
3615    modeInBody(p, token);
3616}
3617
3618// The "after after frameset" insertion mode
3619//------------------------------------------------------------------
3620function startTagAfterAfterFrameset<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3621    switch (token.tagID) {
3622        case $.HTML: {
3623            startTagInBody(p, token);
3624            break;
3625        }
3626        case $.NOFRAMES: {
3627            startTagInHead(p, token);
3628            break;
3629        }
3630        default:
3631        // Do nothing
3632    }
3633}
3634
3635// The rules for parsing tokens in foreign content
3636//------------------------------------------------------------------
3637function nullCharacterInForeignContent<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
3638    token.chars = unicode.REPLACEMENT_CHARACTER;
3639    p._insertCharacters(token);
3640}
3641
3642function characterInForeignContent<T extends TreeAdapterTypeMap>(p: Parser<T>, token: CharacterToken): void {
3643    p._insertCharacters(token);
3644    p.framesetOk = false;
3645}
3646
3647function popUntilHtmlOrIntegrationPoint<T extends TreeAdapterTypeMap>(p: Parser<T>): void {
3648    while (
3649        p.treeAdapter.getNamespaceURI(p.openElements.current) !== NS.HTML &&
3650        !p._isIntegrationPoint(p.openElements.currentTagId, p.openElements.current)
3651    ) {
3652        p.openElements.pop();
3653    }
3654}
3655
3656function startTagInForeignContent<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3657    if (foreignContent.causesExit(token)) {
3658        popUntilHtmlOrIntegrationPoint(p);
3659
3660        p._startTagOutsideForeignContent(token);
3661    } else {
3662        const current = p._getAdjustedCurrentElement();
3663        const currentNs = p.treeAdapter.getNamespaceURI(current);
3664
3665        if (token.selfClosing) {
3666            p._appendElement(token, currentNs);
3667        } else {
3668            p._insertElement(token, currentNs);
3669        }
3670
3671        token.ackSelfClosing = true;
3672    }
3673}
3674
3675function endTagInForeignContent<T extends TreeAdapterTypeMap>(p: Parser<T>, token: TagToken): void {
3676    if (token.tagID === $.P || token.tagID === $.BR) {
3677        popUntilHtmlOrIntegrationPoint(p);
3678
3679        p._endTagOutsideForeignContent(token);
3680
3681        return;
3682    }
3683    for (let i = p.openElements.stackTop; i > 0; i--) {
3684        const element = p.openElements.items[i];
3685
3686        if (p.treeAdapter.getNamespaceURI(element) === NS.HTML) {
3687            p._endTagOutsideForeignContent(token);
3688            break;
3689        }
3690
3691        const tagName = p.treeAdapter.getTagName(element);
3692
3693        if (tagName.toLowerCase() === token.tagName) {
3694            //NOTE: update the token tag name for `_setEndLocation`.
3695            token.tagName = tagName;
3696            p.openElements.shortenToLength(i);
3697            break;
3698        }
3699    }
3700}
3701