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