UNPKG

happy-dom

Version:

Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.

655 lines 31.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const PropertySymbol = __importStar(require("../PropertySymbol.cjs")); const NamespaceURI_js_1 = __importDefault(require("../config/NamespaceURI.cjs")); const Node_js_1 = __importDefault(require("../nodes/node/Node.cjs")); const XMLEncodeUtility_js_1 = __importDefault(require("../utilities/XMLEncodeUtility.cjs")); const NodeFactory_js_1 = __importDefault(require("../nodes/NodeFactory.cjs")); /** * Markup RegExp. * * Group 1: Beginning of start tag (e.g. "div" in "<div"). * Group 2: End tag (e.g. "div" in "</div>"). * Group 3: Comment with ending "--" (e.g. " Comment 1 " in "<!-- Comment 1 -->"). * Group 4: Comment without ending "--" (e.g. " Comment 1 " in "<!-- Comment 1 >"). * Group 5: Exclamation mark comment (e.g. "DOCTYPE html" in "<!DOCTYPE html>"). * Group 6: Processing instruction (e.g. "xml version="1.0"?" in "<?xml version="1.0"?>"). * Group 7: End of self closing start tag (e.g. "/>" in "<img/>"). * Group 8: End of start tag (e.g. ">" in "<div>"). */ const MARKUP_REGEXP = /<([^\s/!>?]+)|<\/([^\s/!>?]+)\s*>|(<!--)|(-->)|(<!)|(<\?)|(\/>)|(>)/gm; /** * Attribute RegExp. * * Group 1: Attribute name when the attribute has a value using double apostrophe (e.g. "name" in "<div name="value">"). * Group 2: Attribute value when the attribute has a value using double apostrophe (e.g. "value" in "<div name="value">"). * Group 3: Attribute end apostrophe when the attribute has a value using double apostrophe (e.g. '"' in "<div name="value">"). * Group 4: Attribute name when the attribute has a value using single apostrophe (e.g. "name" in "<div name='value'>"). * Group 5: Attribute value when the attribute has a value using single apostrophe (e.g. "value" in "<div name='value'>"). * Group 6: Attribute end apostrophe when the attribute has a value using single apostrophe (e.g. "'" in "<div name='value'>"). * Group 7: Attribute name when the attribute has no value (e.g. "disabled" in "<div disabled>"). */ const ATTRIBUTE_REGEXP = /\s*([a-zA-Z0-9-_:]+)\s*=\s*"([^"]*)("{0,1})|\s*([a-zA-Z0-9-_:]+)\s*=\s*'([^']*)('{0,1})/gm; /** * Attribute without value RegExp. */ const ATTRIBUTE_WITHOUT_VALUE_REGEXP = /^\s*([a-zA-Z0-9-_:]+)$/; /** * XML processing instruction version RegExp. */ const XML_PROCESSING_INSTRUCTION_VERSION_REGEXP = /version="[^"]+"/; /** * Document type attribute RegExp. * * Group 1: Attribute value. */ const DOCUMENT_TYPE_ATTRIBUTE_REGEXP = /"([^"]+)"/gm; /** * Space RegExp. */ const SPACE_REGEXP = /\s+/; /** * New line RegExp. */ const NEW_LINE_REGEXP = /\n/g; /** * Markup read state (which state the parser is in). */ var MarkupReadStateEnum; (function (MarkupReadStateEnum) { MarkupReadStateEnum["any"] = "any"; MarkupReadStateEnum["startTag"] = "startTag"; MarkupReadStateEnum["comment"] = "comment"; MarkupReadStateEnum["documentType"] = "documentType"; MarkupReadStateEnum["processingInstruction"] = "processingInstruction"; MarkupReadStateEnum["error"] = "error"; })(MarkupReadStateEnum || (MarkupReadStateEnum = {})); const NAMESPACE_URIS = Object.values(NamespaceURI_js_1.default); /** * XML parser. */ class XMLParser { window; rootNode = null; nodeStack = []; tagNameStack = []; defaultNamespaceStack = null; namespacePrefixStack = null; startTagIndex = 0; markupRegExp = null; lastIndex = 0; errorIndex = 0; nextElement = null; nextTagName = null; currentNode = null; readState = MarkupReadStateEnum.any; errorMessage = null; /** * Constructor. * * @param window Window. * @param [options] Options. * @param [options.mode] Mode. Defaults to "htmlFragment". * @param [options.evaluateScripts] Set to "true" to enable script execution */ constructor(window) { this.window = window; } /** * Parses XML and returns an XML document containing nodes found. * * @param xml XML string. * @returns XML document. */ parse(xml) { this.rootNode = new this.window.XMLDocument(); this.nodeStack = [this.rootNode]; this.tagNameStack = [null]; this.currentNode = this.rootNode; this.readState = MarkupReadStateEnum.any; this.defaultNamespaceStack = [null]; this.namespacePrefixStack = [null]; this.startTagIndex = 0; this.errorIndex = 0; this.errorMessage = null; this.markupRegExp = new RegExp(MARKUP_REGEXP, 'gm'); this.lastIndex = 0; let match; this.rootNode[PropertySymbol.defaultView] = this.window; xml = String(xml); while ((match = this.markupRegExp.exec(xml))) { switch (this.readState) { case MarkupReadStateEnum.any: if (match.index !== this.lastIndex && (match[1] || match[2] || match[3] || match[4] || match[5] !== undefined || match[6])) { // Plain text between tags. this.parsePlainText(xml.substring(this.lastIndex, match.index)); } if (match[1]) { // Start tag. this.parseStartTag(match[1]); } else if (match[2]) { // End tag. if (!this.parseEndTag(match[2])) { this.errorMessage = `Opening and ending tag mismatch: ${this.tagNameStack[this.tagNameStack.length - 1]} line ${xml.substring(0, this.startTagIndex).split('\n').length} and ${match[2]}\n`; this.errorIndex = this.markupRegExp.lastIndex; this.readState = MarkupReadStateEnum.error; this.removeOverflowingTextNodes(); } } else if (match[3]) { // Comment. this.startTagIndex = this.markupRegExp.lastIndex; this.readState = MarkupReadStateEnum.comment; } else if (match[5] !== undefined) { // Document type this.startTagIndex = this.markupRegExp.lastIndex; this.readState = MarkupReadStateEnum.documentType; } else if (match[6]) { // Processing instruction. this.startTagIndex = this.markupRegExp.lastIndex; this.readState = MarkupReadStateEnum.processingInstruction; } else { // Plain text between tags, including the matched tag as it is not a valid start or end tag. this.parsePlainText(xml.substring(this.lastIndex, this.markupRegExp.lastIndex)); } break; case MarkupReadStateEnum.startTag: // End of start tag // match[7] is matching "/>" (e.g. "<img/>"). // match[8] is matching ">" (e.g. "<div>"). if (match[7] || match[8]) { const attributeString = xml.substring(this.startTagIndex, match[2] ? this.markupRegExp.lastIndex - 1 : match.index); const isSelfClosed = !!match[7]; this.parseEndOfStartTag(attributeString, isSelfClosed); } else { this.errorMessage = match[2] && this.lastIndex !== this.startTagIndex ? `Unescaped '&lt;' not allowed in attributes values\n` : 'error parsing attribute name\n'; this.errorIndex = match.index; this.readState = MarkupReadStateEnum.error; this.removeOverflowingTextNodes(); } break; case MarkupReadStateEnum.comment: // Comment end tag. if (match[4]) { this.parseComment(xml.substring(this.startTagIndex, match.index)); } break; case MarkupReadStateEnum.documentType: // Document type end tag. if (match[7] || match[8]) { this.parseDocumentType(xml.substring(this.startTagIndex, match.index)); } break; case MarkupReadStateEnum.processingInstruction: // Processing instruction end tag. if (match[7] || match[8]) { this.parseProcessingInstruction(xml.substring(this.startTagIndex, match.index)); } break; case MarkupReadStateEnum.error: this.parseError(xml.slice(0, this.errorIndex), this.errorMessage); return this.rootNode; } this.lastIndex = this.markupRegExp.lastIndex; } if (this.readState === MarkupReadStateEnum.error) { this.parseError(xml.slice(0, this.errorIndex), this.errorMessage); return this.rootNode; } if (this.readState === MarkupReadStateEnum.comment) { this.parseError(xml, 'Comment not terminated\n'); this.removeOverflowingTextNodes(); return this.rootNode; } // Missing start tag (e.g. when parsing just a string like "Test"). if (this.rootNode[PropertySymbol.elementArray].length === 0) { this.parseError('', `Start tag expected, '&lt;' not found`); return this.rootNode; } // Plain text after tags. if (this.lastIndex !== xml.length && this.currentNode) { this.parsePlainText(xml.substring(this.lastIndex)); } // Missing end tag. if (this.nodeStack.length !== 1) { this.parseError(xml, this.nextElement ? 'attributes construct error\n' : 'Premature end of data in tag article line 1\n'); return this.rootNode; } return this.rootNode; } /** * Parses plain text. * * @param text Text. */ parsePlainText(text) { if (this.currentNode === this.rootNode) { const xmlText = text.replace(SPACE_REGEXP, ''); if (xmlText) { this.errorMessage = 'Extra content at the end of the document\n'; this.errorIndex = this.lastIndex; this.readState = MarkupReadStateEnum.error; } } else if (text.includes('&nbsp;')) { this.errorMessage = `Entity 'nbsp' not defined\n`; this.errorIndex = this.lastIndex + text.indexOf('&nbsp;') + 6; this.readState = MarkupReadStateEnum.error; } else { this.currentNode[PropertySymbol.appendChild](this.rootNode.createTextNode(XMLEncodeUtility_js_1.default.decodeXMLEntities(text)), true); } } /** * Parses processing instruction. * * @param text Text. */ parseProcessingInstruction(text) { const parts = text.split(SPACE_REGEXP); const endsWithQuestionMark = text[text.length - 1] === '?'; if (parts[0] === 'xml') { if (this.currentNode !== this.rootNode || this.rootNode[PropertySymbol.nodeArray].length !== 0 || parts.length === 1) { this.errorMessage = 'XML declaration allowed only at the start of the document\n'; this.errorIndex = this.markupRegExp.lastIndex - text.length + 2; this.readState = MarkupReadStateEnum.error; this.removeOverflowingTextNodes(); } else if (!XML_PROCESSING_INSTRUCTION_VERSION_REGEXP.test(parts[1])) { this.errorMessage = 'Malformed declaration expecting version\n'; this.errorIndex = this.markupRegExp.lastIndex - text.length + 3; this.readState = MarkupReadStateEnum.error; } else if (!endsWithQuestionMark) { this.errorMessage = 'Blank needed here\n'; this.errorIndex = this.markupRegExp.lastIndex - 1; this.readState = MarkupReadStateEnum.error; } else { // When the processing instruction has "xml" as target, we should not add it as a child node. // Instead we will store the state on the root node, so that it is added when serializing the document with XMLSerializer. // TODO: We need to handle validation of encoding. const name = parts[0]; // We need to remove the ending "?". const content = parts.slice(1).join(' ').slice(0, -1); this.rootNode[PropertySymbol.xmlProcessingInstruction] = this.rootNode.createProcessingInstruction(name, content); this.readState = MarkupReadStateEnum.any; } } else { if (parts.length === 1 && !endsWithQuestionMark) { this.errorMessage = 'ParsePI: PI processing-instruction space expected\n'; this.errorIndex = this.markupRegExp.lastIndex - 1; this.readState = MarkupReadStateEnum.error; } else if (parts.length > 1 && !endsWithQuestionMark) { this.errorMessage = 'ParsePI: PI processing-instruction never end ...\n'; this.errorIndex = this.markupRegExp.lastIndex - 1; this.readState = MarkupReadStateEnum.error; } else { const name = parts[0]; // We need to remove the ending "?". const content = parts.slice(1).join(' ').slice(0, -1); this.currentNode[PropertySymbol.appendChild](this.rootNode.createProcessingInstruction(name, content), true); this.readState = MarkupReadStateEnum.any; } } } /** * Parses comment. * * @param comment Comment. */ parseComment(comment) { // Comments are not allowed in the root when parsing XML. if (this.currentNode !== this.rootNode) { this.currentNode[PropertySymbol.appendChild](this.rootNode.createComment(XMLEncodeUtility_js_1.default.decodeXMLEntities(comment)), true); } this.readState = MarkupReadStateEnum.any; } /** * Parses document type. * * @param text Text. */ parseDocumentType(text) { if (this.currentNode === this.rootNode && this.rootNode[PropertySymbol.nodeArray].length === 0) { const documentType = this.getDocumentType(XMLEncodeUtility_js_1.default.decodeXMLEntities(text)); if (documentType?.name) { this.rootNode[PropertySymbol.appendChild](this.window.document.implementation.createDocumentType(documentType.name, documentType.publicId, documentType.systemId), true); this.readState = MarkupReadStateEnum.any; } else if (documentType) { this.errorMessage = 'xmlParseDocTypeDecl : no DOCTYPE name\n'; this.errorIndex = this.markupRegExp.lastIndex - text.length - 2; this.readState = MarkupReadStateEnum.error; } else { this.errorMessage = 'StartTag: invalid element name\n'; this.errorIndex = this.markupRegExp.lastIndex - text.length - 2; this.readState = MarkupReadStateEnum.error; } } else if (this.currentNode === this.rootNode && this.rootNode[PropertySymbol.elementArray].length === 1) { this.errorMessage = 'Extra content at the end of the document\n'; this.errorIndex = this.markupRegExp.lastIndex - text.length - 2; this.readState = MarkupReadStateEnum.error; } else { this.errorMessage = 'StartTag: invalid element name\n'; this.errorIndex = this.markupRegExp.lastIndex - text.length - 2; this.readState = MarkupReadStateEnum.error; } } /** * Parses start tag. * * @param tagName Tag name. */ parseStartTag(tagName) { const parts = tagName.split(':'); if (parts.length > 1) { this.nextElement = this.rootNode.createElementNS(this.namespacePrefixStack[this.namespacePrefixStack.length - 1]?.get(parts[0]) || null, tagName); } else { this.nextElement = this.rootNode.createElementNS(this.defaultNamespaceStack[this.defaultNamespaceStack.length - 1] || null, tagName); } this.namespacePrefixStack.push(new Map(this.namespacePrefixStack[this.namespacePrefixStack.length - 1])); this.nextTagName = tagName; this.startTagIndex = this.markupRegExp.lastIndex; this.readState = MarkupReadStateEnum.startTag; } /** * Parses end of start tag. * * @param attributeString Attribute string. * @param isSelfClosed Is self closed. */ parseEndOfStartTag(attributeString, isSelfClosed) { const namespacePrefix = this.namespacePrefixStack[this.namespacePrefixStack.length - 1]; if (attributeString) { const attributeRegexp = new RegExp(ATTRIBUTE_REGEXP, 'gm'); let attributeMatch; let lastIndex = 0; while ((attributeMatch = attributeRegexp.exec(attributeString))) { const textBetweenAttributes = attributeString .substring(lastIndex, attributeMatch.index) .replace(SPACE_REGEXP, ''); // If there is text between attributes, the text did not match a valid attribute. if (textBetweenAttributes.length) { const match = textBetweenAttributes.match(ATTRIBUTE_WITHOUT_VALUE_REGEXP); this.errorMessage = match ? `Specification mandates value for attribute ${match[1]}\n` : 'attributes construct error\n'; this.errorIndex = this.startTagIndex; this.readState = MarkupReadStateEnum.error; this.removeOverflowingTextNodes(); return; } if ((attributeMatch[1] && attributeMatch[3] === '"') || (attributeMatch[4] && attributeMatch[6] === "'")) { // Valid attribute name and value. const name = attributeMatch[1] ?? attributeMatch[4]; const rawValue = attributeMatch[2] ?? attributeMatch[5]; // In XML, new line characters should be replaced with a space. const value = rawValue ? XMLEncodeUtility_js_1.default.decodeXMLAttributeValue(rawValue.replace(NEW_LINE_REGEXP, ' ')) : ''; const attributes = this.nextElement[PropertySymbol.attributes]; const nameParts = name.split(':'); if (nameParts.length > 2 || (nameParts.length === 2 && (!nameParts[0] || !nameParts[1]))) { this.errorMessage = `Failed to parse QName '${name}'\n`; this.errorIndex = this.startTagIndex + attributeMatch.index + attributeMatch[0].split('=')[0].length; this.readState = MarkupReadStateEnum.error; return; } let namespaceURI = null; // In the SVG namespace, the attribute "xmlns" should be set to the "http://www.w3.org/2000/xmlns/" namespace and "xlink" to the "http://www.w3.org/1999/xlink" namespace. switch (nameParts[0]) { case 'xmlns': namespaceURI = NamespaceURI_js_1.default.xmlns; break; case 'xlink': namespaceURI = NamespaceURI_js_1.default.xlink; break; } if (!attributes.getNamedItemNS(namespaceURI, nameParts[1] ?? name)) { const attribute = NodeFactory_js_1.default.createNode(this.rootNode, this.window.Attr); attribute[PropertySymbol.namespaceURI] = namespaceURI; attribute[PropertySymbol.name] = name; attribute[PropertySymbol.localName] = namespaceURI && nameParts[1] ? nameParts[1] : name; attribute[PropertySymbol.prefix] = namespaceURI && nameParts[1] ? nameParts[0] : null; attribute[PropertySymbol.value] = value; attributes[PropertySymbol.setNamedItem](attribute); // Attributes prefixed with "xmlns:" should be added to the namespace prefix map, so that the prefix can be added as namespaceURI to elements using the prefix. if (attribute[PropertySymbol.prefix] === 'xmlns') { namespacePrefix.set(attribute[PropertySymbol.localName], value); // If the prefix matches the current element, we should set the namespace URI of the element to the value of the attribute. // We don't need to upgrade the element, as there are no defined element types using a prefix. if (this.nextElement[PropertySymbol.prefix] === attribute[PropertySymbol.localName]) { this.nextElement[PropertySymbol.namespaceURI] = value; } } // If the attribute is "xmlns", we should upgrade the element to an element created using the namespace URI. else if (name === 'xmlns' && !this.nextElement[PropertySymbol.prefix]) { // We only need to create a new instance if it is a known namespace URI. if (NAMESPACE_URIS.includes(value)) { this.nextElement = this.rootNode.createElementNS(value, this.nextElement[PropertySymbol.tagName]); this.nextElement[PropertySymbol.attributes] = attributes; attributes[PropertySymbol.ownerElement] = this.nextElement; for (const item of attributes[PropertySymbol.items].values()) { item[PropertySymbol.ownerElement] = this.nextElement; } } else { this.nextElement[PropertySymbol.namespaceURI] = value; } } } else { this.errorMessage = `Attribute ${name} redefined\n`; this.errorIndex = this.startTagIndex; this.readState = MarkupReadStateEnum.error; } this.startTagIndex += attributeMatch[0].length; } else if ((attributeMatch[1] && attributeMatch[3] !== '"') || (attributeMatch[4] && attributeMatch[6] !== "'")) { // End attribute apostrophe is missing (e.g. "attr='value" or 'attr="value'). // We should continue to the next end of start tag match. return; } lastIndex = attributeRegexp.lastIndex; } const attributeStringEnd = attributeString.substring(lastIndex).replace(SPACE_REGEXP, ''); if (attributeStringEnd.length) { const match = attributeStringEnd.match(ATTRIBUTE_WITHOUT_VALUE_REGEXP); if (match) { this.errorMessage = `Specification mandates value for attribute ${match[1]}\n`; this.errorIndex = this.markupRegExp.lastIndex - 2; } else { this.errorMessage = 'attributes construct error\n'; this.errorIndex = this.startTagIndex; } this.readState = MarkupReadStateEnum.error; this.removeOverflowingTextNodes(); return; } } // Prefixed elements need to have a namespace URI defined by a prefixed "xmlns:" attribute either by a parent or in the current element. if (this.nextElement[PropertySymbol.prefix] && !this.nextElement[PropertySymbol.namespaceURI]) { this.errorMessage = `Namespace prefix ${this.nextElement[PropertySymbol.prefix]} on name is not defined\n`; this.errorIndex = this.lastIndex; this.readState = MarkupReadStateEnum.error; return; } // Only one document element is allowed in the document. if (this.currentNode === this.rootNode && this.rootNode[PropertySymbol.elementArray].length !== 0) { this.errorMessage = 'Extra content at the end of the document\n'; this.errorIndex = this.lastIndex - this.nextElement[PropertySymbol.tagName].length - 1; this.readState = MarkupReadStateEnum.error; return; } this.currentNode[PropertySymbol.appendChild](this.nextElement, true); // Sets the new element as the current node. // XML nodes can be self closed using "/>" if (!isSelfClosed) { this.currentNode = this.nextElement; this.nodeStack.push(this.currentNode); this.tagNameStack.push(this.nextTagName); if (this.currentNode[PropertySymbol.namespaceURI] && !this.currentNode[PropertySymbol.prefix]) { this.defaultNamespaceStack.push(this.currentNode[PropertySymbol.namespaceURI]); } else { this.defaultNamespaceStack.push(this.defaultNamespaceStack[this.defaultNamespaceStack.length - 1]); } } this.nextElement = null; this.nextTagName = null; this.readState = MarkupReadStateEnum.any; this.startTagIndex = this.markupRegExp.lastIndex; } /** * Parses end tag. * * @param tagName Tag name. * @returns True if the end tag was parsed, false otherwise. */ parseEndTag(tagName) { if (this.tagNameStack[this.tagNameStack.length - 1] === tagName) { this.nodeStack.pop(); this.tagNameStack.pop(); this.namespacePrefixStack.pop(); this.defaultNamespaceStack.pop(); this.currentNode = this.nodeStack[this.nodeStack.length - 1] || this.rootNode; return true; } return false; } /** * Parses XML document error. * * @param readXML XML that has been read. * @param errorMessage Error message. */ parseError(readXML, errorMessage) { let errorRoot = this.rootNode.documentElement; if (!errorRoot) { const documentElement = this.rootNode.createElementNS(NamespaceURI_js_1.default.html, 'html'); const body = this.rootNode.createElementNS(NamespaceURI_js_1.default.html, 'body'); documentElement.appendChild(body); errorRoot = body; this.rootNode[PropertySymbol.appendChild](documentElement, true); } const rows = readXML.split('\n'); const column = rows[rows.length - 1].length + 1; const error = `error on line ${rows.length} at column ${column}: ${errorMessage}`; const errorElement = this.rootNode.createElementNS(NamespaceURI_js_1.default.html, 'parsererror'); errorElement.setAttribute('style', 'display: block; white-space: pre; border: 2px solid #c77; padding: 0 1em 0 1em; margin: 1em; background-color: #fdd; color: black'); errorElement.innerHTML = `<h3>This page contains the following errors:</h3><div style="font-family:monospace;font-size:12px">${error}</div><h3>Below is a rendering of the page up to the first error.</h3>`; errorRoot.insertBefore(errorElement, errorRoot.firstChild); } /** * Removes overflowing text nodes in the current node. * * This needs to be done for some errors. */ removeOverflowingTextNodes() { if (this.currentNode && this.currentNode !== this.rootNode) { while (this.currentNode.lastChild?.[PropertySymbol.nodeType] === Node_js_1.default.TEXT_NODE) { this.currentNode.removeChild(this.currentNode.lastChild); } } } /** * Returns document type. * * @param value Value. * @returns Document type. */ getDocumentType(value) { if (!value.toUpperCase().startsWith('DOCTYPE')) { return null; } const docTypeSplit = value.split(SPACE_REGEXP); if (docTypeSplit.length <= 1) { return null; } const docTypeString = docTypeSplit.slice(1).join(' '); const attributes = []; const attributeRegExp = new RegExp(DOCUMENT_TYPE_ATTRIBUTE_REGEXP, 'gm'); const isPublic = docTypeString.toUpperCase().includes('PUBLIC'); let attributeMatch; while ((attributeMatch = attributeRegExp.exec(docTypeString))) { attributes.push(attributeMatch[1]); } const publicId = isPublic ? attributes[0] || '' : ''; const systemId = isPublic ? attributes[1] || '' : attributes[0] || ''; return { name: docTypeSplit[1].toLowerCase(), publicId, systemId }; } } exports.default = XMLParser; //# sourceMappingURL=XMLParser.cjs.map