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