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