Spaces:
Paused
Paused
| import { Tokenizer, TokenizerMode } from '../tokenizer/index.js'; | |
| import { OpenElementStack } from './open-element-stack.js'; | |
| import { FormattingElementList, EntryType } from './formatting-element-list.js'; | |
| import { defaultTreeAdapter } from '../tree-adapters/default.js'; | |
| import * as doctype from '../common/doctype.js'; | |
| import * as foreignContent from '../common/foreign-content.js'; | |
| import { ERR } from '../common/error-codes.js'; | |
| import * as unicode from '../common/unicode.js'; | |
| import { TAG_ID as $, TAG_NAMES as TN, NS, ATTRS, SPECIAL_ELEMENTS, DOCUMENT_MODE, NUMBERED_HEADERS, getTagID, } from '../common/html.js'; | |
| import { TokenType, getTokenAttr, } from '../common/token.js'; | |
| //Misc constants | |
| const HIDDEN_INPUT_TYPE = 'hidden'; | |
| //Adoption agency loops iteration count | |
| const AA_OUTER_LOOP_ITER = 8; | |
| const AA_INNER_LOOP_ITER = 3; | |
| //Insertion modes | |
| var InsertionMode; | |
| (function (InsertionMode) { | |
| InsertionMode[InsertionMode["INITIAL"] = 0] = "INITIAL"; | |
| InsertionMode[InsertionMode["BEFORE_HTML"] = 1] = "BEFORE_HTML"; | |
| InsertionMode[InsertionMode["BEFORE_HEAD"] = 2] = "BEFORE_HEAD"; | |
| InsertionMode[InsertionMode["IN_HEAD"] = 3] = "IN_HEAD"; | |
| InsertionMode[InsertionMode["IN_HEAD_NO_SCRIPT"] = 4] = "IN_HEAD_NO_SCRIPT"; | |
| InsertionMode[InsertionMode["AFTER_HEAD"] = 5] = "AFTER_HEAD"; | |
| InsertionMode[InsertionMode["IN_BODY"] = 6] = "IN_BODY"; | |
| InsertionMode[InsertionMode["TEXT"] = 7] = "TEXT"; | |
| InsertionMode[InsertionMode["IN_TABLE"] = 8] = "IN_TABLE"; | |
| InsertionMode[InsertionMode["IN_TABLE_TEXT"] = 9] = "IN_TABLE_TEXT"; | |
| InsertionMode[InsertionMode["IN_CAPTION"] = 10] = "IN_CAPTION"; | |
| InsertionMode[InsertionMode["IN_COLUMN_GROUP"] = 11] = "IN_COLUMN_GROUP"; | |
| InsertionMode[InsertionMode["IN_TABLE_BODY"] = 12] = "IN_TABLE_BODY"; | |
| InsertionMode[InsertionMode["IN_ROW"] = 13] = "IN_ROW"; | |
| InsertionMode[InsertionMode["IN_CELL"] = 14] = "IN_CELL"; | |
| InsertionMode[InsertionMode["IN_SELECT"] = 15] = "IN_SELECT"; | |
| InsertionMode[InsertionMode["IN_SELECT_IN_TABLE"] = 16] = "IN_SELECT_IN_TABLE"; | |
| InsertionMode[InsertionMode["IN_TEMPLATE"] = 17] = "IN_TEMPLATE"; | |
| InsertionMode[InsertionMode["AFTER_BODY"] = 18] = "AFTER_BODY"; | |
| InsertionMode[InsertionMode["IN_FRAMESET"] = 19] = "IN_FRAMESET"; | |
| InsertionMode[InsertionMode["AFTER_FRAMESET"] = 20] = "AFTER_FRAMESET"; | |
| InsertionMode[InsertionMode["AFTER_AFTER_BODY"] = 21] = "AFTER_AFTER_BODY"; | |
| InsertionMode[InsertionMode["AFTER_AFTER_FRAMESET"] = 22] = "AFTER_AFTER_FRAMESET"; | |
| })(InsertionMode || (InsertionMode = {})); | |
| const BASE_LOC = { | |
| startLine: -1, | |
| startCol: -1, | |
| startOffset: -1, | |
| endLine: -1, | |
| endCol: -1, | |
| endOffset: -1, | |
| }; | |
| const TABLE_STRUCTURE_TAGS = new Set([$.TABLE, $.TBODY, $.TFOOT, $.THEAD, $.TR]); | |
| const defaultParserOptions = { | |
| scriptingEnabled: true, | |
| sourceCodeLocationInfo: false, | |
| treeAdapter: defaultTreeAdapter, | |
| onParseError: null, | |
| }; | |
| //Parser | |
| export class Parser { | |
| constructor(options, document, | |
| /** @internal */ | |
| fragmentContext = null, | |
| /** @internal */ | |
| scriptHandler = null) { | |
| this.fragmentContext = fragmentContext; | |
| this.scriptHandler = scriptHandler; | |
| this.currentToken = null; | |
| this.stopped = false; | |
| /** @internal */ | |
| this.insertionMode = InsertionMode.INITIAL; | |
| /** @internal */ | |
| this.originalInsertionMode = InsertionMode.INITIAL; | |
| /** @internal */ | |
| this.headElement = null; | |
| /** @internal */ | |
| this.formElement = null; | |
| /** Indicates that the current node is not an element in the HTML namespace */ | |
| this.currentNotInHTML = false; | |
| /** | |
| * The template insertion mode stack is maintained from the left. | |
| * Ie. the topmost element will always have index 0. | |
| * | |
| * @internal | |
| */ | |
| this.tmplInsertionModeStack = []; | |
| /** @internal */ | |
| this.pendingCharacterTokens = []; | |
| /** @internal */ | |
| this.hasNonWhitespacePendingCharacterToken = false; | |
| /** @internal */ | |
| this.framesetOk = true; | |
| /** @internal */ | |
| this.skipNextNewLine = false; | |
| /** @internal */ | |
| this.fosterParentingEnabled = false; | |
| this.options = { | |
| ...defaultParserOptions, | |
| ...options, | |
| }; | |
| this.treeAdapter = this.options.treeAdapter; | |
| this.onParseError = this.options.onParseError; | |
| // Always enable location info if we report parse errors. | |
| if (this.onParseError) { | |
| this.options.sourceCodeLocationInfo = true; | |
| } | |
| this.document = document !== null && document !== void 0 ? document : this.treeAdapter.createDocument(); | |
| this.tokenizer = new Tokenizer(this.options, this); | |
| this.activeFormattingElements = new FormattingElementList(this.treeAdapter); | |
| this.fragmentContextID = fragmentContext ? getTagID(this.treeAdapter.getTagName(fragmentContext)) : $.UNKNOWN; | |
| this._setContextModes(fragmentContext !== null && fragmentContext !== void 0 ? fragmentContext : this.document, this.fragmentContextID); | |
| this.openElements = new OpenElementStack(this.document, this.treeAdapter, this); | |
| } | |
| // API | |
| static parse(html, options) { | |
| const parser = new this(options); | |
| parser.tokenizer.write(html, true); | |
| return parser.document; | |
| } | |
| static getFragmentParser(fragmentContext, options) { | |
| const opts = { | |
| ...defaultParserOptions, | |
| ...options, | |
| }; | |
| //NOTE: use a <template> element as the fragment context if no context element was provided, | |
| //so we will parse in a "forgiving" manner | |
| fragmentContext !== null && fragmentContext !== void 0 ? fragmentContext : (fragmentContext = opts.treeAdapter.createElement(TN.TEMPLATE, NS.HTML, [])); | |
| //NOTE: create a fake element which will be used as the `document` for fragment parsing. | |
| //This is important for jsdom, where a new `document` cannot be created. This led to | |
| //fragment parsing messing with the main `document`. | |
| const documentMock = opts.treeAdapter.createElement('documentmock', NS.HTML, []); | |
| const parser = new this(opts, documentMock, fragmentContext); | |
| if (parser.fragmentContextID === $.TEMPLATE) { | |
| parser.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE); | |
| } | |
| parser._initTokenizerForFragmentParsing(); | |
| parser._insertFakeRootElement(); | |
| parser._resetInsertionMode(); | |
| parser._findFormInFragmentContext(); | |
| return parser; | |
| } | |
| getFragment() { | |
| const rootElement = this.treeAdapter.getFirstChild(this.document); | |
| const fragment = this.treeAdapter.createDocumentFragment(); | |
| this._adoptNodes(rootElement, fragment); | |
| return fragment; | |
| } | |
| //Errors | |
| /** @internal */ | |
| _err(token, code, beforeToken) { | |
| var _a; | |
| if (!this.onParseError) | |
| return; | |
| const loc = (_a = token.location) !== null && _a !== void 0 ? _a : BASE_LOC; | |
| const err = { | |
| code, | |
| startLine: loc.startLine, | |
| startCol: loc.startCol, | |
| startOffset: loc.startOffset, | |
| endLine: beforeToken ? loc.startLine : loc.endLine, | |
| endCol: beforeToken ? loc.startCol : loc.endCol, | |
| endOffset: beforeToken ? loc.startOffset : loc.endOffset, | |
| }; | |
| this.onParseError(err); | |
| } | |
| //Stack events | |
| /** @internal */ | |
| onItemPush(node, tid, isTop) { | |
| var _a, _b; | |
| (_b = (_a = this.treeAdapter).onItemPush) === null || _b === void 0 ? void 0 : _b.call(_a, node); | |
| if (isTop && this.openElements.stackTop > 0) | |
| this._setContextModes(node, tid); | |
| } | |
| /** @internal */ | |
| onItemPop(node, isTop) { | |
| var _a, _b; | |
| if (this.options.sourceCodeLocationInfo) { | |
| this._setEndLocation(node, this.currentToken); | |
| } | |
| (_b = (_a = this.treeAdapter).onItemPop) === null || _b === void 0 ? void 0 : _b.call(_a, node, this.openElements.current); | |
| if (isTop) { | |
| let current; | |
| let currentTagId; | |
| if (this.openElements.stackTop === 0 && this.fragmentContext) { | |
| current = this.fragmentContext; | |
| currentTagId = this.fragmentContextID; | |
| } | |
| else { | |
| ({ current, currentTagId } = this.openElements); | |
| } | |
| this._setContextModes(current, currentTagId); | |
| } | |
| } | |
| _setContextModes(current, tid) { | |
| const isHTML = current === this.document || (current && this.treeAdapter.getNamespaceURI(current) === NS.HTML); | |
| this.currentNotInHTML = !isHTML; | |
| this.tokenizer.inForeignNode = | |
| !isHTML && current !== undefined && tid !== undefined && !this._isIntegrationPoint(tid, current); | |
| } | |
| /** @protected */ | |
| _switchToTextParsing(currentToken, nextTokenizerState) { | |
| this._insertElement(currentToken, NS.HTML); | |
| this.tokenizer.state = nextTokenizerState; | |
| this.originalInsertionMode = this.insertionMode; | |
| this.insertionMode = InsertionMode.TEXT; | |
| } | |
| switchToPlaintextParsing() { | |
| this.insertionMode = InsertionMode.TEXT; | |
| this.originalInsertionMode = InsertionMode.IN_BODY; | |
| this.tokenizer.state = TokenizerMode.PLAINTEXT; | |
| } | |
| //Fragment parsing | |
| /** @protected */ | |
| _getAdjustedCurrentElement() { | |
| return this.openElements.stackTop === 0 && this.fragmentContext | |
| ? this.fragmentContext | |
| : this.openElements.current; | |
| } | |
| /** @protected */ | |
| _findFormInFragmentContext() { | |
| let node = this.fragmentContext; | |
| while (node) { | |
| if (this.treeAdapter.getTagName(node) === TN.FORM) { | |
| this.formElement = node; | |
| break; | |
| } | |
| node = this.treeAdapter.getParentNode(node); | |
| } | |
| } | |
| _initTokenizerForFragmentParsing() { | |
| if (!this.fragmentContext || this.treeAdapter.getNamespaceURI(this.fragmentContext) !== NS.HTML) { | |
| return; | |
| } | |
| switch (this.fragmentContextID) { | |
| case $.TITLE: | |
| case $.TEXTAREA: { | |
| this.tokenizer.state = TokenizerMode.RCDATA; | |
| break; | |
| } | |
| case $.STYLE: | |
| case $.XMP: | |
| case $.IFRAME: | |
| case $.NOEMBED: | |
| case $.NOFRAMES: | |
| case $.NOSCRIPT: { | |
| this.tokenizer.state = TokenizerMode.RAWTEXT; | |
| break; | |
| } | |
| case $.SCRIPT: { | |
| this.tokenizer.state = TokenizerMode.SCRIPT_DATA; | |
| break; | |
| } | |
| case $.PLAINTEXT: { | |
| this.tokenizer.state = TokenizerMode.PLAINTEXT; | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| //Tree mutation | |
| /** @protected */ | |
| _setDocumentType(token) { | |
| const name = token.name || ''; | |
| const publicId = token.publicId || ''; | |
| const systemId = token.systemId || ''; | |
| this.treeAdapter.setDocumentType(this.document, name, publicId, systemId); | |
| if (token.location) { | |
| const documentChildren = this.treeAdapter.getChildNodes(this.document); | |
| const docTypeNode = documentChildren.find((node) => this.treeAdapter.isDocumentTypeNode(node)); | |
| if (docTypeNode) { | |
| this.treeAdapter.setNodeSourceCodeLocation(docTypeNode, token.location); | |
| } | |
| } | |
| } | |
| /** @protected */ | |
| _attachElementToTree(element, location) { | |
| if (this.options.sourceCodeLocationInfo) { | |
| const loc = location && { | |
| ...location, | |
| startTag: location, | |
| }; | |
| this.treeAdapter.setNodeSourceCodeLocation(element, loc); | |
| } | |
| if (this._shouldFosterParentOnInsertion()) { | |
| this._fosterParentElement(element); | |
| } | |
| else { | |
| const parent = this.openElements.currentTmplContentOrNode; | |
| this.treeAdapter.appendChild(parent !== null && parent !== void 0 ? parent : this.document, element); | |
| } | |
| } | |
| /** | |
| * For self-closing tags. Add an element to the tree, but skip adding it | |
| * to the stack. | |
| */ | |
| /** @protected */ | |
| _appendElement(token, namespaceURI) { | |
| const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs); | |
| this._attachElementToTree(element, token.location); | |
| } | |
| /** @protected */ | |
| _insertElement(token, namespaceURI) { | |
| const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs); | |
| this._attachElementToTree(element, token.location); | |
| this.openElements.push(element, token.tagID); | |
| } | |
| /** @protected */ | |
| _insertFakeElement(tagName, tagID) { | |
| const element = this.treeAdapter.createElement(tagName, NS.HTML, []); | |
| this._attachElementToTree(element, null); | |
| this.openElements.push(element, tagID); | |
| } | |
| /** @protected */ | |
| _insertTemplate(token) { | |
| const tmpl = this.treeAdapter.createElement(token.tagName, NS.HTML, token.attrs); | |
| const content = this.treeAdapter.createDocumentFragment(); | |
| this.treeAdapter.setTemplateContent(tmpl, content); | |
| this._attachElementToTree(tmpl, token.location); | |
| this.openElements.push(tmpl, token.tagID); | |
| if (this.options.sourceCodeLocationInfo) | |
| this.treeAdapter.setNodeSourceCodeLocation(content, null); | |
| } | |
| /** @protected */ | |
| _insertFakeRootElement() { | |
| const element = this.treeAdapter.createElement(TN.HTML, NS.HTML, []); | |
| if (this.options.sourceCodeLocationInfo) | |
| this.treeAdapter.setNodeSourceCodeLocation(element, null); | |
| this.treeAdapter.appendChild(this.openElements.current, element); | |
| this.openElements.push(element, $.HTML); | |
| } | |
| /** @protected */ | |
| _appendCommentNode(token, parent) { | |
| const commentNode = this.treeAdapter.createCommentNode(token.data); | |
| this.treeAdapter.appendChild(parent, commentNode); | |
| if (this.options.sourceCodeLocationInfo) { | |
| this.treeAdapter.setNodeSourceCodeLocation(commentNode, token.location); | |
| } | |
| } | |
| /** @protected */ | |
| _insertCharacters(token) { | |
| let parent; | |
| let beforeElement; | |
| if (this._shouldFosterParentOnInsertion()) { | |
| ({ parent, beforeElement } = this._findFosterParentingLocation()); | |
| if (beforeElement) { | |
| this.treeAdapter.insertTextBefore(parent, token.chars, beforeElement); | |
| } | |
| else { | |
| this.treeAdapter.insertText(parent, token.chars); | |
| } | |
| } | |
| else { | |
| parent = this.openElements.currentTmplContentOrNode; | |
| this.treeAdapter.insertText(parent, token.chars); | |
| } | |
| if (!token.location) | |
| return; | |
| const siblings = this.treeAdapter.getChildNodes(parent); | |
| const textNodeIdx = beforeElement ? siblings.lastIndexOf(beforeElement) : siblings.length; | |
| const textNode = siblings[textNodeIdx - 1]; | |
| //NOTE: if we have a location assigned by another token, then just update the end position | |
| const tnLoc = this.treeAdapter.getNodeSourceCodeLocation(textNode); | |
| if (tnLoc) { | |
| const { endLine, endCol, endOffset } = token.location; | |
| this.treeAdapter.updateNodeSourceCodeLocation(textNode, { endLine, endCol, endOffset }); | |
| } | |
| else if (this.options.sourceCodeLocationInfo) { | |
| this.treeAdapter.setNodeSourceCodeLocation(textNode, token.location); | |
| } | |
| } | |
| /** @protected */ | |
| _adoptNodes(donor, recipient) { | |
| for (let child = this.treeAdapter.getFirstChild(donor); child; child = this.treeAdapter.getFirstChild(donor)) { | |
| this.treeAdapter.detachNode(child); | |
| this.treeAdapter.appendChild(recipient, child); | |
| } | |
| } | |
| /** @protected */ | |
| _setEndLocation(element, closingToken) { | |
| if (this.treeAdapter.getNodeSourceCodeLocation(element) && closingToken.location) { | |
| const ctLoc = closingToken.location; | |
| const tn = this.treeAdapter.getTagName(element); | |
| const endLoc = | |
| // NOTE: For cases like <p> <p> </p> - First 'p' closes without a closing | |
| // tag and for cases like <td> <p> </td> - 'p' closes without a closing tag. | |
| closingToken.type === TokenType.END_TAG && tn === closingToken.tagName | |
| ? { | |
| endTag: { ...ctLoc }, | |
| endLine: ctLoc.endLine, | |
| endCol: ctLoc.endCol, | |
| endOffset: ctLoc.endOffset, | |
| } | |
| : { | |
| endLine: ctLoc.startLine, | |
| endCol: ctLoc.startCol, | |
| endOffset: ctLoc.startOffset, | |
| }; | |
| this.treeAdapter.updateNodeSourceCodeLocation(element, endLoc); | |
| } | |
| } | |
| //Token processing | |
| shouldProcessStartTagTokenInForeignContent(token) { | |
| // Check that neither current === document, or ns === NS.HTML | |
| if (!this.currentNotInHTML) | |
| return false; | |
| let current; | |
| let currentTagId; | |
| if (this.openElements.stackTop === 0 && this.fragmentContext) { | |
| current = this.fragmentContext; | |
| currentTagId = this.fragmentContextID; | |
| } | |
| else { | |
| ({ current, currentTagId } = this.openElements); | |
| } | |
| if (token.tagID === $.SVG && | |
| this.treeAdapter.getTagName(current) === TN.ANNOTATION_XML && | |
| this.treeAdapter.getNamespaceURI(current) === NS.MATHML) { | |
| return false; | |
| } | |
| return ( | |
| // Check that `current` is not an integration point for HTML or MathML elements. | |
| this.tokenizer.inForeignNode || | |
| // If it _is_ an integration point, then we might have to check that it is not an HTML | |
| // integration point. | |
| ((token.tagID === $.MGLYPH || token.tagID === $.MALIGNMARK) && | |
| currentTagId !== undefined && | |
| !this._isIntegrationPoint(currentTagId, current, NS.HTML))); | |
| } | |
| /** @protected */ | |
| _processToken(token) { | |
| switch (token.type) { | |
| case TokenType.CHARACTER: { | |
| this.onCharacter(token); | |
| break; | |
| } | |
| case TokenType.NULL_CHARACTER: { | |
| this.onNullCharacter(token); | |
| break; | |
| } | |
| case TokenType.COMMENT: { | |
| this.onComment(token); | |
| break; | |
| } | |
| case TokenType.DOCTYPE: { | |
| this.onDoctype(token); | |
| break; | |
| } | |
| case TokenType.START_TAG: { | |
| this._processStartTag(token); | |
| break; | |
| } | |
| case TokenType.END_TAG: { | |
| this.onEndTag(token); | |
| break; | |
| } | |
| case TokenType.EOF: { | |
| this.onEof(token); | |
| break; | |
| } | |
| case TokenType.WHITESPACE_CHARACTER: { | |
| this.onWhitespaceCharacter(token); | |
| break; | |
| } | |
| } | |
| } | |
| //Integration points | |
| /** @protected */ | |
| _isIntegrationPoint(tid, element, foreignNS) { | |
| const ns = this.treeAdapter.getNamespaceURI(element); | |
| const attrs = this.treeAdapter.getAttrList(element); | |
| return foreignContent.isIntegrationPoint(tid, ns, attrs, foreignNS); | |
| } | |
| //Active formatting elements reconstruction | |
| /** @protected */ | |
| _reconstructActiveFormattingElements() { | |
| const listLength = this.activeFormattingElements.entries.length; | |
| if (listLength) { | |
| const endIndex = this.activeFormattingElements.entries.findIndex((entry) => entry.type === EntryType.Marker || this.openElements.contains(entry.element)); | |
| const unopenIdx = endIndex === -1 ? listLength - 1 : endIndex - 1; | |
| for (let i = unopenIdx; i >= 0; i--) { | |
| const entry = this.activeFormattingElements.entries[i]; | |
| this._insertElement(entry.token, this.treeAdapter.getNamespaceURI(entry.element)); | |
| entry.element = this.openElements.current; | |
| } | |
| } | |
| } | |
| //Close elements | |
| /** @protected */ | |
| _closeTableCell() { | |
| this.openElements.generateImpliedEndTags(); | |
| this.openElements.popUntilTableCellPopped(); | |
| this.activeFormattingElements.clearToLastMarker(); | |
| this.insertionMode = InsertionMode.IN_ROW; | |
| } | |
| /** @protected */ | |
| _closePElement() { | |
| this.openElements.generateImpliedEndTagsWithExclusion($.P); | |
| this.openElements.popUntilTagNamePopped($.P); | |
| } | |
| //Insertion modes | |
| /** @protected */ | |
| _resetInsertionMode() { | |
| for (let i = this.openElements.stackTop; i >= 0; i--) { | |
| //Insertion mode reset map | |
| switch (i === 0 && this.fragmentContext ? this.fragmentContextID : this.openElements.tagIDs[i]) { | |
| case $.TR: { | |
| this.insertionMode = InsertionMode.IN_ROW; | |
| return; | |
| } | |
| case $.TBODY: | |
| case $.THEAD: | |
| case $.TFOOT: { | |
| this.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| return; | |
| } | |
| case $.CAPTION: { | |
| this.insertionMode = InsertionMode.IN_CAPTION; | |
| return; | |
| } | |
| case $.COLGROUP: { | |
| this.insertionMode = InsertionMode.IN_COLUMN_GROUP; | |
| return; | |
| } | |
| case $.TABLE: { | |
| this.insertionMode = InsertionMode.IN_TABLE; | |
| return; | |
| } | |
| case $.BODY: { | |
| this.insertionMode = InsertionMode.IN_BODY; | |
| return; | |
| } | |
| case $.FRAMESET: { | |
| this.insertionMode = InsertionMode.IN_FRAMESET; | |
| return; | |
| } | |
| case $.SELECT: { | |
| this._resetInsertionModeForSelect(i); | |
| return; | |
| } | |
| case $.TEMPLATE: { | |
| this.insertionMode = this.tmplInsertionModeStack[0]; | |
| return; | |
| } | |
| case $.HTML: { | |
| this.insertionMode = this.headElement ? InsertionMode.AFTER_HEAD : InsertionMode.BEFORE_HEAD; | |
| return; | |
| } | |
| case $.TD: | |
| case $.TH: { | |
| if (i > 0) { | |
| this.insertionMode = InsertionMode.IN_CELL; | |
| return; | |
| } | |
| break; | |
| } | |
| case $.HEAD: { | |
| if (i > 0) { | |
| this.insertionMode = InsertionMode.IN_HEAD; | |
| return; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| this.insertionMode = InsertionMode.IN_BODY; | |
| } | |
| /** @protected */ | |
| _resetInsertionModeForSelect(selectIdx) { | |
| if (selectIdx > 0) { | |
| for (let i = selectIdx - 1; i > 0; i--) { | |
| const tn = this.openElements.tagIDs[i]; | |
| if (tn === $.TEMPLATE) { | |
| break; | |
| } | |
| else if (tn === $.TABLE) { | |
| this.insertionMode = InsertionMode.IN_SELECT_IN_TABLE; | |
| return; | |
| } | |
| } | |
| } | |
| this.insertionMode = InsertionMode.IN_SELECT; | |
| } | |
| //Foster parenting | |
| /** @protected */ | |
| _isElementCausesFosterParenting(tn) { | |
| return TABLE_STRUCTURE_TAGS.has(tn); | |
| } | |
| /** @protected */ | |
| _shouldFosterParentOnInsertion() { | |
| return (this.fosterParentingEnabled && | |
| this.openElements.currentTagId !== undefined && | |
| this._isElementCausesFosterParenting(this.openElements.currentTagId)); | |
| } | |
| /** @protected */ | |
| _findFosterParentingLocation() { | |
| for (let i = this.openElements.stackTop; i >= 0; i--) { | |
| const openElement = this.openElements.items[i]; | |
| switch (this.openElements.tagIDs[i]) { | |
| case $.TEMPLATE: { | |
| if (this.treeAdapter.getNamespaceURI(openElement) === NS.HTML) { | |
| return { parent: this.treeAdapter.getTemplateContent(openElement), beforeElement: null }; | |
| } | |
| break; | |
| } | |
| case $.TABLE: { | |
| const parent = this.treeAdapter.getParentNode(openElement); | |
| if (parent) { | |
| return { parent, beforeElement: openElement }; | |
| } | |
| return { parent: this.openElements.items[i - 1], beforeElement: null }; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| return { parent: this.openElements.items[0], beforeElement: null }; | |
| } | |
| /** @protected */ | |
| _fosterParentElement(element) { | |
| const location = this._findFosterParentingLocation(); | |
| if (location.beforeElement) { | |
| this.treeAdapter.insertBefore(location.parent, element, location.beforeElement); | |
| } | |
| else { | |
| this.treeAdapter.appendChild(location.parent, element); | |
| } | |
| } | |
| //Special elements | |
| /** @protected */ | |
| _isSpecialElement(element, id) { | |
| const ns = this.treeAdapter.getNamespaceURI(element); | |
| return SPECIAL_ELEMENTS[ns].has(id); | |
| } | |
| /** @internal */ | |
| onCharacter(token) { | |
| this.skipNextNewLine = false; | |
| if (this.tokenizer.inForeignNode) { | |
| characterInForeignContent(this, token); | |
| return; | |
| } | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: { | |
| tokenInInitialMode(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HTML: { | |
| tokenBeforeHtml(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HEAD: { | |
| tokenBeforeHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD: { | |
| tokenInHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: { | |
| tokenInHeadNoScript(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_HEAD: { | |
| tokenAfterHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_BODY: | |
| case InsertionMode.IN_CAPTION: | |
| case InsertionMode.IN_CELL: | |
| case InsertionMode.IN_TEMPLATE: { | |
| characterInBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.TEXT: | |
| case InsertionMode.IN_SELECT: | |
| case InsertionMode.IN_SELECT_IN_TABLE: { | |
| this._insertCharacters(token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE: | |
| case InsertionMode.IN_TABLE_BODY: | |
| case InsertionMode.IN_ROW: { | |
| characterInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| characterInTableText(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_COLUMN_GROUP: { | |
| tokenInColumnGroup(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_BODY: { | |
| tokenAfterBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_AFTER_BODY: { | |
| tokenAfterAfterBody(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onNullCharacter(token) { | |
| this.skipNextNewLine = false; | |
| if (this.tokenizer.inForeignNode) { | |
| nullCharacterInForeignContent(this, token); | |
| return; | |
| } | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: { | |
| tokenInInitialMode(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HTML: { | |
| tokenBeforeHtml(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HEAD: { | |
| tokenBeforeHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD: { | |
| tokenInHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: { | |
| tokenInHeadNoScript(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_HEAD: { | |
| tokenAfterHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.TEXT: { | |
| this._insertCharacters(token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE: | |
| case InsertionMode.IN_TABLE_BODY: | |
| case InsertionMode.IN_ROW: { | |
| characterInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_COLUMN_GROUP: { | |
| tokenInColumnGroup(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_BODY: { | |
| tokenAfterBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_AFTER_BODY: { | |
| tokenAfterAfterBody(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onComment(token) { | |
| this.skipNextNewLine = false; | |
| if (this.currentNotInHTML) { | |
| appendComment(this, token); | |
| return; | |
| } | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: | |
| case InsertionMode.BEFORE_HTML: | |
| case InsertionMode.BEFORE_HEAD: | |
| case InsertionMode.IN_HEAD: | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: | |
| case InsertionMode.AFTER_HEAD: | |
| case InsertionMode.IN_BODY: | |
| case InsertionMode.IN_TABLE: | |
| case InsertionMode.IN_CAPTION: | |
| case InsertionMode.IN_COLUMN_GROUP: | |
| case InsertionMode.IN_TABLE_BODY: | |
| case InsertionMode.IN_ROW: | |
| case InsertionMode.IN_CELL: | |
| case InsertionMode.IN_SELECT: | |
| case InsertionMode.IN_SELECT_IN_TABLE: | |
| case InsertionMode.IN_TEMPLATE: | |
| case InsertionMode.IN_FRAMESET: | |
| case InsertionMode.AFTER_FRAMESET: { | |
| appendComment(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| tokenInTableText(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_BODY: { | |
| appendCommentToRootHtmlElement(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_AFTER_BODY: | |
| case InsertionMode.AFTER_AFTER_FRAMESET: { | |
| appendCommentToDocument(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onDoctype(token) { | |
| this.skipNextNewLine = false; | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: { | |
| doctypeInInitialMode(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HEAD: | |
| case InsertionMode.IN_HEAD: | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: | |
| case InsertionMode.AFTER_HEAD: { | |
| this._err(token, ERR.misplacedDoctype); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| tokenInTableText(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onStartTag(token) { | |
| this.skipNextNewLine = false; | |
| this.currentToken = token; | |
| this._processStartTag(token); | |
| if (token.selfClosing && !token.ackSelfClosing) { | |
| this._err(token, ERR.nonVoidHtmlElementStartTagWithTrailingSolidus); | |
| } | |
| } | |
| /** | |
| * Processes a given start tag. | |
| * | |
| * `onStartTag` checks if a self-closing tag was recognized. When a token | |
| * is moved inbetween multiple insertion modes, this check for self-closing | |
| * could lead to false positives. To avoid this, `_processStartTag` is used | |
| * for nested calls. | |
| * | |
| * @param token The token to process. | |
| * @protected | |
| */ | |
| _processStartTag(token) { | |
| if (this.shouldProcessStartTagTokenInForeignContent(token)) { | |
| startTagInForeignContent(this, token); | |
| } | |
| else { | |
| this._startTagOutsideForeignContent(token); | |
| } | |
| } | |
| /** @protected */ | |
| _startTagOutsideForeignContent(token) { | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: { | |
| tokenInInitialMode(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HTML: { | |
| startTagBeforeHtml(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HEAD: { | |
| startTagBeforeHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD: { | |
| startTagInHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: { | |
| startTagInHeadNoScript(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_HEAD: { | |
| startTagAfterHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_BODY: { | |
| startTagInBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE: { | |
| startTagInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| tokenInTableText(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_CAPTION: { | |
| startTagInCaption(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_COLUMN_GROUP: { | |
| startTagInColumnGroup(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_BODY: { | |
| startTagInTableBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_ROW: { | |
| startTagInRow(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_CELL: { | |
| startTagInCell(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_SELECT: { | |
| startTagInSelect(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_SELECT_IN_TABLE: { | |
| startTagInSelectInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TEMPLATE: { | |
| startTagInTemplate(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_BODY: { | |
| startTagAfterBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_FRAMESET: { | |
| startTagInFrameset(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_FRAMESET: { | |
| startTagAfterFrameset(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_AFTER_BODY: { | |
| startTagAfterAfterBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_AFTER_FRAMESET: { | |
| startTagAfterAfterFrameset(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onEndTag(token) { | |
| this.skipNextNewLine = false; | |
| this.currentToken = token; | |
| if (this.currentNotInHTML) { | |
| endTagInForeignContent(this, token); | |
| } | |
| else { | |
| this._endTagOutsideForeignContent(token); | |
| } | |
| } | |
| /** @protected */ | |
| _endTagOutsideForeignContent(token) { | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: { | |
| tokenInInitialMode(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HTML: { | |
| endTagBeforeHtml(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HEAD: { | |
| endTagBeforeHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD: { | |
| endTagInHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: { | |
| endTagInHeadNoScript(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_HEAD: { | |
| endTagAfterHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_BODY: { | |
| endTagInBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.TEXT: { | |
| endTagInText(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE: { | |
| endTagInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| tokenInTableText(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_CAPTION: { | |
| endTagInCaption(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_COLUMN_GROUP: { | |
| endTagInColumnGroup(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_BODY: { | |
| endTagInTableBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_ROW: { | |
| endTagInRow(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_CELL: { | |
| endTagInCell(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_SELECT: { | |
| endTagInSelect(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_SELECT_IN_TABLE: { | |
| endTagInSelectInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TEMPLATE: { | |
| endTagInTemplate(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_BODY: { | |
| endTagAfterBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_FRAMESET: { | |
| endTagInFrameset(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_FRAMESET: { | |
| endTagAfterFrameset(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_AFTER_BODY: { | |
| tokenAfterAfterBody(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onEof(token) { | |
| switch (this.insertionMode) { | |
| case InsertionMode.INITIAL: { | |
| tokenInInitialMode(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HTML: { | |
| tokenBeforeHtml(this, token); | |
| break; | |
| } | |
| case InsertionMode.BEFORE_HEAD: { | |
| tokenBeforeHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD: { | |
| tokenInHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: { | |
| tokenInHeadNoScript(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_HEAD: { | |
| tokenAfterHead(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_BODY: | |
| case InsertionMode.IN_TABLE: | |
| case InsertionMode.IN_CAPTION: | |
| case InsertionMode.IN_COLUMN_GROUP: | |
| case InsertionMode.IN_TABLE_BODY: | |
| case InsertionMode.IN_ROW: | |
| case InsertionMode.IN_CELL: | |
| case InsertionMode.IN_SELECT: | |
| case InsertionMode.IN_SELECT_IN_TABLE: { | |
| eofInBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.TEXT: { | |
| eofInText(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| tokenInTableText(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TEMPLATE: { | |
| eofInTemplate(this, token); | |
| break; | |
| } | |
| case InsertionMode.AFTER_BODY: | |
| case InsertionMode.IN_FRAMESET: | |
| case InsertionMode.AFTER_FRAMESET: | |
| case InsertionMode.AFTER_AFTER_BODY: | |
| case InsertionMode.AFTER_AFTER_FRAMESET: { | |
| stopParsing(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| /** @internal */ | |
| onWhitespaceCharacter(token) { | |
| if (this.skipNextNewLine) { | |
| this.skipNextNewLine = false; | |
| if (token.chars.charCodeAt(0) === unicode.CODE_POINTS.LINE_FEED) { | |
| if (token.chars.length === 1) { | |
| return; | |
| } | |
| token.chars = token.chars.substr(1); | |
| } | |
| } | |
| if (this.tokenizer.inForeignNode) { | |
| this._insertCharacters(token); | |
| return; | |
| } | |
| switch (this.insertionMode) { | |
| case InsertionMode.IN_HEAD: | |
| case InsertionMode.IN_HEAD_NO_SCRIPT: | |
| case InsertionMode.AFTER_HEAD: | |
| case InsertionMode.TEXT: | |
| case InsertionMode.IN_COLUMN_GROUP: | |
| case InsertionMode.IN_SELECT: | |
| case InsertionMode.IN_SELECT_IN_TABLE: | |
| case InsertionMode.IN_FRAMESET: | |
| case InsertionMode.AFTER_FRAMESET: { | |
| this._insertCharacters(token); | |
| break; | |
| } | |
| case InsertionMode.IN_BODY: | |
| case InsertionMode.IN_CAPTION: | |
| case InsertionMode.IN_CELL: | |
| case InsertionMode.IN_TEMPLATE: | |
| case InsertionMode.AFTER_BODY: | |
| case InsertionMode.AFTER_AFTER_BODY: | |
| case InsertionMode.AFTER_AFTER_FRAMESET: { | |
| whitespaceCharacterInBody(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE: | |
| case InsertionMode.IN_TABLE_BODY: | |
| case InsertionMode.IN_ROW: { | |
| characterInTable(this, token); | |
| break; | |
| } | |
| case InsertionMode.IN_TABLE_TEXT: { | |
| whitespaceCharacterInTableText(this, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| } | |
| //Adoption agency algorithm | |
| //(see: http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html#adoptionAgency) | |
| //------------------------------------------------------------------ | |
| //Steps 5-8 of the algorithm | |
| function aaObtainFormattingElementEntry(p, token) { | |
| let formattingElementEntry = p.activeFormattingElements.getElementEntryInScopeWithTagName(token.tagName); | |
| if (formattingElementEntry) { | |
| if (!p.openElements.contains(formattingElementEntry.element)) { | |
| p.activeFormattingElements.removeEntry(formattingElementEntry); | |
| formattingElementEntry = null; | |
| } | |
| else if (!p.openElements.hasInScope(token.tagID)) { | |
| formattingElementEntry = null; | |
| } | |
| } | |
| else { | |
| genericEndTagInBody(p, token); | |
| } | |
| return formattingElementEntry; | |
| } | |
| //Steps 9 and 10 of the algorithm | |
| function aaObtainFurthestBlock(p, formattingElementEntry) { | |
| let furthestBlock = null; | |
| let idx = p.openElements.stackTop; | |
| for (; idx >= 0; idx--) { | |
| const element = p.openElements.items[idx]; | |
| if (element === formattingElementEntry.element) { | |
| break; | |
| } | |
| if (p._isSpecialElement(element, p.openElements.tagIDs[idx])) { | |
| furthestBlock = element; | |
| } | |
| } | |
| if (!furthestBlock) { | |
| p.openElements.shortenToLength(Math.max(idx, 0)); | |
| p.activeFormattingElements.removeEntry(formattingElementEntry); | |
| } | |
| return furthestBlock; | |
| } | |
| //Step 13 of the algorithm | |
| function aaInnerLoop(p, furthestBlock, formattingElement) { | |
| let lastElement = furthestBlock; | |
| let nextElement = p.openElements.getCommonAncestor(furthestBlock); | |
| for (let i = 0, element = nextElement; element !== formattingElement; i++, element = nextElement) { | |
| //NOTE: store the next element for the next loop iteration (it may be deleted from the stack by step 9.5) | |
| nextElement = p.openElements.getCommonAncestor(element); | |
| const elementEntry = p.activeFormattingElements.getElementEntry(element); | |
| const counterOverflow = elementEntry && i >= AA_INNER_LOOP_ITER; | |
| const shouldRemoveFromOpenElements = !elementEntry || counterOverflow; | |
| if (shouldRemoveFromOpenElements) { | |
| if (counterOverflow) { | |
| p.activeFormattingElements.removeEntry(elementEntry); | |
| } | |
| p.openElements.remove(element); | |
| } | |
| else { | |
| element = aaRecreateElementFromEntry(p, elementEntry); | |
| if (lastElement === furthestBlock) { | |
| p.activeFormattingElements.bookmark = elementEntry; | |
| } | |
| p.treeAdapter.detachNode(lastElement); | |
| p.treeAdapter.appendChild(element, lastElement); | |
| lastElement = element; | |
| } | |
| } | |
| return lastElement; | |
| } | |
| //Step 13.7 of the algorithm | |
| function aaRecreateElementFromEntry(p, elementEntry) { | |
| const ns = p.treeAdapter.getNamespaceURI(elementEntry.element); | |
| const newElement = p.treeAdapter.createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs); | |
| p.openElements.replace(elementEntry.element, newElement); | |
| elementEntry.element = newElement; | |
| return newElement; | |
| } | |
| //Step 14 of the algorithm | |
| function aaInsertLastNodeInCommonAncestor(p, commonAncestor, lastElement) { | |
| const tn = p.treeAdapter.getTagName(commonAncestor); | |
| const tid = getTagID(tn); | |
| if (p._isElementCausesFosterParenting(tid)) { | |
| p._fosterParentElement(lastElement); | |
| } | |
| else { | |
| const ns = p.treeAdapter.getNamespaceURI(commonAncestor); | |
| if (tid === $.TEMPLATE && ns === NS.HTML) { | |
| commonAncestor = p.treeAdapter.getTemplateContent(commonAncestor); | |
| } | |
| p.treeAdapter.appendChild(commonAncestor, lastElement); | |
| } | |
| } | |
| //Steps 15-19 of the algorithm | |
| function aaReplaceFormattingElement(p, furthestBlock, formattingElementEntry) { | |
| const ns = p.treeAdapter.getNamespaceURI(formattingElementEntry.element); | |
| const { token } = formattingElementEntry; | |
| const newElement = p.treeAdapter.createElement(token.tagName, ns, token.attrs); | |
| p._adoptNodes(furthestBlock, newElement); | |
| p.treeAdapter.appendChild(furthestBlock, newElement); | |
| p.activeFormattingElements.insertElementAfterBookmark(newElement, token); | |
| p.activeFormattingElements.removeEntry(formattingElementEntry); | |
| p.openElements.remove(formattingElementEntry.element); | |
| p.openElements.insertAfter(furthestBlock, newElement, token.tagID); | |
| } | |
| //Algorithm entry point | |
| function callAdoptionAgency(p, token) { | |
| for (let i = 0; i < AA_OUTER_LOOP_ITER; i++) { | |
| const formattingElementEntry = aaObtainFormattingElementEntry(p, token); | |
| if (!formattingElementEntry) { | |
| break; | |
| } | |
| const furthestBlock = aaObtainFurthestBlock(p, formattingElementEntry); | |
| if (!furthestBlock) { | |
| break; | |
| } | |
| p.activeFormattingElements.bookmark = formattingElementEntry; | |
| const lastElement = aaInnerLoop(p, furthestBlock, formattingElementEntry.element); | |
| const commonAncestor = p.openElements.getCommonAncestor(formattingElementEntry.element); | |
| p.treeAdapter.detachNode(lastElement); | |
| if (commonAncestor) | |
| aaInsertLastNodeInCommonAncestor(p, commonAncestor, lastElement); | |
| aaReplaceFormattingElement(p, furthestBlock, formattingElementEntry); | |
| } | |
| } | |
| //Generic token handlers | |
| //------------------------------------------------------------------ | |
| function appendComment(p, token) { | |
| p._appendCommentNode(token, p.openElements.currentTmplContentOrNode); | |
| } | |
| function appendCommentToRootHtmlElement(p, token) { | |
| p._appendCommentNode(token, p.openElements.items[0]); | |
| } | |
| function appendCommentToDocument(p, token) { | |
| p._appendCommentNode(token, p.document); | |
| } | |
| function stopParsing(p, token) { | |
| p.stopped = true; | |
| // NOTE: Set end locations for elements that remain on the open element stack. | |
| if (token.location) { | |
| // NOTE: If we are not in a fragment, `html` and `body` will stay on the stack. | |
| // This is a problem, as we might overwrite their end position here. | |
| const target = p.fragmentContext ? 0 : 2; | |
| for (let i = p.openElements.stackTop; i >= target; i--) { | |
| p._setEndLocation(p.openElements.items[i], token); | |
| } | |
| // Handle `html` and `body` | |
| if (!p.fragmentContext && p.openElements.stackTop >= 0) { | |
| const htmlElement = p.openElements.items[0]; | |
| const htmlLocation = p.treeAdapter.getNodeSourceCodeLocation(htmlElement); | |
| if (htmlLocation && !htmlLocation.endTag) { | |
| p._setEndLocation(htmlElement, token); | |
| if (p.openElements.stackTop >= 1) { | |
| const bodyElement = p.openElements.items[1]; | |
| const bodyLocation = p.treeAdapter.getNodeSourceCodeLocation(bodyElement); | |
| if (bodyLocation && !bodyLocation.endTag) { | |
| p._setEndLocation(bodyElement, token); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // The "initial" insertion mode | |
| //------------------------------------------------------------------ | |
| function doctypeInInitialMode(p, token) { | |
| p._setDocumentType(token); | |
| const mode = token.forceQuirks ? DOCUMENT_MODE.QUIRKS : doctype.getDocumentMode(token); | |
| if (!doctype.isConforming(token)) { | |
| p._err(token, ERR.nonConformingDoctype); | |
| } | |
| p.treeAdapter.setDocumentMode(p.document, mode); | |
| p.insertionMode = InsertionMode.BEFORE_HTML; | |
| } | |
| function tokenInInitialMode(p, token) { | |
| p._err(token, ERR.missingDoctype, true); | |
| p.treeAdapter.setDocumentMode(p.document, DOCUMENT_MODE.QUIRKS); | |
| p.insertionMode = InsertionMode.BEFORE_HTML; | |
| p._processToken(token); | |
| } | |
| // The "before html" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagBeforeHtml(p, token) { | |
| if (token.tagID === $.HTML) { | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.BEFORE_HEAD; | |
| } | |
| else { | |
| tokenBeforeHtml(p, token); | |
| } | |
| } | |
| function endTagBeforeHtml(p, token) { | |
| const tn = token.tagID; | |
| if (tn === $.HTML || tn === $.HEAD || tn === $.BODY || tn === $.BR) { | |
| tokenBeforeHtml(p, token); | |
| } | |
| } | |
| function tokenBeforeHtml(p, token) { | |
| p._insertFakeRootElement(); | |
| p.insertionMode = InsertionMode.BEFORE_HEAD; | |
| p._processToken(token); | |
| } | |
| // The "before head" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagBeforeHead(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.HEAD: { | |
| p._insertElement(token, NS.HTML); | |
| p.headElement = p.openElements.current; | |
| p.insertionMode = InsertionMode.IN_HEAD; | |
| break; | |
| } | |
| default: { | |
| tokenBeforeHead(p, token); | |
| } | |
| } | |
| } | |
| function endTagBeforeHead(p, token) { | |
| const tn = token.tagID; | |
| if (tn === $.HEAD || tn === $.BODY || tn === $.HTML || tn === $.BR) { | |
| tokenBeforeHead(p, token); | |
| } | |
| else { | |
| p._err(token, ERR.endTagWithoutMatchingOpenElement); | |
| } | |
| } | |
| function tokenBeforeHead(p, token) { | |
| p._insertFakeElement(TN.HEAD, $.HEAD); | |
| p.headElement = p.openElements.current; | |
| p.insertionMode = InsertionMode.IN_HEAD; | |
| p._processToken(token); | |
| } | |
| // The "in head" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInHead(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.BASE: | |
| case $.BASEFONT: | |
| case $.BGSOUND: | |
| case $.LINK: | |
| case $.META: { | |
| p._appendElement(token, NS.HTML); | |
| token.ackSelfClosing = true; | |
| break; | |
| } | |
| case $.TITLE: { | |
| p._switchToTextParsing(token, TokenizerMode.RCDATA); | |
| break; | |
| } | |
| case $.NOSCRIPT: { | |
| if (p.options.scriptingEnabled) { | |
| p._switchToTextParsing(token, TokenizerMode.RAWTEXT); | |
| } | |
| else { | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_HEAD_NO_SCRIPT; | |
| } | |
| break; | |
| } | |
| case $.NOFRAMES: | |
| case $.STYLE: { | |
| p._switchToTextParsing(token, TokenizerMode.RAWTEXT); | |
| break; | |
| } | |
| case $.SCRIPT: { | |
| p._switchToTextParsing(token, TokenizerMode.SCRIPT_DATA); | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| p._insertTemplate(token); | |
| p.activeFormattingElements.insertMarker(); | |
| p.framesetOk = false; | |
| p.insertionMode = InsertionMode.IN_TEMPLATE; | |
| p.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE); | |
| break; | |
| } | |
| case $.HEAD: { | |
| p._err(token, ERR.misplacedStartTagForHeadElement); | |
| break; | |
| } | |
| default: { | |
| tokenInHead(p, token); | |
| } | |
| } | |
| } | |
| function endTagInHead(p, token) { | |
| switch (token.tagID) { | |
| case $.HEAD: { | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.AFTER_HEAD; | |
| break; | |
| } | |
| case $.BODY: | |
| case $.BR: | |
| case $.HTML: { | |
| tokenInHead(p, token); | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| templateEndTagInHead(p, token); | |
| break; | |
| } | |
| default: { | |
| p._err(token, ERR.endTagWithoutMatchingOpenElement); | |
| } | |
| } | |
| } | |
| function templateEndTagInHead(p, token) { | |
| if (p.openElements.tmplCount > 0) { | |
| p.openElements.generateImpliedEndTagsThoroughly(); | |
| if (p.openElements.currentTagId !== $.TEMPLATE) { | |
| p._err(token, ERR.closingOfElementWithOpenChildElements); | |
| } | |
| p.openElements.popUntilTagNamePopped($.TEMPLATE); | |
| p.activeFormattingElements.clearToLastMarker(); | |
| p.tmplInsertionModeStack.shift(); | |
| p._resetInsertionMode(); | |
| } | |
| else { | |
| p._err(token, ERR.endTagWithoutMatchingOpenElement); | |
| } | |
| } | |
| function tokenInHead(p, token) { | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.AFTER_HEAD; | |
| p._processToken(token); | |
| } | |
| // The "in head no script" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInHeadNoScript(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.BASEFONT: | |
| case $.BGSOUND: | |
| case $.HEAD: | |
| case $.LINK: | |
| case $.META: | |
| case $.NOFRAMES: | |
| case $.STYLE: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| case $.NOSCRIPT: { | |
| p._err(token, ERR.nestedNoscriptInHead); | |
| break; | |
| } | |
| default: { | |
| tokenInHeadNoScript(p, token); | |
| } | |
| } | |
| } | |
| function endTagInHeadNoScript(p, token) { | |
| switch (token.tagID) { | |
| case $.NOSCRIPT: { | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_HEAD; | |
| break; | |
| } | |
| case $.BR: { | |
| tokenInHeadNoScript(p, token); | |
| break; | |
| } | |
| default: { | |
| p._err(token, ERR.endTagWithoutMatchingOpenElement); | |
| } | |
| } | |
| } | |
| function tokenInHeadNoScript(p, token) { | |
| const errCode = token.type === TokenType.EOF ? ERR.openElementsLeftAfterEof : ERR.disallowedContentInNoscriptInHead; | |
| p._err(token, errCode); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_HEAD; | |
| p._processToken(token); | |
| } | |
| // The "after head" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagAfterHead(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.BODY: { | |
| p._insertElement(token, NS.HTML); | |
| p.framesetOk = false; | |
| p.insertionMode = InsertionMode.IN_BODY; | |
| break; | |
| } | |
| case $.FRAMESET: { | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_FRAMESET; | |
| break; | |
| } | |
| case $.BASE: | |
| case $.BASEFONT: | |
| case $.BGSOUND: | |
| case $.LINK: | |
| case $.META: | |
| case $.NOFRAMES: | |
| case $.SCRIPT: | |
| case $.STYLE: | |
| case $.TEMPLATE: | |
| case $.TITLE: { | |
| p._err(token, ERR.abandonedHeadElementChild); | |
| p.openElements.push(p.headElement, $.HEAD); | |
| startTagInHead(p, token); | |
| p.openElements.remove(p.headElement); | |
| break; | |
| } | |
| case $.HEAD: { | |
| p._err(token, ERR.misplacedStartTagForHeadElement); | |
| break; | |
| } | |
| default: { | |
| tokenAfterHead(p, token); | |
| } | |
| } | |
| } | |
| function endTagAfterHead(p, token) { | |
| switch (token.tagID) { | |
| case $.BODY: | |
| case $.HTML: | |
| case $.BR: { | |
| tokenAfterHead(p, token); | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| templateEndTagInHead(p, token); | |
| break; | |
| } | |
| default: { | |
| p._err(token, ERR.endTagWithoutMatchingOpenElement); | |
| } | |
| } | |
| } | |
| function tokenAfterHead(p, token) { | |
| p._insertFakeElement(TN.BODY, $.BODY); | |
| p.insertionMode = InsertionMode.IN_BODY; | |
| modeInBody(p, token); | |
| } | |
| // The "in body" insertion mode | |
| //------------------------------------------------------------------ | |
| function modeInBody(p, token) { | |
| switch (token.type) { | |
| case TokenType.CHARACTER: { | |
| characterInBody(p, token); | |
| break; | |
| } | |
| case TokenType.WHITESPACE_CHARACTER: { | |
| whitespaceCharacterInBody(p, token); | |
| break; | |
| } | |
| case TokenType.COMMENT: { | |
| appendComment(p, token); | |
| break; | |
| } | |
| case TokenType.START_TAG: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case TokenType.END_TAG: { | |
| endTagInBody(p, token); | |
| break; | |
| } | |
| case TokenType.EOF: { | |
| eofInBody(p, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| function whitespaceCharacterInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertCharacters(token); | |
| } | |
| function characterInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertCharacters(token); | |
| p.framesetOk = false; | |
| } | |
| function htmlStartTagInBody(p, token) { | |
| if (p.openElements.tmplCount === 0) { | |
| p.treeAdapter.adoptAttributes(p.openElements.items[0], token.attrs); | |
| } | |
| } | |
| function bodyStartTagInBody(p, token) { | |
| const bodyElement = p.openElements.tryPeekProperlyNestedBodyElement(); | |
| if (bodyElement && p.openElements.tmplCount === 0) { | |
| p.framesetOk = false; | |
| p.treeAdapter.adoptAttributes(bodyElement, token.attrs); | |
| } | |
| } | |
| function framesetStartTagInBody(p, token) { | |
| const bodyElement = p.openElements.tryPeekProperlyNestedBodyElement(); | |
| if (p.framesetOk && bodyElement) { | |
| p.treeAdapter.detachNode(bodyElement); | |
| p.openElements.popAllUpToHtmlElement(); | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_FRAMESET; | |
| } | |
| } | |
| function addressStartTagInBody(p, token) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function numberedHeaderStartTagInBody(p, token) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| if (p.openElements.currentTagId !== undefined && NUMBERED_HEADERS.has(p.openElements.currentTagId)) { | |
| p.openElements.pop(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function preStartTagInBody(p, token) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| //NOTE: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move | |
| //on to the next one. (Newlines at the start of pre blocks are ignored as an authoring convenience.) | |
| p.skipNextNewLine = true; | |
| p.framesetOk = false; | |
| } | |
| function formStartTagInBody(p, token) { | |
| const inTemplate = p.openElements.tmplCount > 0; | |
| if (!p.formElement || inTemplate) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| if (!inTemplate) { | |
| p.formElement = p.openElements.current; | |
| } | |
| } | |
| } | |
| function listItemStartTagInBody(p, token) { | |
| p.framesetOk = false; | |
| const tn = token.tagID; | |
| for (let i = p.openElements.stackTop; i >= 0; i--) { | |
| const elementId = p.openElements.tagIDs[i]; | |
| if ((tn === $.LI && elementId === $.LI) || | |
| ((tn === $.DD || tn === $.DT) && (elementId === $.DD || elementId === $.DT))) { | |
| p.openElements.generateImpliedEndTagsWithExclusion(elementId); | |
| p.openElements.popUntilTagNamePopped(elementId); | |
| break; | |
| } | |
| if (elementId !== $.ADDRESS && | |
| elementId !== $.DIV && | |
| elementId !== $.P && | |
| p._isSpecialElement(p.openElements.items[i], elementId)) { | |
| break; | |
| } | |
| } | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function plaintextStartTagInBody(p, token) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| p.tokenizer.state = TokenizerMode.PLAINTEXT; | |
| } | |
| function buttonStartTagInBody(p, token) { | |
| if (p.openElements.hasInScope($.BUTTON)) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilTagNamePopped($.BUTTON); | |
| } | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| p.framesetOk = false; | |
| } | |
| function aStartTagInBody(p, token) { | |
| const activeElementEntry = p.activeFormattingElements.getElementEntryInScopeWithTagName(TN.A); | |
| if (activeElementEntry) { | |
| callAdoptionAgency(p, token); | |
| p.openElements.remove(activeElementEntry.element); | |
| p.activeFormattingElements.removeEntry(activeElementEntry); | |
| } | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| p.activeFormattingElements.pushElement(p.openElements.current, token); | |
| } | |
| function bStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| p.activeFormattingElements.pushElement(p.openElements.current, token); | |
| } | |
| function nobrStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| if (p.openElements.hasInScope($.NOBR)) { | |
| callAdoptionAgency(p, token); | |
| p._reconstructActiveFormattingElements(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| p.activeFormattingElements.pushElement(p.openElements.current, token); | |
| } | |
| function appletStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| p.activeFormattingElements.insertMarker(); | |
| p.framesetOk = false; | |
| } | |
| function tableStartTagInBody(p, token) { | |
| if (p.treeAdapter.getDocumentMode(p.document) !== DOCUMENT_MODE.QUIRKS && p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| p.framesetOk = false; | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| } | |
| function areaStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._appendElement(token, NS.HTML); | |
| p.framesetOk = false; | |
| token.ackSelfClosing = true; | |
| } | |
| function isHiddenInput(token) { | |
| const inputType = getTokenAttr(token, ATTRS.TYPE); | |
| return inputType != null && inputType.toLowerCase() === HIDDEN_INPUT_TYPE; | |
| } | |
| function inputStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._appendElement(token, NS.HTML); | |
| if (!isHiddenInput(token)) { | |
| p.framesetOk = false; | |
| } | |
| token.ackSelfClosing = true; | |
| } | |
| function paramStartTagInBody(p, token) { | |
| p._appendElement(token, NS.HTML); | |
| token.ackSelfClosing = true; | |
| } | |
| function hrStartTagInBody(p, token) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._appendElement(token, NS.HTML); | |
| p.framesetOk = false; | |
| token.ackSelfClosing = true; | |
| } | |
| function imageStartTagInBody(p, token) { | |
| token.tagName = TN.IMG; | |
| token.tagID = $.IMG; | |
| areaStartTagInBody(p, token); | |
| } | |
| function textareaStartTagInBody(p, token) { | |
| p._insertElement(token, NS.HTML); | |
| //NOTE: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move | |
| //on to the next one. (Newlines at the start of textarea elements are ignored as an authoring convenience.) | |
| p.skipNextNewLine = true; | |
| p.tokenizer.state = TokenizerMode.RCDATA; | |
| p.originalInsertionMode = p.insertionMode; | |
| p.framesetOk = false; | |
| p.insertionMode = InsertionMode.TEXT; | |
| } | |
| function xmpStartTagInBody(p, token) { | |
| if (p.openElements.hasInButtonScope($.P)) { | |
| p._closePElement(); | |
| } | |
| p._reconstructActiveFormattingElements(); | |
| p.framesetOk = false; | |
| p._switchToTextParsing(token, TokenizerMode.RAWTEXT); | |
| } | |
| function iframeStartTagInBody(p, token) { | |
| p.framesetOk = false; | |
| p._switchToTextParsing(token, TokenizerMode.RAWTEXT); | |
| } | |
| //NOTE: here we assume that we always act as a user agent with enabled plugins/frames, so we parse | |
| //<noembed>/<noframes> as rawtext. | |
| function rawTextStartTagInBody(p, token) { | |
| p._switchToTextParsing(token, TokenizerMode.RAWTEXT); | |
| } | |
| function selectStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| p.framesetOk = false; | |
| p.insertionMode = | |
| p.insertionMode === InsertionMode.IN_TABLE || | |
| p.insertionMode === InsertionMode.IN_CAPTION || | |
| p.insertionMode === InsertionMode.IN_TABLE_BODY || | |
| p.insertionMode === InsertionMode.IN_ROW || | |
| p.insertionMode === InsertionMode.IN_CELL | |
| ? InsertionMode.IN_SELECT_IN_TABLE | |
| : InsertionMode.IN_SELECT; | |
| } | |
| function optgroupStartTagInBody(p, token) { | |
| if (p.openElements.currentTagId === $.OPTION) { | |
| p.openElements.pop(); | |
| } | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function rbStartTagInBody(p, token) { | |
| if (p.openElements.hasInScope($.RUBY)) { | |
| p.openElements.generateImpliedEndTags(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function rtStartTagInBody(p, token) { | |
| if (p.openElements.hasInScope($.RUBY)) { | |
| p.openElements.generateImpliedEndTagsWithExclusion($.RTC); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function mathStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| foreignContent.adjustTokenMathMLAttrs(token); | |
| foreignContent.adjustTokenXMLAttrs(token); | |
| if (token.selfClosing) { | |
| p._appendElement(token, NS.MATHML); | |
| } | |
| else { | |
| p._insertElement(token, NS.MATHML); | |
| } | |
| token.ackSelfClosing = true; | |
| } | |
| function svgStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| foreignContent.adjustTokenSVGAttrs(token); | |
| foreignContent.adjustTokenXMLAttrs(token); | |
| if (token.selfClosing) { | |
| p._appendElement(token, NS.SVG); | |
| } | |
| else { | |
| p._insertElement(token, NS.SVG); | |
| } | |
| token.ackSelfClosing = true; | |
| } | |
| function genericStartTagInBody(p, token) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertElement(token, NS.HTML); | |
| } | |
| function startTagInBody(p, token) { | |
| switch (token.tagID) { | |
| case $.I: | |
| case $.S: | |
| case $.B: | |
| case $.U: | |
| case $.EM: | |
| case $.TT: | |
| case $.BIG: | |
| case $.CODE: | |
| case $.FONT: | |
| case $.SMALL: | |
| case $.STRIKE: | |
| case $.STRONG: { | |
| bStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.A: { | |
| aStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.H1: | |
| case $.H2: | |
| case $.H3: | |
| case $.H4: | |
| case $.H5: | |
| case $.H6: { | |
| numberedHeaderStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.P: | |
| case $.DL: | |
| case $.OL: | |
| case $.UL: | |
| case $.DIV: | |
| case $.DIR: | |
| case $.NAV: | |
| case $.MAIN: | |
| case $.MENU: | |
| case $.ASIDE: | |
| case $.CENTER: | |
| case $.FIGURE: | |
| case $.FOOTER: | |
| case $.HEADER: | |
| case $.HGROUP: | |
| case $.DIALOG: | |
| case $.DETAILS: | |
| case $.ADDRESS: | |
| case $.ARTICLE: | |
| case $.SEARCH: | |
| case $.SECTION: | |
| case $.SUMMARY: | |
| case $.FIELDSET: | |
| case $.BLOCKQUOTE: | |
| case $.FIGCAPTION: { | |
| addressStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.LI: | |
| case $.DD: | |
| case $.DT: { | |
| listItemStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.BR: | |
| case $.IMG: | |
| case $.WBR: | |
| case $.AREA: | |
| case $.EMBED: | |
| case $.KEYGEN: { | |
| areaStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.HR: { | |
| hrStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.RB: | |
| case $.RTC: { | |
| rbStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.RT: | |
| case $.RP: { | |
| rtStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.PRE: | |
| case $.LISTING: { | |
| preStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.XMP: { | |
| xmpStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.SVG: { | |
| svgStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.HTML: { | |
| htmlStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.BASE: | |
| case $.LINK: | |
| case $.META: | |
| case $.STYLE: | |
| case $.TITLE: | |
| case $.SCRIPT: | |
| case $.BGSOUND: | |
| case $.BASEFONT: | |
| case $.TEMPLATE: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| case $.BODY: { | |
| bodyStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.FORM: { | |
| formStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.NOBR: { | |
| nobrStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.MATH: { | |
| mathStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.TABLE: { | |
| tableStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.INPUT: { | |
| inputStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.PARAM: | |
| case $.TRACK: | |
| case $.SOURCE: { | |
| paramStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.IMAGE: { | |
| imageStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.BUTTON: { | |
| buttonStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.APPLET: | |
| case $.OBJECT: | |
| case $.MARQUEE: { | |
| appletStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.IFRAME: { | |
| iframeStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.SELECT: { | |
| selectStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.OPTION: | |
| case $.OPTGROUP: { | |
| optgroupStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.NOEMBED: | |
| case $.NOFRAMES: { | |
| rawTextStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.FRAMESET: { | |
| framesetStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.TEXTAREA: { | |
| textareaStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.NOSCRIPT: { | |
| if (p.options.scriptingEnabled) { | |
| rawTextStartTagInBody(p, token); | |
| } | |
| else { | |
| genericStartTagInBody(p, token); | |
| } | |
| break; | |
| } | |
| case $.PLAINTEXT: { | |
| plaintextStartTagInBody(p, token); | |
| break; | |
| } | |
| case $.COL: | |
| case $.TH: | |
| case $.TD: | |
| case $.TR: | |
| case $.HEAD: | |
| case $.FRAME: | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: | |
| case $.CAPTION: | |
| case $.COLGROUP: { | |
| // Ignore token | |
| break; | |
| } | |
| default: { | |
| genericStartTagInBody(p, token); | |
| } | |
| } | |
| } | |
| function bodyEndTagInBody(p, token) { | |
| if (p.openElements.hasInScope($.BODY)) { | |
| p.insertionMode = InsertionMode.AFTER_BODY; | |
| //NOTE: <body> is never popped from the stack, so we need to updated | |
| //the end location explicitly. | |
| if (p.options.sourceCodeLocationInfo) { | |
| const bodyElement = p.openElements.tryPeekProperlyNestedBodyElement(); | |
| if (bodyElement) { | |
| p._setEndLocation(bodyElement, token); | |
| } | |
| } | |
| } | |
| } | |
| function htmlEndTagInBody(p, token) { | |
| if (p.openElements.hasInScope($.BODY)) { | |
| p.insertionMode = InsertionMode.AFTER_BODY; | |
| endTagAfterBody(p, token); | |
| } | |
| } | |
| function addressEndTagInBody(p, token) { | |
| const tn = token.tagID; | |
| if (p.openElements.hasInScope(tn)) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilTagNamePopped(tn); | |
| } | |
| } | |
| function formEndTagInBody(p) { | |
| const inTemplate = p.openElements.tmplCount > 0; | |
| const { formElement } = p; | |
| if (!inTemplate) { | |
| p.formElement = null; | |
| } | |
| if ((formElement || inTemplate) && p.openElements.hasInScope($.FORM)) { | |
| p.openElements.generateImpliedEndTags(); | |
| if (inTemplate) { | |
| p.openElements.popUntilTagNamePopped($.FORM); | |
| } | |
| else if (formElement) { | |
| p.openElements.remove(formElement); | |
| } | |
| } | |
| } | |
| function pEndTagInBody(p) { | |
| if (!p.openElements.hasInButtonScope($.P)) { | |
| p._insertFakeElement(TN.P, $.P); | |
| } | |
| p._closePElement(); | |
| } | |
| function liEndTagInBody(p) { | |
| if (p.openElements.hasInListItemScope($.LI)) { | |
| p.openElements.generateImpliedEndTagsWithExclusion($.LI); | |
| p.openElements.popUntilTagNamePopped($.LI); | |
| } | |
| } | |
| function ddEndTagInBody(p, token) { | |
| const tn = token.tagID; | |
| if (p.openElements.hasInScope(tn)) { | |
| p.openElements.generateImpliedEndTagsWithExclusion(tn); | |
| p.openElements.popUntilTagNamePopped(tn); | |
| } | |
| } | |
| function numberedHeaderEndTagInBody(p) { | |
| if (p.openElements.hasNumberedHeaderInScope()) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilNumberedHeaderPopped(); | |
| } | |
| } | |
| function appletEndTagInBody(p, token) { | |
| const tn = token.tagID; | |
| if (p.openElements.hasInScope(tn)) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilTagNamePopped(tn); | |
| p.activeFormattingElements.clearToLastMarker(); | |
| } | |
| } | |
| function brEndTagInBody(p) { | |
| p._reconstructActiveFormattingElements(); | |
| p._insertFakeElement(TN.BR, $.BR); | |
| p.openElements.pop(); | |
| p.framesetOk = false; | |
| } | |
| function genericEndTagInBody(p, token) { | |
| const tn = token.tagName; | |
| const tid = token.tagID; | |
| for (let i = p.openElements.stackTop; i > 0; i--) { | |
| const element = p.openElements.items[i]; | |
| const elementId = p.openElements.tagIDs[i]; | |
| // Compare the tag name here, as the tag might not be a known tag with an ID. | |
| if (tid === elementId && (tid !== $.UNKNOWN || p.treeAdapter.getTagName(element) === tn)) { | |
| p.openElements.generateImpliedEndTagsWithExclusion(tid); | |
| if (p.openElements.stackTop >= i) | |
| p.openElements.shortenToLength(i); | |
| break; | |
| } | |
| if (p._isSpecialElement(element, elementId)) { | |
| break; | |
| } | |
| } | |
| } | |
| function endTagInBody(p, token) { | |
| switch (token.tagID) { | |
| case $.A: | |
| case $.B: | |
| case $.I: | |
| case $.S: | |
| case $.U: | |
| case $.EM: | |
| case $.TT: | |
| case $.BIG: | |
| case $.CODE: | |
| case $.FONT: | |
| case $.NOBR: | |
| case $.SMALL: | |
| case $.STRIKE: | |
| case $.STRONG: { | |
| callAdoptionAgency(p, token); | |
| break; | |
| } | |
| case $.P: { | |
| pEndTagInBody(p); | |
| break; | |
| } | |
| case $.DL: | |
| case $.UL: | |
| case $.OL: | |
| case $.DIR: | |
| case $.DIV: | |
| case $.NAV: | |
| case $.PRE: | |
| case $.MAIN: | |
| case $.MENU: | |
| case $.ASIDE: | |
| case $.BUTTON: | |
| case $.CENTER: | |
| case $.FIGURE: | |
| case $.FOOTER: | |
| case $.HEADER: | |
| case $.HGROUP: | |
| case $.DIALOG: | |
| case $.ADDRESS: | |
| case $.ARTICLE: | |
| case $.DETAILS: | |
| case $.SEARCH: | |
| case $.SECTION: | |
| case $.SUMMARY: | |
| case $.LISTING: | |
| case $.FIELDSET: | |
| case $.BLOCKQUOTE: | |
| case $.FIGCAPTION: { | |
| addressEndTagInBody(p, token); | |
| break; | |
| } | |
| case $.LI: { | |
| liEndTagInBody(p); | |
| break; | |
| } | |
| case $.DD: | |
| case $.DT: { | |
| ddEndTagInBody(p, token); | |
| break; | |
| } | |
| case $.H1: | |
| case $.H2: | |
| case $.H3: | |
| case $.H4: | |
| case $.H5: | |
| case $.H6: { | |
| numberedHeaderEndTagInBody(p); | |
| break; | |
| } | |
| case $.BR: { | |
| brEndTagInBody(p); | |
| break; | |
| } | |
| case $.BODY: { | |
| bodyEndTagInBody(p, token); | |
| break; | |
| } | |
| case $.HTML: { | |
| htmlEndTagInBody(p, token); | |
| break; | |
| } | |
| case $.FORM: { | |
| formEndTagInBody(p); | |
| break; | |
| } | |
| case $.APPLET: | |
| case $.OBJECT: | |
| case $.MARQUEE: { | |
| appletEndTagInBody(p, token); | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| templateEndTagInHead(p, token); | |
| break; | |
| } | |
| default: { | |
| genericEndTagInBody(p, token); | |
| } | |
| } | |
| } | |
| function eofInBody(p, token) { | |
| if (p.tmplInsertionModeStack.length > 0) { | |
| eofInTemplate(p, token); | |
| } | |
| else { | |
| stopParsing(p, token); | |
| } | |
| } | |
| // The "text" insertion mode | |
| //------------------------------------------------------------------ | |
| function endTagInText(p, token) { | |
| var _a; | |
| if (token.tagID === $.SCRIPT) { | |
| (_a = p.scriptHandler) === null || _a === void 0 ? void 0 : _a.call(p, p.openElements.current); | |
| } | |
| p.openElements.pop(); | |
| p.insertionMode = p.originalInsertionMode; | |
| } | |
| function eofInText(p, token) { | |
| p._err(token, ERR.eofInElementThatCanContainOnlyText); | |
| p.openElements.pop(); | |
| p.insertionMode = p.originalInsertionMode; | |
| p.onEof(token); | |
| } | |
| // The "in table" insertion mode | |
| //------------------------------------------------------------------ | |
| function characterInTable(p, token) { | |
| if (p.openElements.currentTagId !== undefined && TABLE_STRUCTURE_TAGS.has(p.openElements.currentTagId)) { | |
| p.pendingCharacterTokens.length = 0; | |
| p.hasNonWhitespacePendingCharacterToken = false; | |
| p.originalInsertionMode = p.insertionMode; | |
| p.insertionMode = InsertionMode.IN_TABLE_TEXT; | |
| switch (token.type) { | |
| case TokenType.CHARACTER: { | |
| characterInTableText(p, token); | |
| break; | |
| } | |
| case TokenType.WHITESPACE_CHARACTER: { | |
| whitespaceCharacterInTableText(p, token); | |
| break; | |
| } | |
| // Ignore null | |
| } | |
| } | |
| else { | |
| tokenInTable(p, token); | |
| } | |
| } | |
| function captionStartTagInTable(p, token) { | |
| p.openElements.clearBackToTableContext(); | |
| p.activeFormattingElements.insertMarker(); | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_CAPTION; | |
| } | |
| function colgroupStartTagInTable(p, token) { | |
| p.openElements.clearBackToTableContext(); | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_COLUMN_GROUP; | |
| } | |
| function colStartTagInTable(p, token) { | |
| p.openElements.clearBackToTableContext(); | |
| p._insertFakeElement(TN.COLGROUP, $.COLGROUP); | |
| p.insertionMode = InsertionMode.IN_COLUMN_GROUP; | |
| startTagInColumnGroup(p, token); | |
| } | |
| function tbodyStartTagInTable(p, token) { | |
| p.openElements.clearBackToTableContext(); | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| } | |
| function tdStartTagInTable(p, token) { | |
| p.openElements.clearBackToTableContext(); | |
| p._insertFakeElement(TN.TBODY, $.TBODY); | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| startTagInTableBody(p, token); | |
| } | |
| function tableStartTagInTable(p, token) { | |
| if (p.openElements.hasInTableScope($.TABLE)) { | |
| p.openElements.popUntilTagNamePopped($.TABLE); | |
| p._resetInsertionMode(); | |
| p._processStartTag(token); | |
| } | |
| } | |
| function inputStartTagInTable(p, token) { | |
| if (isHiddenInput(token)) { | |
| p._appendElement(token, NS.HTML); | |
| } | |
| else { | |
| tokenInTable(p, token); | |
| } | |
| token.ackSelfClosing = true; | |
| } | |
| function formStartTagInTable(p, token) { | |
| if (!p.formElement && p.openElements.tmplCount === 0) { | |
| p._insertElement(token, NS.HTML); | |
| p.formElement = p.openElements.current; | |
| p.openElements.pop(); | |
| } | |
| } | |
| function startTagInTable(p, token) { | |
| switch (token.tagID) { | |
| case $.TD: | |
| case $.TH: | |
| case $.TR: { | |
| tdStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.STYLE: | |
| case $.SCRIPT: | |
| case $.TEMPLATE: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| case $.COL: { | |
| colStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.FORM: { | |
| formStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.TABLE: { | |
| tableStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: { | |
| tbodyStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.INPUT: { | |
| inputStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.CAPTION: { | |
| captionStartTagInTable(p, token); | |
| break; | |
| } | |
| case $.COLGROUP: { | |
| colgroupStartTagInTable(p, token); | |
| break; | |
| } | |
| default: { | |
| tokenInTable(p, token); | |
| } | |
| } | |
| } | |
| function endTagInTable(p, token) { | |
| switch (token.tagID) { | |
| case $.TABLE: { | |
| if (p.openElements.hasInTableScope($.TABLE)) { | |
| p.openElements.popUntilTagNamePopped($.TABLE); | |
| p._resetInsertionMode(); | |
| } | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| templateEndTagInHead(p, token); | |
| break; | |
| } | |
| case $.BODY: | |
| case $.CAPTION: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.HTML: | |
| case $.TBODY: | |
| case $.TD: | |
| case $.TFOOT: | |
| case $.TH: | |
| case $.THEAD: | |
| case $.TR: { | |
| // Ignore token | |
| break; | |
| } | |
| default: { | |
| tokenInTable(p, token); | |
| } | |
| } | |
| } | |
| function tokenInTable(p, token) { | |
| const savedFosterParentingState = p.fosterParentingEnabled; | |
| p.fosterParentingEnabled = true; | |
| // Process token in `In Body` mode | |
| modeInBody(p, token); | |
| p.fosterParentingEnabled = savedFosterParentingState; | |
| } | |
| // The "in table text" insertion mode | |
| //------------------------------------------------------------------ | |
| function whitespaceCharacterInTableText(p, token) { | |
| p.pendingCharacterTokens.push(token); | |
| } | |
| function characterInTableText(p, token) { | |
| p.pendingCharacterTokens.push(token); | |
| p.hasNonWhitespacePendingCharacterToken = true; | |
| } | |
| function tokenInTableText(p, token) { | |
| let i = 0; | |
| if (p.hasNonWhitespacePendingCharacterToken) { | |
| for (; i < p.pendingCharacterTokens.length; i++) { | |
| tokenInTable(p, p.pendingCharacterTokens[i]); | |
| } | |
| } | |
| else { | |
| for (; i < p.pendingCharacterTokens.length; i++) { | |
| p._insertCharacters(p.pendingCharacterTokens[i]); | |
| } | |
| } | |
| p.insertionMode = p.originalInsertionMode; | |
| p._processToken(token); | |
| } | |
| // The "in caption" insertion mode | |
| //------------------------------------------------------------------ | |
| const TABLE_VOID_ELEMENTS = new Set([$.CAPTION, $.COL, $.COLGROUP, $.TBODY, $.TD, $.TFOOT, $.TH, $.THEAD, $.TR]); | |
| function startTagInCaption(p, token) { | |
| const tn = token.tagID; | |
| if (TABLE_VOID_ELEMENTS.has(tn)) { | |
| if (p.openElements.hasInTableScope($.CAPTION)) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilTagNamePopped($.CAPTION); | |
| p.activeFormattingElements.clearToLastMarker(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| startTagInTable(p, token); | |
| } | |
| } | |
| else { | |
| startTagInBody(p, token); | |
| } | |
| } | |
| function endTagInCaption(p, token) { | |
| const tn = token.tagID; | |
| switch (tn) { | |
| case $.CAPTION: | |
| case $.TABLE: { | |
| if (p.openElements.hasInTableScope($.CAPTION)) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilTagNamePopped($.CAPTION); | |
| p.activeFormattingElements.clearToLastMarker(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| if (tn === $.TABLE) { | |
| endTagInTable(p, token); | |
| } | |
| } | |
| break; | |
| } | |
| case $.BODY: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.HTML: | |
| case $.TBODY: | |
| case $.TD: | |
| case $.TFOOT: | |
| case $.TH: | |
| case $.THEAD: | |
| case $.TR: { | |
| // Ignore token | |
| break; | |
| } | |
| default: { | |
| endTagInBody(p, token); | |
| } | |
| } | |
| } | |
| // The "in column group" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInColumnGroup(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.COL: { | |
| p._appendElement(token, NS.HTML); | |
| token.ackSelfClosing = true; | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| default: { | |
| tokenInColumnGroup(p, token); | |
| } | |
| } | |
| } | |
| function endTagInColumnGroup(p, token) { | |
| switch (token.tagID) { | |
| case $.COLGROUP: { | |
| if (p.openElements.currentTagId === $.COLGROUP) { | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| } | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| templateEndTagInHead(p, token); | |
| break; | |
| } | |
| case $.COL: { | |
| // Ignore token | |
| break; | |
| } | |
| default: { | |
| tokenInColumnGroup(p, token); | |
| } | |
| } | |
| } | |
| function tokenInColumnGroup(p, token) { | |
| if (p.openElements.currentTagId === $.COLGROUP) { | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| p._processToken(token); | |
| } | |
| } | |
| // The "in table body" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInTableBody(p, token) { | |
| switch (token.tagID) { | |
| case $.TR: { | |
| p.openElements.clearBackToTableBodyContext(); | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_ROW; | |
| break; | |
| } | |
| case $.TH: | |
| case $.TD: { | |
| p.openElements.clearBackToTableBodyContext(); | |
| p._insertFakeElement(TN.TR, $.TR); | |
| p.insertionMode = InsertionMode.IN_ROW; | |
| startTagInRow(p, token); | |
| break; | |
| } | |
| case $.CAPTION: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: { | |
| if (p.openElements.hasTableBodyContextInTableScope()) { | |
| p.openElements.clearBackToTableBodyContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| startTagInTable(p, token); | |
| } | |
| break; | |
| } | |
| default: { | |
| startTagInTable(p, token); | |
| } | |
| } | |
| } | |
| function endTagInTableBody(p, token) { | |
| const tn = token.tagID; | |
| switch (token.tagID) { | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: { | |
| if (p.openElements.hasInTableScope(tn)) { | |
| p.openElements.clearBackToTableBodyContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| } | |
| break; | |
| } | |
| case $.TABLE: { | |
| if (p.openElements.hasTableBodyContextInTableScope()) { | |
| p.openElements.clearBackToTableBodyContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| endTagInTable(p, token); | |
| } | |
| break; | |
| } | |
| case $.BODY: | |
| case $.CAPTION: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.HTML: | |
| case $.TD: | |
| case $.TH: | |
| case $.TR: { | |
| // Ignore token | |
| break; | |
| } | |
| default: { | |
| endTagInTable(p, token); | |
| } | |
| } | |
| } | |
| // The "in row" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInRow(p, token) { | |
| switch (token.tagID) { | |
| case $.TH: | |
| case $.TD: { | |
| p.openElements.clearBackToTableRowContext(); | |
| p._insertElement(token, NS.HTML); | |
| p.insertionMode = InsertionMode.IN_CELL; | |
| p.activeFormattingElements.insertMarker(); | |
| break; | |
| } | |
| case $.CAPTION: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: | |
| case $.TR: { | |
| if (p.openElements.hasInTableScope($.TR)) { | |
| p.openElements.clearBackToTableRowContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| startTagInTableBody(p, token); | |
| } | |
| break; | |
| } | |
| default: { | |
| startTagInTable(p, token); | |
| } | |
| } | |
| } | |
| function endTagInRow(p, token) { | |
| switch (token.tagID) { | |
| case $.TR: { | |
| if (p.openElements.hasInTableScope($.TR)) { | |
| p.openElements.clearBackToTableRowContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| } | |
| break; | |
| } | |
| case $.TABLE: { | |
| if (p.openElements.hasInTableScope($.TR)) { | |
| p.openElements.clearBackToTableRowContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| endTagInTableBody(p, token); | |
| } | |
| break; | |
| } | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: { | |
| if (p.openElements.hasInTableScope(token.tagID) || p.openElements.hasInTableScope($.TR)) { | |
| p.openElements.clearBackToTableRowContext(); | |
| p.openElements.pop(); | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| endTagInTableBody(p, token); | |
| } | |
| break; | |
| } | |
| case $.BODY: | |
| case $.CAPTION: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.HTML: | |
| case $.TD: | |
| case $.TH: { | |
| // Ignore end tag | |
| break; | |
| } | |
| default: { | |
| endTagInTable(p, token); | |
| } | |
| } | |
| } | |
| // The "in cell" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInCell(p, token) { | |
| const tn = token.tagID; | |
| if (TABLE_VOID_ELEMENTS.has(tn)) { | |
| if (p.openElements.hasInTableScope($.TD) || p.openElements.hasInTableScope($.TH)) { | |
| p._closeTableCell(); | |
| startTagInRow(p, token); | |
| } | |
| } | |
| else { | |
| startTagInBody(p, token); | |
| } | |
| } | |
| function endTagInCell(p, token) { | |
| const tn = token.tagID; | |
| switch (tn) { | |
| case $.TD: | |
| case $.TH: { | |
| if (p.openElements.hasInTableScope(tn)) { | |
| p.openElements.generateImpliedEndTags(); | |
| p.openElements.popUntilTagNamePopped(tn); | |
| p.activeFormattingElements.clearToLastMarker(); | |
| p.insertionMode = InsertionMode.IN_ROW; | |
| } | |
| break; | |
| } | |
| case $.TABLE: | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: | |
| case $.TR: { | |
| if (p.openElements.hasInTableScope(tn)) { | |
| p._closeTableCell(); | |
| endTagInRow(p, token); | |
| } | |
| break; | |
| } | |
| case $.BODY: | |
| case $.CAPTION: | |
| case $.COL: | |
| case $.COLGROUP: | |
| case $.HTML: { | |
| // Ignore token | |
| break; | |
| } | |
| default: { | |
| endTagInBody(p, token); | |
| } | |
| } | |
| } | |
| // The "in select" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInSelect(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.OPTION: { | |
| if (p.openElements.currentTagId === $.OPTION) { | |
| p.openElements.pop(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| break; | |
| } | |
| case $.OPTGROUP: { | |
| if (p.openElements.currentTagId === $.OPTION) { | |
| p.openElements.pop(); | |
| } | |
| if (p.openElements.currentTagId === $.OPTGROUP) { | |
| p.openElements.pop(); | |
| } | |
| p._insertElement(token, NS.HTML); | |
| break; | |
| } | |
| case $.HR: { | |
| if (p.openElements.currentTagId === $.OPTION) { | |
| p.openElements.pop(); | |
| } | |
| if (p.openElements.currentTagId === $.OPTGROUP) { | |
| p.openElements.pop(); | |
| } | |
| p._appendElement(token, NS.HTML); | |
| token.ackSelfClosing = true; | |
| break; | |
| } | |
| case $.INPUT: | |
| case $.KEYGEN: | |
| case $.TEXTAREA: | |
| case $.SELECT: { | |
| if (p.openElements.hasInSelectScope($.SELECT)) { | |
| p.openElements.popUntilTagNamePopped($.SELECT); | |
| p._resetInsertionMode(); | |
| if (token.tagID !== $.SELECT) { | |
| p._processStartTag(token); | |
| } | |
| } | |
| break; | |
| } | |
| case $.SCRIPT: | |
| case $.TEMPLATE: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| function endTagInSelect(p, token) { | |
| switch (token.tagID) { | |
| case $.OPTGROUP: { | |
| if (p.openElements.stackTop > 0 && | |
| p.openElements.currentTagId === $.OPTION && | |
| p.openElements.tagIDs[p.openElements.stackTop - 1] === $.OPTGROUP) { | |
| p.openElements.pop(); | |
| } | |
| if (p.openElements.currentTagId === $.OPTGROUP) { | |
| p.openElements.pop(); | |
| } | |
| break; | |
| } | |
| case $.OPTION: { | |
| if (p.openElements.currentTagId === $.OPTION) { | |
| p.openElements.pop(); | |
| } | |
| break; | |
| } | |
| case $.SELECT: { | |
| if (p.openElements.hasInSelectScope($.SELECT)) { | |
| p.openElements.popUntilTagNamePopped($.SELECT); | |
| p._resetInsertionMode(); | |
| } | |
| break; | |
| } | |
| case $.TEMPLATE: { | |
| templateEndTagInHead(p, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| // The "in select in table" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInSelectInTable(p, token) { | |
| const tn = token.tagID; | |
| if (tn === $.CAPTION || | |
| tn === $.TABLE || | |
| tn === $.TBODY || | |
| tn === $.TFOOT || | |
| tn === $.THEAD || | |
| tn === $.TR || | |
| tn === $.TD || | |
| tn === $.TH) { | |
| p.openElements.popUntilTagNamePopped($.SELECT); | |
| p._resetInsertionMode(); | |
| p._processStartTag(token); | |
| } | |
| else { | |
| startTagInSelect(p, token); | |
| } | |
| } | |
| function endTagInSelectInTable(p, token) { | |
| const tn = token.tagID; | |
| if (tn === $.CAPTION || | |
| tn === $.TABLE || | |
| tn === $.TBODY || | |
| tn === $.TFOOT || | |
| tn === $.THEAD || | |
| tn === $.TR || | |
| tn === $.TD || | |
| tn === $.TH) { | |
| if (p.openElements.hasInTableScope(tn)) { | |
| p.openElements.popUntilTagNamePopped($.SELECT); | |
| p._resetInsertionMode(); | |
| p.onEndTag(token); | |
| } | |
| } | |
| else { | |
| endTagInSelect(p, token); | |
| } | |
| } | |
| // The "in template" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInTemplate(p, token) { | |
| switch (token.tagID) { | |
| // First, handle tags that can start without a mode change | |
| case $.BASE: | |
| case $.BASEFONT: | |
| case $.BGSOUND: | |
| case $.LINK: | |
| case $.META: | |
| case $.NOFRAMES: | |
| case $.SCRIPT: | |
| case $.STYLE: | |
| case $.TEMPLATE: | |
| case $.TITLE: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| // Re-process the token in the appropriate mode | |
| case $.CAPTION: | |
| case $.COLGROUP: | |
| case $.TBODY: | |
| case $.TFOOT: | |
| case $.THEAD: { | |
| p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE; | |
| p.insertionMode = InsertionMode.IN_TABLE; | |
| startTagInTable(p, token); | |
| break; | |
| } | |
| case $.COL: { | |
| p.tmplInsertionModeStack[0] = InsertionMode.IN_COLUMN_GROUP; | |
| p.insertionMode = InsertionMode.IN_COLUMN_GROUP; | |
| startTagInColumnGroup(p, token); | |
| break; | |
| } | |
| case $.TR: { | |
| p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE_BODY; | |
| p.insertionMode = InsertionMode.IN_TABLE_BODY; | |
| startTagInTableBody(p, token); | |
| break; | |
| } | |
| case $.TD: | |
| case $.TH: { | |
| p.tmplInsertionModeStack[0] = InsertionMode.IN_ROW; | |
| p.insertionMode = InsertionMode.IN_ROW; | |
| startTagInRow(p, token); | |
| break; | |
| } | |
| default: { | |
| p.tmplInsertionModeStack[0] = InsertionMode.IN_BODY; | |
| p.insertionMode = InsertionMode.IN_BODY; | |
| startTagInBody(p, token); | |
| } | |
| } | |
| } | |
| function endTagInTemplate(p, token) { | |
| if (token.tagID === $.TEMPLATE) { | |
| templateEndTagInHead(p, token); | |
| } | |
| } | |
| function eofInTemplate(p, token) { | |
| if (p.openElements.tmplCount > 0) { | |
| p.openElements.popUntilTagNamePopped($.TEMPLATE); | |
| p.activeFormattingElements.clearToLastMarker(); | |
| p.tmplInsertionModeStack.shift(); | |
| p._resetInsertionMode(); | |
| p.onEof(token); | |
| } | |
| else { | |
| stopParsing(p, token); | |
| } | |
| } | |
| // The "after body" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagAfterBody(p, token) { | |
| if (token.tagID === $.HTML) { | |
| startTagInBody(p, token); | |
| } | |
| else { | |
| tokenAfterBody(p, token); | |
| } | |
| } | |
| function endTagAfterBody(p, token) { | |
| var _a; | |
| if (token.tagID === $.HTML) { | |
| if (!p.fragmentContext) { | |
| p.insertionMode = InsertionMode.AFTER_AFTER_BODY; | |
| } | |
| //NOTE: <html> is never popped from the stack, so we need to updated | |
| //the end location explicitly. | |
| if (p.options.sourceCodeLocationInfo && p.openElements.tagIDs[0] === $.HTML) { | |
| p._setEndLocation(p.openElements.items[0], token); | |
| // Update the body element, if it doesn't have an end tag | |
| const bodyElement = p.openElements.items[1]; | |
| if (bodyElement && !((_a = p.treeAdapter.getNodeSourceCodeLocation(bodyElement)) === null || _a === void 0 ? void 0 : _a.endTag)) { | |
| p._setEndLocation(bodyElement, token); | |
| } | |
| } | |
| } | |
| else { | |
| tokenAfterBody(p, token); | |
| } | |
| } | |
| function tokenAfterBody(p, token) { | |
| p.insertionMode = InsertionMode.IN_BODY; | |
| modeInBody(p, token); | |
| } | |
| // The "in frameset" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagInFrameset(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.FRAMESET: { | |
| p._insertElement(token, NS.HTML); | |
| break; | |
| } | |
| case $.FRAME: { | |
| p._appendElement(token, NS.HTML); | |
| token.ackSelfClosing = true; | |
| break; | |
| } | |
| case $.NOFRAMES: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| function endTagInFrameset(p, token) { | |
| if (token.tagID === $.FRAMESET && !p.openElements.isRootHtmlElementCurrent()) { | |
| p.openElements.pop(); | |
| if (!p.fragmentContext && p.openElements.currentTagId !== $.FRAMESET) { | |
| p.insertionMode = InsertionMode.AFTER_FRAMESET; | |
| } | |
| } | |
| } | |
| // The "after frameset" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagAfterFrameset(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.NOFRAMES: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| function endTagAfterFrameset(p, token) { | |
| if (token.tagID === $.HTML) { | |
| p.insertionMode = InsertionMode.AFTER_AFTER_FRAMESET; | |
| } | |
| } | |
| // The "after after body" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagAfterAfterBody(p, token) { | |
| if (token.tagID === $.HTML) { | |
| startTagInBody(p, token); | |
| } | |
| else { | |
| tokenAfterAfterBody(p, token); | |
| } | |
| } | |
| function tokenAfterAfterBody(p, token) { | |
| p.insertionMode = InsertionMode.IN_BODY; | |
| modeInBody(p, token); | |
| } | |
| // The "after after frameset" insertion mode | |
| //------------------------------------------------------------------ | |
| function startTagAfterAfterFrameset(p, token) { | |
| switch (token.tagID) { | |
| case $.HTML: { | |
| startTagInBody(p, token); | |
| break; | |
| } | |
| case $.NOFRAMES: { | |
| startTagInHead(p, token); | |
| break; | |
| } | |
| default: | |
| // Do nothing | |
| } | |
| } | |
| // The rules for parsing tokens in foreign content | |
| //------------------------------------------------------------------ | |
| function nullCharacterInForeignContent(p, token) { | |
| token.chars = unicode.REPLACEMENT_CHARACTER; | |
| p._insertCharacters(token); | |
| } | |
| function characterInForeignContent(p, token) { | |
| p._insertCharacters(token); | |
| p.framesetOk = false; | |
| } | |
| function popUntilHtmlOrIntegrationPoint(p) { | |
| while (p.treeAdapter.getNamespaceURI(p.openElements.current) !== NS.HTML && | |
| p.openElements.currentTagId !== undefined && | |
| !p._isIntegrationPoint(p.openElements.currentTagId, p.openElements.current)) { | |
| p.openElements.pop(); | |
| } | |
| } | |
| function startTagInForeignContent(p, token) { | |
| if (foreignContent.causesExit(token)) { | |
| popUntilHtmlOrIntegrationPoint(p); | |
| p._startTagOutsideForeignContent(token); | |
| } | |
| else { | |
| const current = p._getAdjustedCurrentElement(); | |
| const currentNs = p.treeAdapter.getNamespaceURI(current); | |
| if (currentNs === NS.MATHML) { | |
| foreignContent.adjustTokenMathMLAttrs(token); | |
| } | |
| else if (currentNs === NS.SVG) { | |
| foreignContent.adjustTokenSVGTagName(token); | |
| foreignContent.adjustTokenSVGAttrs(token); | |
| } | |
| foreignContent.adjustTokenXMLAttrs(token); | |
| if (token.selfClosing) { | |
| p._appendElement(token, currentNs); | |
| } | |
| else { | |
| p._insertElement(token, currentNs); | |
| } | |
| token.ackSelfClosing = true; | |
| } | |
| } | |
| function endTagInForeignContent(p, token) { | |
| if (token.tagID === $.P || token.tagID === $.BR) { | |
| popUntilHtmlOrIntegrationPoint(p); | |
| p._endTagOutsideForeignContent(token); | |
| return; | |
| } | |
| for (let i = p.openElements.stackTop; i > 0; i--) { | |
| const element = p.openElements.items[i]; | |
| if (p.treeAdapter.getNamespaceURI(element) === NS.HTML) { | |
| p._endTagOutsideForeignContent(token); | |
| break; | |
| } | |
| const tagName = p.treeAdapter.getTagName(element); | |
| if (tagName.toLowerCase() === token.tagName) { | |
| //NOTE: update the token tag name for `_setEndLocation`. | |
| token.tagName = tagName; | |
| p.openElements.shortenToLength(i); | |
| break; | |
| } | |
| } | |
| } | |