Repository: capricorn86/happy-dom
Pull request #904 (2023-05-11 07:57:00)

#578@minor: Refactors the XML parser, which fixes several issues rela…
…ted to parsing complex HTML. This will improve performance significantly.


Patch:
diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json
index fbfee4e8..26560d5f 100644
--- a/packages/happy-dom/package.json
+++ b/packages/happy-dom/package.json
@@ -52,7 +52,7 @@
 		"webidl-conversions": "^7.0.0",
 		"whatwg-encoding": "^2.0.0",
 		"whatwg-mimetype": "^3.0.0",
-	  	"iconv-lite": "^0.6.3"
+		"iconv-lite": "^0.6.3"
 	},
 	"devDependencies": {
 		"@types/he": "^1.1.2",
diff --git a/packages/happy-dom/src/config/ChildLessElements.ts b/packages/happy-dom/src/config/ChildLessElements.ts
deleted file mode 100644
index dc5606eb..00000000
--- a/packages/happy-dom/src/config/ChildLessElements.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export default ['style', 'script'];
diff --git a/packages/happy-dom/src/config/PlainTextElements.ts b/packages/happy-dom/src/config/PlainTextElements.ts
index dc5606eb..e0ac55c3 100644
--- a/packages/happy-dom/src/config/PlainTextElements.ts
+++ b/packages/happy-dom/src/config/PlainTextElements.ts
@@ -1,1 +1,4 @@
-export default ['style', 'script'];
+export default {
+	STYLE: true,
+	SCRIPT: true
+};
diff --git a/packages/happy-dom/src/config/UnnestableElements.ts b/packages/happy-dom/src/config/UnnestableElements.ts
index 5eb5a373..1d2820a1 100644
--- a/packages/happy-dom/src/config/UnnestableElements.ts
+++ b/packages/happy-dom/src/config/UnnestableElements.ts
@@ -1,18 +1,18 @@
-export default [
-	'a',
-	'button',
-	'dd',
-	'dt',
-	'form',
-	'h1',
-	'h2',
-	'h3',
-	'h4',
-	'h5',
-	'h6',
-	'li',
-	'option',
-	'p',
-	'select',
-	'table'
-];
+export default {
+	A: true,
+	BUTTON: true,
+	DD: true,
+	DT: true,
+	FORM: true,
+	H1: true,
+	H2: true,
+	H3: true,
+	H4: true,
+	H5: true,
+	H6: true,
+	LI: true,
+	OPTION: true,
+	P: true,
+	SELECT: true,
+	TABLE: true
+};
diff --git a/packages/happy-dom/src/config/VoidElements.ts b/packages/happy-dom/src/config/VoidElements.ts
index a867539a..93073f0d 100644
--- a/packages/happy-dom/src/config/VoidElements.ts
+++ b/packages/happy-dom/src/config/VoidElements.ts
@@ -1,17 +1,16 @@
-export default [
-	'area',
-	'base',
-	'br',
-	'col',
-	'embed',
-	'hr',
-	'img',
-	'input',
-	'link',
-	'meta',
-	'param',
-	'source',
-	'track',
-	'wbr',
-	'meta'
-];
+export default {
+	AREA: true,
+	BASE: true,
+	BR: true,
+	COL: true,
+	EMBED: true,
+	HR: true,
+	IMG: true,
+	INPUT: true,
+	LINK: true,
+	META: true,
+	PARAM: true,
+	SOURCE: true,
+	TRACK: true,
+	WBR: true
+};
diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts
index 8656835c..61cf256a 100644
--- a/packages/happy-dom/src/dom-parser/DOMParser.ts
+++ b/packages/happy-dom/src/dom-parser/DOMParser.ts
@@ -44,7 +44,7 @@ export default class DOMParser {
 		newDocument.childNodes.length = 0;
 		newDocument.children.length = 0;
 
-		const root = XMLParser.parse(newDocument, string, true);
+		const root = XMLParser.parse(newDocument, string, { evaluateScripts: true });
 		let documentElement = null;
 		let documentTypeNode = null;
 
@@ -65,7 +65,7 @@ export default class DOMParser {
 				newDocument.appendChild(documentTypeNode);
 			}
 			newDocument.appendChild(documentElement);
-			const body = newDocument.querySelector('body');
+			const body = newDocument.body;
 			if (body) {
 				for (const child of root.childNodes.slice()) {
 					body.appendChild(child);
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index 9ce6205f..20840dc6 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -236,9 +236,9 @@ export default class Document extends Node implements IDocument {
 	 * @returns Title.
 	 */
 	public get title(): string {
-		const el = this.querySelector('title');
-		if (el) {
-			return el.textContent;
+		const element = ParentNodeUtility.getElementByTagName(this, 'title');
+		if (element) {
+			return element.textContent;
 		}
 		return '';
 	}
@@ -248,13 +248,13 @@ export default class Document extends Node implements IDocument {
 	 *
 	 */
 	public set title(title: string) {
-		const el = this.querySelector('title');
-		if (el) {
-			el.textContent = title;
+		const element = ParentNodeUtility.getElementByTagName(this, 'title');
+		if (element) {
+			element.textContent = title;
 		} else {
-			const titleEl = this.createElement('title');
-			titleEl.textContent = title;
-			this.head.appendChild(titleEl);
+			const newElement = this.createElement('title');
+			newElement.textContent = title;
+			this.head.appendChild(newElement);
 		}
 	}
 
@@ -430,9 +430,9 @@ export default class Document extends Node implements IDocument {
 	 * @returns Base URI.
 	 */
 	public get baseURI(): string {
-		const base = <IHTMLBaseElement>this.querySelector('base');
-		if (base) {
-			return base.href;
+		const element = <IHTMLBaseElement | null>ParentNodeUtility.getElementByTagName(this, 'base');
+		if (element) {
+			return element.href;
 		}
 		return this.defaultView.location.href;
 	}
@@ -653,7 +653,7 @@ export default class Document extends Node implements IDocument {
 	 * @param html HTML.
 	 */
 	public write(html: string): void {
-		const root = XMLParser.parse(this, html, true);
+		const root = XMLParser.parse(this, html, { evaluateScripts: true });
 
 		if (this._isFirstWrite || this._isFirstWriteAfterOpen) {
 			if (this._isFirstWrite) {
@@ -688,8 +688,8 @@ export default class Document extends Node implements IDocument {
 
 					this.appendChild(documentElement);
 				} else {
-					const rootBody = root.querySelector('body');
-					const body = this.querySelector('body');
+					const rootBody = ParentNodeUtility.getElementByTagName(root, 'body');
+					const body = ParentNodeUtility.getElementByTagName(this, 'body');
 					if (rootBody && body) {
 						for (const child of rootBody.childNodes.slice()) {
 							body.appendChild(child);
@@ -697,7 +697,7 @@ export default class Document extends Node implements IDocument {
 					}
 				}
 
-				const body = this.querySelector('body');
+				const body = ParentNodeUtility.getElementByTagName(this, 'body');
 				if (body) {
 					for (const child of root.childNodes.slice()) {
 						if (child['tagName'] !== 'HTML' && child.nodeType !== Node.DOCUMENT_TYPE_NODE) {
@@ -720,9 +720,10 @@ export default class Document extends Node implements IDocument {
 				this.appendChild(documentElement);
 			}
 		} else {
-			const bodyNode = root.querySelector('body');
+			const bodyNode = ParentNodeUtility.getElementByTagName(root, 'body');
+			const body = ParentNodeUtility.getElementByTagName(this, 'body');
 			for (const child of (bodyNode || root).childNodes.slice()) {
-				this.body.appendChild(child);
+				body.appendChild(child);
 			}
 		}
 	}
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index 34c8c087..9bb6083b 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -244,9 +244,7 @@ export default class Element extends Node implements IElement {
 			this.removeChild(child);
 		}
 
-		for (const node of XMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
-			this.appendChild(node);
-		}
+		XMLParser.parse(this.ownerDocument, html, { rootNode: this });
 	}
 
 	/**
diff --git a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts
index 317ebd0e..3178c6de 100644
--- a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts
+++ b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts
@@ -2,8 +2,8 @@ import HTMLElement from '../html-element/HTMLElement';
 import IDocumentFragment from '../document-fragment/IDocumentFragment';
 import INode from '../node/INode';
 import IHTMLTemplateElement from './IHTMLTemplateElement';
-import XMLParser from '../../xml-parser/XMLParser';
 import XMLSerializer from '../../xml-serializer/XMLSerializer';
+import XMLParser from '../../xml-parser/XMLParser';
 
 /**
  * HTML Template Element.
@@ -29,9 +29,7 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem
 			this.content.removeChild(child);
 		}
 
-		for (const node of XMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
-			this.content.appendChild(node);
-		}
+		XMLParser.parse(this.ownerDocument, html, { rootNode: this.content });
 	}
 
 	/**
diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts
index b3bc54b8..8daedd44 100644
--- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts
+++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts
@@ -16,16 +16,13 @@ export default class ParentNodeUtility {
 	 * @param parentNode Parent node.
 	 * @param nodes List of Node or DOMString.
 	 */
-	public static append(parentNode: INode, ...nodes: (INode | string)[]): void {
+	public static append(
+		parentNode: IElement | IDocument | IDocumentFragment,
+		...nodes: (INode | string)[]
+	): void {
 		for (const node of nodes) {
 			if (typeof node === 'string') {
-				const newChildNodes = XMLParser.parse(
-					<IDocument>parentNode.ownerDocument,
-					node
-				).childNodes.slice();
-				for (const newChildNode of newChildNodes) {
-					parentNode.appendChild(newChildNode);
-				}
+				XMLParser.parse(<IDocument>parentNode.ownerDocument, node, { rootNode: parentNode });
 			} else {
 				parentNode.appendChild(node);
 			}
@@ -38,9 +35,11 @@ export default class ParentNodeUtility {
 	 * @param parentNode Parent node.
 	 * @param nodes List of Node or DOMString.
 	 */
-	public static prepend(parentNode: INode, ...nodes: (string | INode)[]): void {
+	public static prepend(
+		parentNode: IElement | IDocument | IDocumentFragment,
+		...nodes: (string | INode)[]
+	): void {
 		const firstChild = parentNode.firstChild;
-
 		for (const node of nodes) {
 			if (typeof node === 'string') {
 				const newChildNodes = XMLParser.parse(
@@ -62,7 +61,10 @@ export default class ParentNodeUtility {
 	 * @param parentNode Parent node.
 	 * @param nodes List of Node or DOMString.
 	 */
-	public static replaceChildren(parentNode: INode, ...nodes: (string | INode)[]): void {
+	public static replaceChildren(
+		parentNode: IElement | IDocument | IDocumentFragment,
+		...nodes: (string | INode)[]
+	): void {
 		for (const node of parentNode.childNodes.slice()) {
 			parentNode.removeChild(node);
 		}
diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
index 9bbdad41..3e52c8e1 100644
--- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
+++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
@@ -42,9 +42,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot
 			this.removeChild(child);
 		}
 
-		for (const node of XMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
-			this.appendChild(node);
-		}
+		XMLParser.parse(this.ownerDocument, html, { rootNode: this });
 	}
 
 	/**
diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts
index a0eed489..927bb774 100644
--- a/packages/happy-dom/src/range/Range.ts
+++ b/packages/happy-dom/src/range/Range.ts
@@ -406,7 +406,7 @@ export default class Range {
 	 */
 	public createContextualFragment(tagString: string): IDocumentFragment {
 		// TODO: We only have support for HTML in the parser currently, so it is not necessary to check which context it is
-		return XMLParser.parse(this._ownerDocument, tagString);
+		return <IDocumentFragment>XMLParser.parse(this._ownerDocument, tagString);
 	}
 
 	/**
diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts
index 0b7db6b0..0d8e61ef 100755
--- a/packages/happy-dom/src/xml-parser/XMLParser.ts
+++ b/packages/happy-dom/src/xml-parser/XMLParser.ts
@@ -1,25 +1,59 @@
-import Node from '../nodes/node/Node';
-import Element from '../nodes/element/Element';
 import IDocument from '../nodes/document/IDocument';
 import VoidElements from '../config/VoidElements';
 import UnnestableElements from '../config/UnnestableElements';
-import ChildLessElements from '../config/ChildLessElements';
-import { decode } from 'he';
 import NamespaceURI from '../config/NamespaceURI';
 import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement';
-import INode from '../nodes/node/INode';
 import IElement from '../nodes/element/IElement';
 import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement';
-import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment';
 import PlainTextElements from '../config/PlainTextElements';
+import IDocumentType from '../nodes/document-type/IDocumentType';
+import INode from '../nodes/node/INode';
+import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment';
+import { decode } from 'he';
+
+/**
+ * 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 =
+	/<([a-zA-Z0-9-]+)|<\/([a-zA-Z0-9-]+)>|<!--([^-]+)-->|<!--([^>]+)>|<!([^>]+)>|<\?([^>]+)>|(\/>)|(>)/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 apostrophe value using double apostrophe (e.g. "value" in "<div name="value">").
+ * Group 3: Attribute value when the attribute has a value using double apostrophe (e.g. "value" in "<div name="value">").
+ * Group 4: Attribute apostrophe when the attribute has a value using double apostrophe (e.g. "value" in "<div name="value">").
+ * Group 5: Attribute name when the attribute has a value using single apostrophe (e.g. "name" in "<div name='value'>").
+ * Group 6: Attribute apostrophe when the attribute has a value using single apostrophe (e.g. "name" in "<div name='value'>").
+ * Group 7: Attribute value when the attribute has a value using single apostrophe (e.g. "value" in "<div name='value'>").
+ * Group 8: Attribute apostrophe when the attribute has a value using single apostrophe (e.g. "name" in "<div name='value'>").
+ * Group 9: Attribute name when the attribute has no value (e.g. "disabled" in "<div disabled>").
+ */
+const ATTRIBUTE_REGEXP =
+	/\s*([a-zA-Z0-9-_:]+) *= *("{0,1})([^"]*)("{0,1})|\s*([a-zA-Z0-9-_:]+) *= *('{0,1})([^']*)('{0,1})|\s*([a-zA-Z0-9-_:]+)/gm;
+
+enum MarkupReadStateEnum {
+	startOrEndTag = 'startOrEndTag',
+	insideStartTag = 'insideStartTag',
+	plainTextContent = 'plainTextContent'
+}
 
-const CONDITION_COMMENT_REGEXP =
-	/<!(--)?\[if (!|le|lt|lte|gt|gte|\(.*\)|&|\|| |IE|WindowsEdition|Contoso|true|false|\d+\.?(\d+)?|)*\]>/gi;
-const CONDITION_COMMENT_END_REGEXP = /<!\[endif\](--)?>/gi;
-const MARKUP_REGEXP = /<(\/?)([a-z][-.0-9_a-z]*)\s*([^<>]*?)(\/?)>/gi;
-const COMMENT_REGEXP = /<!--(.*?)-->|<([!?])([^>]*)>/gi;
+/**
+ * Document type attribute RegExp.
+ *
+ * Group 1: Attribute value.
+ */
 const DOCUMENT_TYPE_ATTRIBUTE_REGEXP = /"([^"]+)"/gm;
-const ATTRIBUTE_REGEXP = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))/gms;
 
 /**
  * XML parser.
@@ -29,121 +63,233 @@ export default class XMLParser {
 	 * Parses XML/HTML and returns a root element.
 	 *
 	 * @param document Document.
-	 * @param data HTML data.
-	 * @param [evaluateScripts = false] Set to "true" to enable script execution.
-	 * @returns Root element.
+	 * @param xml XML/HTML string.
+	 * @param [options] Options.
+	 * @param [options.rootNode] Node to append elements to. Otherwise a new DocumentFragment is created.
+	 * @param [options.evaluateScripts = false] Set to "true" to enable script execution.
+	 * @returns Root node.
 	 */
 	public static parse(
 		document: IDocument,
-		data: string,
-		evaluateScripts = false
-	): IDocumentFragment {
-		const root = document.createDocumentFragment();
-		const stack: Array<IElement | IDocumentFragment> = [root];
-		const markupRegexp = new RegExp(MARKUP_REGEXP, 'gi');
-		let parent: IDocumentFragment | IElement = root;
-		let parentTagName = null;
-		let parentUnnestableTagName = null;
-		let lastTextIndex = 0;
+		xml: string,
+		options?: { rootNode?: IElement | IDocumentFragment | IDocument; evaluateScripts?: boolean }
+	): IElement | IDocumentFragment | IDocument {
+		const root = options && options.rootNode ? options.rootNode : document.createDocumentFragment();
+		const stack: INode[] = [root];
+		const markupRegexp = new RegExp(MARKUP_REGEXP, 'gm');
+		const { evaluateScripts = false } = options || {};
+		const unnestableTagNames: string[] = [];
+		let currentNode: INode | null = root;
 		let match: RegExpExecArray;
+		let plainTextTagName: string | null = null;
+		let readState: MarkupReadStateEnum = MarkupReadStateEnum.startOrEndTag;
+		let startTagIndex = 0;
+		let lastIndex = 0;
 
-		if (data !== null && data !== undefined) {
-			data = String(data);
-
-			while ((match = markupRegexp.exec(data))) {
-				const tagName = match[2].toLowerCase();
-				const isStartTag = !match[1];
-
-				if (parent && match.index !== lastTextIndex) {
-					const text = data.substring(lastTextIndex, match.index);
-					if (parentTagName && PlainTextElements.includes(parentTagName)) {
-						parent.appendChild(document.createTextNode(text));
-					} else {
-						let condCommMatch;
-						let condCommEndMatch;
-						const condCommRegexp = new RegExp(CONDITION_COMMENT_REGEXP, 'gi');
-						const condCommEndRegexp = new RegExp(CONDITION_COMMENT_END_REGEXP, 'gi');
-						// @Refer: https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/?redirectedfrom=MSDN
+		if (xml !== null && xml !== undefined) {
+			xml = String(xml);
+
+			while ((match = markupRegexp.exec(xml))) {
+				switch (readState) {
+					case MarkupReadStateEnum.startOrEndTag:
 						if (
-							isStartTag &&
-							(condCommMatch = condCommRegexp.exec(text)) &&
-							condCommMatch[0] &&
-							(condCommEndMatch = condCommEndRegexp.exec(data.substring(markupRegexp.lastIndex))) &&
-							condCommEndMatch[0]
+							match.index !== lastIndex &&
+							(match[1] || match[2] || match[3] || match[4] || match[5] || match[6])
+						) {
+							// Plain text between tags.
+
+							currentNode.appendChild(
+								document.createTextNode(xml.substring(lastIndex, match.index))
+							);
+						}
+
+						if (match[1]) {
+							// Start tag.
+
+							const tagName = match[1].toUpperCase();
+
+							// Some elements are not allowed to be nested (e.g. "<a><a></a></a>" is not allowed.).
+							// Therefore we need to auto-close the tag, so that it become valid (e.g. "<a></a><a></a>").
+							const unnestableTagNameIndex = unnestableTagNames.indexOf(tagName);
+							if (unnestableTagNameIndex !== -1) {
+								unnestableTagNames.splice(unnestableTagNameIndex, 1);
+								while (currentNode !== root) {
+									if ((<IElement>currentNode).tagName === tagName) {
+										stack.pop();
+										currentNode = stack[stack.length - 1] || root;
+										break;
+									}
+									stack.pop();
+									currentNode = stack[stack.length - 1] || root;
+								}
+							}
+
+							// NamespaceURI is inherited from the parent element.
+							// It should default to SVG for SVG elements.
+							const namespaceURI =
+								tagName === 'SVG'
+									? NamespaceURI.svg
+									: (<IElement>currentNode).namespaceURI || NamespaceURI.html;
+							const newElement = document.createElementNS(namespaceURI, tagName);
+
+							currentNode.appendChild(newElement);
+							currentNode = newElement;
+							stack.push(currentNode);
+							readState = MarkupReadStateEnum.insideStartTag;
+							startTagIndex = markupRegexp.lastIndex;
+						} else if (match[2]) {
+							// End tag.
+
+							if (match[2].toUpperCase() === (<IElement>currentNode).tagName) {
+								// Some elements are not allowed to be nested (e.g. "<a><a></a></a>" is not allowed.).
+								// Therefore we need to auto-close the tag, so that it become valid (e.g. "<a></a><a></a>").
+								const unnestableTagNameIndex = unnestableTagNames.indexOf(
+									(<IElement>currentNode).tagName
+								);
+								if (unnestableTagNameIndex !== -1) {
+									unnestableTagNames.splice(unnestableTagNameIndex, 1);
+								}
+
+								stack.pop();
+								currentNode = stack[stack.length - 1] || root;
+							}
+						} else if (
+							match[3] ||
+							match[4] ||
+							(match[6] && (<IElement>currentNode).namespaceURI === NamespaceURI.html)
 						) {
-							markupRegexp.lastIndex += condCommEndRegexp.lastIndex;
-							continue;
+							// Comment.
+
+							currentNode.appendChild(
+								document.createComment((match[6] ? '?' : '') + (match[3] || match[4] || match[6]))
+							);
+						} else if (match[5]) {
+							// Exclamation mark comment (usually <!DOCTYPE>).
+
+							currentNode.appendChild(
+								this.getDocumentTypeNode(document, match[5]) || document.createComment(match[5])
+							);
+						} else if (match[6]) {
+							// Processing instruction (not supported by HTML).
+							// TODO: Add support for processing instructions.
 						} else {
-							this.appendTextAndCommentNodes(document, parent, text);
+							// Plain text between tags, including the match as it is not a valid start or end tag.
+
+							currentNode.appendChild(
+								document.createTextNode(xml.substring(lastIndex, markupRegexp.lastIndex))
+							);
 						}
-					}
-				}
 
-				if (isStartTag) {
-					const namespaceURI =
-						tagName === 'svg'
-							? NamespaceURI.svg
-							: (<IElement>parent).namespaceURI || NamespaceURI.html;
-					const newElement = document.createElementNS(namespaceURI, tagName);
-
-					// Scripts are not allowed to be executed when they are parsed using innerHTML, outerHTML, replaceWith() etc.
-					// However, they are allowed to be executed when document.write() is used.
-					// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement
-					if (tagName === 'script') {
-						(<HTMLScriptElement>newElement)._evaluateScript = evaluateScripts;
-					}
-
-					// An assumption that the same rule should be applied for the HTMLLinkElement is made here.
-					if (tagName === 'link') {
-						(<HTMLLinkElement>newElement)._evaluateCSS = evaluateScripts;
-					}
-
-					this.setAttributes(newElement, match[3]);
-
-					if (!match[4] && !VoidElements.includes(tagName)) {
-						// Some elements are not allowed to be nested (e.g. "<a><a></a></a>" is not allowed.).
-						// Therefore we will auto-close the tag.
-						if (parentUnnestableTagName === tagName) {
-							stack.pop();
-							parent = <Element>parent.parentNode || root;
+						break;
+					case MarkupReadStateEnum.insideStartTag:
+						// End of start tag
+						if (match[7] || match[8]) {
+							// End of start tag.
+
+							// Attribute name and value.
+
+							const attributeString = xml.substring(startTagIndex, match.index);
+							let hasAttributeStringEnded = true;
+
+							if (!!attributeString) {
+								const attributeRegexp = new RegExp(ATTRIBUTE_REGEXP, 'gm');
+								let attributeMatch: RegExpExecArray;
+
+								while ((attributeMatch = attributeRegexp.exec(attributeString))) {
+									if (
+										(attributeMatch[1] && attributeMatch[2] === attributeMatch[4]) ||
+										(attributeMatch[5] && attributeMatch[6] === attributeMatch[8]) ||
+										attributeMatch[9]
+									) {
+										// Valid attribute name and value.
+
+										const name = attributeMatch[1] || attributeMatch[5] || attributeMatch[9] || '';
+										const rawValue = attributeMatch[3] || attributeMatch[7] || '';
+										const value = rawValue ? decode(rawValue) : '';
+										const namespaceURI =
+											(<IElement>currentNode).tagName === 'SVG' && name === 'xmlns' ? value : null;
+
+										(<IElement>currentNode).setAttributeNS(namespaceURI, name, value);
+
+										startTagIndex += attributeMatch[0].length;
+									} else if (!attributeMatch[4] && !attributeMatch[8]) {
+										// End attribute apostrophe is missing (e.g. "attr='value" or 'attr="value').
+
+										hasAttributeStringEnded = false;
+										break;
+									}
+								}
+							}
+
+							// We need to check if the attribute string is read completely.
+							// The attribute string can potentially contain "/>" or ">".
+							if (hasAttributeStringEnded) {
+								// Checks if the tag is a self closing tag (ends with "/>") or void element.
+								// When it is a self closing tag or void element it should be closed immediately.
+								// Self closing tags are not allowed in the HTML namespace, but the parser should still allow it for void elements.
+								// Self closing tags is supported in the SVG namespace.
+								if (
+									VoidElements[(<IElement>currentNode).tagName] ||
+									(match[7] && (<IElement>currentNode).namespaceURI === NamespaceURI.svg)
+								) {
+									stack.pop();
+									currentNode = stack[stack.length - 1] || root;
+									readState = MarkupReadStateEnum.startOrEndTag;
+								} else {
+									// Plain text elements such as <script> and <style> should only contain text.
+									plainTextTagName = PlainTextElements[(<IElement>currentNode).tagName]
+										? (<IElement>currentNode).tagName
+										: null;
+
+									readState = !!plainTextTagName
+										? MarkupReadStateEnum.plainTextContent
+										: MarkupReadStateEnum.startOrEndTag;
+
+									if (UnnestableElements[(<IElement>currentNode).tagName]) {
+										unnestableTagNames.push((<IElement>currentNode).tagName);
+									}
+								}
+
+								startTagIndex = markupRegexp.lastIndex;
+							}
 						}
 
-						parent = <Element>parent.appendChild(newElement);
-						parentTagName = tagName;
-						parentUnnestableTagName = this.getUnnestableTagName(parent);
-						stack.push(parent);
-					} else {
-						parent.appendChild(newElement);
-					}
-					lastTextIndex = markupRegexp.lastIndex;
-
-					// Tags which contain non-parsed content
-					// For example: <script> JavaScript should not be parsed
-					if (ChildLessElements.includes(tagName)) {
-						let childLessMatch = null;
-						while ((childLessMatch = markupRegexp.exec(data))) {
-							if (childLessMatch[2].toLowerCase() === tagName && childLessMatch[1]) {
-								markupRegexp.lastIndex -= childLessMatch[0].length;
-								break;
+						break;
+					case MarkupReadStateEnum.plainTextContent:
+						if (match[2] && match[2].toUpperCase() === plainTextTagName) {
+							// End of plain text tag.
+
+							// Scripts are not allowed to be executed when they are parsed using innerHTML, outerHTML, replaceWith() etc.
+							// However, they are allowed to be executed when document.write() is used.
+							// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement
+							if (plainTextTagName === 'SCRIPT') {
+								(<HTMLScriptElement>currentNode)._evaluateScript = evaluateScripts;
+							} else if (plainTextTagName === 'LINK') {
+								// An assumption that the same rule should be applied for the HTMLLinkElement is made here.
+								(<HTMLLinkElement>currentNode)._evaluateCSS = evaluateScripts;
 							}
+
+							// Plain text elements such as <script> and <style> should only contain text.
+							currentNode.appendChild(
+								document.createTextNode(xml.substring(startTagIndex, match.index))
+							);
+
+							stack.pop();
+							currentNode = stack[stack.length - 1] || root;
+							plainTextTagName = null;
+							readState = MarkupReadStateEnum.startOrEndTag;
 						}
-					}
-				} else {
-					stack.pop();
-					parent = stack[stack.length - 1] || root;
-					parentTagName = (<IElement>parent).tagName
-						? (<IElement>parent).tagName.toLowerCase()
-						: null;
-					parentUnnestableTagName = this.getUnnestableTagName(parent);
-
-					lastTextIndex = markupRegexp.lastIndex;
+
+						break;
 				}
+
+				lastIndex = markupRegexp.lastIndex;
 			}
 
-			// Text after last element
-			if ((!match && data.length > 0) || (match && lastTextIndex !== match.index)) {
-				const text = data.substring(lastTextIndex);
-				this.appendTextAndCommentNodes(document, parent || root, text);
+			if (lastIndex !== xml.length) {
+				// Plain text after tags.
+
+				currentNode.appendChild(document.createTextNode(xml.substring(lastIndex)));
 			}
 		}
 
@@ -151,132 +297,40 @@ export default class XMLParser {
 	}
 
 	/**
-	 * Returns a tag name if element is unnestable.
-	 *
-	 * @param element Element.
-	 * @returns Tag name if element is unnestable.
-	 */
-	private static getUnnestableTagName(element: IElement | IDocumentFragment): string {
-		const tagName = (<IElement>element).tagName ? (<IElement>element).tagName.toLowerCase() : null;
-		return tagName && UnnestableElements.includes(tagName) ? tagName : null;
-	}
-
-	/**
-	 * Appends text and comment nodes.
+	 * Returns document type node.
 	 *
 	 * @param document Document.
-	 * @param node Node.
-	 * @param text Text to search in.
+	 * @param value Value.
+	 * @returns Document type node.
 	 */
-	private static appendTextAndCommentNodes(document: IDocument, node: INode, text: string): void {
-		for (const innerNode of this.getTextAndCommentNodes(document, text)) {
-			node.appendChild(innerNode);
+	private static getDocumentTypeNode(document: IDocument, value: string): IDocumentType {
+		if (!value.toUpperCase().startsWith('DOCTYPE')) {
+			return null;
 		}
-	}
 
-	/**
-	 * Returns text and comment nodes from a text.
-	 *
-	 * @param document Document.
-	 * @param text Text to search in.
-	 * @returns Nodes.
-	 */
-	private static getTextAndCommentNodes(document: IDocument, text: string): Node[] {
-		const nodes = [];
-		const commentRegExp = new RegExp(COMMENT_REGEXP, 'gms');
-		let hasDocumentType = false;
-		let lastIndex = 0;
-		let match;
+		const docTypeSplit = value.split(' ');
 
-		while ((match = commentRegExp.exec(text))) {
-			if (match.index > 0 && lastIndex !== match.index) {
-				const textNode = document.createTextNode(text.substring(lastIndex, match.index));
-				nodes.push(textNode);
-			}
-			if (match[3] && match[3].toUpperCase().startsWith('DOCTYPE')) {
-				const docTypeSplit = match[3].split(' ');
-
-				if (docTypeSplit.length > 1) {
-					const docTypeString = docTypeSplit.slice(1).join(' ');
-					const attributes = [];
-					const attributeRegExp = new RegExp(DOCUMENT_TYPE_ATTRIBUTE_REGEXP, 'gm');
-					const isPublic = docTypeString.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] || '';
-
-					const documentTypeNode = document.implementation.createDocumentType(
-						docTypeSplit[1],
-						publicId,
-						systemId
-					);
-
-					nodes.push(documentTypeNode);
-					hasDocumentType = true;
-				}
-			} else {
-				const comment = match[1] ? match[1] : match[2] === '?' ? '?' + match[3] : match[3];
-				const commentNode = document.createComment(comment);
-				nodes.push(commentNode);
-				lastIndex = match.index + match[0].length;
-			}
+		if (docTypeSplit.length <= 1) {
+			return null;
 		}
 
-		if (!hasDocumentType && lastIndex < text.length) {
-			const textNode = document.createTextNode(text.substring(lastIndex));
-			nodes.push(textNode);
-		}
+		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;
 
-		return nodes;
-	}
-
-	/**
-	 * Sets raw attributes.
-	 *
-	 * @param element Element.
-	 * @param attributesString Raw attributes.
-	 */
-	private static setAttributes(element: IElement, attributesString: string): void {
-		const attributes = attributesString.trim();
-		if (attributes) {
-			const regExp = new RegExp(ATTRIBUTE_REGEXP, 'gi');
-			let match: RegExpExecArray;
-
-			// Attributes with value
-			while ((match = regExp.exec(attributes))) {
-				if (match[1]) {
-					const value = decode(match[2] || match[3] || match[4] || '');
-					const name = this._getAttributeName(element.namespaceURI, match[1]);
-					const namespaceURI = element.tagName === 'SVG' && name === 'xmlns' ? value : null;
-					element.setAttributeNS(namespaceURI, name, value);
-				}
-			}
-
-			// Attributes with no value
-			for (const name of attributes.replace(ATTRIBUTE_REGEXP, '').trim().split(' ')) {
-				if (name) {
-					element.setAttribute(this._getAttributeName(element.namespaceURI, name), '');
-				}
-			}
+		while ((attributeMatch = attributeRegExp.exec(docTypeString))) {
+			attributes.push(attributeMatch[1]);
 		}
-	}
 
-	/**
-	 * Returns attribute name.
-	 *
-	 * @param namespaceURI Namespace URI.
-	 * @param name Name.
-	 * @returns Attribute name based on namespace.
-	 */
-	private static _getAttributeName(namespaceURI: string, name: string): string {
-		if (namespaceURI === NamespaceURI.svg) {
-			return name;
-		}
-		return name.toLowerCase();
+		const publicId = isPublic ? attributes[0] || '' : '';
+		const systemId = isPublic ? attributes[1] || '' : attributes[0] || '';
+
+		return document.implementation.createDocumentType(
+			docTypeSplit[1].toLowerCase(),
+			publicId,
+			systemId
+		);
 	}
 }
diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
index 9a255bb5..75d9bea3 100644
--- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
+++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
@@ -6,6 +6,8 @@ import { escape } from 'he';
 import INode from '../nodes/node/INode';
 import IElement from '../nodes/element/IElement';
 import IHTMLTemplateElement from '../nodes/html-template-element/IHTMLTemplateElement';
+import NodeTypeEnum from '../nodes/node/NodeTypeEnum';
+import IProcessingInstruction from '../nodes/processing-instruction/IProcessingInstruction';
 
 /**
  * Utility for converting an element to string.
@@ -21,16 +23,16 @@ export default class XMLSerializer {
 	 */
 	public serializeToString(root: INode, options?: { includeShadowRoots?: boolean }): string {
 		switch (root.nodeType) {
-			case Node.ELEMENT_NODE:
+			case NodeTypeEnum.elementNode:
 				const element = <Element>root;
 				const tagName = element.tagName.toLowerCase();
 
-				if (VoidElements.includes(tagName)) {
+				if (VoidElements[element.tagName]) {
 					return `<${tagName}${this._getAttributes(element)}>`;
 				}
 
 				const childNodes =
-					element.tagName === 'TEMPLATE'
+					tagName === 'template'
 						? (<IHTMLTemplateElement>root).content.childNodes
 						: root.childNodes;
 				let innerHTML = '';
@@ -57,11 +59,14 @@ export default class XMLSerializer {
 					html += this.serializeToString(node, options);
 				}
 				return html;
-			case Node.COMMENT_NODE:
+			case NodeTypeEnum.commentNode:
 				return `<!--${root.textContent}-->`;
-			case Node.TEXT_NODE:
-				return root['textContent'];
-			case Node.DOCUMENT_TYPE_NODE:
+			case NodeTypeEnum.processingInstructionNode:
+				// TODO: Add support for processing instructions.
+				return `<!--?${(<IProcessingInstruction>root).target} ${root.textContent}?-->`;
+			case NodeTypeEnum.textNode:
+				return root.textContent;
+			case NodeTypeEnum.documentTypeNode:
 				const doctype = <DocumentType>root;
 				const identifier = doctype.publicId ? ' PUBLIC' : doctype.systemId ? ' SYSTEM' : '';
 				const publicId = doctype.publicId ? ` "${doctype.publicId}"` : '';


Test patch:
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index ff8f5791..bc55d083 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -211,18 +211,18 @@ describe('Element', () => {
 
 	describe('set innerHTML()', () => {
 		it('Creates child nodes from provided HTML.', () => {
-			const root = document.createDocumentFragment();
 			const div = document.createElement('div');
 			const textNode = document.createTextNode('text1');
 
 			element.appendChild(document.createElement('div'));
 			div.appendChild(textNode);
-			root.appendChild(div);
 
-			jest.spyOn(XMLParser, 'parse').mockImplementation((parseDocument, html) => {
-				expect(parseDocument).toBe(document);
-				expect(html).toBe('SOME_HTML');
-				return root;
+			jest.spyOn(XMLParser, 'parse').mockImplementation(function (ownerDocument, xml, options) {
+				expect(ownerDocument).toBe(document);
+				expect(xml).toBe('SOME_HTML');
+				expect(options).toEqual({ rootNode: element });
+				element.appendChild(div);
+				return element;
 			});
 			element.innerHTML = 'SOME_HTML';
 
diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts
index 5affbf8d..ff7d4524 100644
--- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts
+++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts
@@ -472,11 +472,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[class*="s1 cl"]');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value that contains a specified value using "[class*="s1 cl"]" or matches exactly a value using "[attr1="value1"]".', () => {
@@ -846,7 +846,7 @@ describe('QuerySelector', () => {
 			const span = document.createElement('span');
 			div1.appendChild(div2);
 			div2.appendChild(span);
-			expect(div1.querySelector('span')).toBe(span);
+			expect(div1.querySelector('span') === span).toBe(true);
 		});
 
 		it('Returns span wkith a specific class name matching ".spanClass".', () => {
@@ -856,7 +856,7 @@ describe('QuerySelector', () => {
 			span.className = 'spanClass';
 			div1.appendChild(div2);
 			div2.appendChild(span);
-			expect(div1.querySelector('.spanClass')).toBe(span);
+			expect(div1.querySelector('.spanClass') === span).toBe(true);
 		});
 
 		it('Returns span wkith a specific class name matching ".spanClass".', () => {
@@ -866,7 +866,7 @@ describe('QuerySelector', () => {
 			span.className = 'spanClass';
 			div1.appendChild(div2);
 			div2.appendChild(span);
-			expect(div1.querySelector('.spanClass')).toBe(span);
+			expect(div1.querySelector('.spanClass') === span).toBe(true);
 		});
 
 		it('Returns span with a specific class name and tag name matching "span.spanClass".', () => {
@@ -876,7 +876,7 @@ describe('QuerySelector', () => {
 			span.className = 'spanClass';
 			div1.appendChild(div2);
 			div2.appendChild(span);
-			expect(div1.querySelector('span.spanClass')).toBe(span);
+			expect(div1.querySelector('span.spanClass') === span).toBe(true);
 		});
 
 		it('Returns div with a specific id and tag name matching "div#divId".', () => {
@@ -886,7 +886,7 @@ describe('QuerySelector', () => {
 			div3.id = 'divId';
 			div1.appendChild(div2);
 			div2.appendChild(div3);
-			expect(div1.querySelector('div#divId')).toBe(div3);
+			expect(div1.querySelector('div#divId') === div3).toBe(true);
 		});
 
 		it('Returns span with a specific class name and tag name matching "custom-element.class1".', () => {
@@ -911,24 +911,24 @@ describe('QuerySelector', () => {
 			span.setAttribute('attr1', 'value1');
 			div1.appendChild(div2);
 			div2.appendChild(span);
-			expect(div1.querySelector('span[attr1="value1"]')).toBe(span);
-			expect(div1.querySelector('[attr1="value1"]')).toBe(span);
-			expect(div1.querySelector('span[attr1]')).toBe(span);
-			expect(div1.querySelector('[attr1]')).toBe(span);
+			expect(div1.querySelector('span[attr1="value1"]') === span).toBe(true);
+			expect(div1.querySelector('[attr1="value1"]') === span).toBe(true);
+			expect(div1.querySelector('span[attr1]') === span).toBe(true);
+			expect(div1.querySelector('[attr1]') === span).toBe(true);
 		});
 
 		it('Returns the first element matching "div > div > span".', () => {
 			const container = document.createElement('div');
 			container.innerHTML = QuerySelectorHTML;
 			const span = container.querySelector('div > div > span');
-			expect(span).toBe(container.children[0].children[1].children[0]);
+			expect(span === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns the first element matching "div > div > .class1.class2".', () => {
 			const container = document.createElement('div');
 			container.innerHTML = QuerySelectorHTML;
 			const div = container.querySelector('div > div > .class1.class2');
-			expect(div).toBe(container.children[0].children[1]);
+			expect(div === container.children[0].children[1]).toBe(true);
 		});
 
 		it('Returns the first element matching "*".', () => {
@@ -937,7 +937,7 @@ describe('QuerySelector', () => {
 			const span = document.createElement('span');
 			div1.appendChild(div2);
 			div2.appendChild(span);
-			expect(div1.querySelector('*')).toBe(div2);
+			expect(div1.querySelector('*') === div2).toBe(true);
 		});
 
 		it('Returns "null" if no element is found.', () => {
diff --git a/packages/happy-dom/test/xml-parser/XMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts
index 4295f787..f198288f 100644
--- a/packages/happy-dom/test/xml-parser/XMLParser.test.ts
+++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts
@@ -12,10 +12,11 @@ import IHTMLTemplateElement from '../../src/nodes/html-template-element/IHTMLTem
 
 const GET_EXPECTED_HTML = (html: string): string =>
 	html
-		.replace('<?Question mark comment>', '<!--?Question mark comment-->')
+		.replace('<?processing instruction?>', '<!--?processing instruction?-->')
+		.replace('<?processing-instruction>', '<!--?processing-instruction-->')
 		.replace('<!Exclamation mark comment>', '<!--Exclamation mark comment-->')
-		.replace('<self-closing-custom-tag />', '<self-closing-custom-tag></self-closing-custom-tag>')
-		.replace(/[\s]/gm, '');
+		.replace('<!DOCTYPE HTML', '<!DOCTYPE html')
+		.replace('<img />', '<img>');
 
 describe('XMLParser', () => {
 	let window: IWindow;
@@ -28,7 +29,7 @@ describe('XMLParser', () => {
 
 	describe('parse()', () => {
 		it('Parses HTML with a single <div>.', () => {
-			const root = XMLParser.parse(window.document, '<div></div>');
+			const root = XMLParser.parse(document, '<div></div>');
 			expect(root.childNodes.length).toBe(1);
 			expect(root.childNodes[0].childNodes.length).toBe(0);
 			expect((<IHTMLElement>root.childNodes[0]).tagName).toBe('DIV');
@@ -36,7 +37,7 @@ describe('XMLParser', () => {
 
 		it('Parses HTML with a single <div> with attributes.', () => {
 			const root = XMLParser.parse(
-				window.document,
+				document,
 				'<div class="class1 class2" id="id" data-no-value></div>'
 			);
 			expect(root.childNodes.length).toBe(1);
@@ -120,10 +121,8 @@ describe('XMLParser', () => {
 		});
 
 		it('Parses an entire HTML page.', () => {
-			const root = XMLParser.parse(window.document, XMLParserHTML);
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
-				GET_EXPECTED_HTML(XMLParserHTML)
-			);
+			const root = XMLParser.parse(document, XMLParserHTML);
+			expect(new XMLSerializer().serializeToString(root)).toBe(GET_EXPECTED_HTML(XMLParserHTML));
 		});
 
 		it('Parses a page with document type set to "HTML 4.01".', () => {
@@ -131,13 +130,63 @@ describe('XMLParser', () => {
 				'<!DOCTYPE html>',
 				'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
 			);
-			const root = XMLParser.parse(window.document, pageHTML);
+			const root = XMLParser.parse(document, pageHTML);
 			const doctype = <DocumentType>root.childNodes[0];
-			expect(doctype.name).toBe('HTML');
+			expect(doctype.name).toBe('html');
 			expect(doctype.publicId).toBe('-//W3C//DTD HTML 4.01//EN');
 			expect(doctype.systemId).toBe('http://www.w3.org/TR/html4/strict.dtd');
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
-				GET_EXPECTED_HTML(pageHTML)
+			expect(new XMLSerializer().serializeToString(root)).toBe(GET_EXPECTED_HTML(pageHTML));
+		});
+
+		it('Handles unnestable elements correctly when there are siblings.', () => {
+			const root = XMLParser.parse(
+				document,
+				`<article>
+                    <span>
+                        <div>
+                            <a><a>Test</a></a>
+                        </div>
+                    </span>
+                    <b>Test</b>
+                </article>`
+			);
+			expect(new XMLSerializer().serializeToString(root).replace(/\s/gm, '')).toBe(
+				`
+                <article>
+                    <span>
+                        <div>
+                            <a></a><a>Test</a>
+                        </div>
+                    </span>
+                    <b>Test</b>
+                </article>
+            `.replace(/\s/gm, '')
+			);
+		});
+
+		it('Handles unnestable elements correctly when the nested element is wrapped by another element.', () => {
+			const root = XMLParser.parse(
+				document,
+				`<article>
+                    <span>
+                        <div>
+                            <a><span><a>Test</a></span></a>
+                        </div>
+                    </span>
+                    <b>Test</b>
+                </article>`
+			);
+			expect(new XMLSerializer().serializeToString(root).replace(/\s/gm, '')).toBe(
+				`
+                <article>
+                    <span>
+                        <div>
+                            <a><span></span></a><a>Test</a>
+                        </div>
+                    </span>
+                    <b>Test</b>
+                </article>
+            `.replace(/\s/gm, '')
 			);
 		});
 
@@ -146,19 +195,17 @@ describe('XMLParser', () => {
 				'<!DOCTYPE html>',
 				'<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">'
 			);
-			const root = XMLParser.parse(window.document, pageHTML);
+			const root = XMLParser.parse(document, pageHTML);
 			const doctype = <DocumentType>root.childNodes[0];
 			expect(doctype.name).toBe('math');
 			expect(doctype.publicId).toBe('');
 			expect(doctype.systemId).toBe('http://www.w3.org/Math/DTD/mathml1/mathml.dtd');
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
-				GET_EXPECTED_HTML(pageHTML)
-			);
+			expect(new XMLSerializer().serializeToString(root)).toBe(GET_EXPECTED_HTML(pageHTML));
 		});
 
 		it('Handles unclosed tags of unnestable elements (e.g. <a>, <li>).', () => {
 			const root = XMLParser.parse(
-				window.document,
+				document,
 				`
 				<div class="test" disabled>
 					<ul>
@@ -171,7 +218,7 @@ describe('XMLParser', () => {
 				`
 			);
 
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
+			expect(new XMLSerializer().serializeToString(root).replace(/\s/gm, '')).toBe(
 				`
 				<div class="test" disabled="">
 					<ul>
@@ -181,13 +228,13 @@ describe('XMLParser', () => {
 					</ul>
 					<a></a><a>Test</a>
 				</div>
-				`.replace(/[\s]/gm, '')
+				`.replace(/\s/gm, '')
 			);
 		});
 
 		it('Does not parse the content of script and style elements.', () => {
 			const root = XMLParser.parse(
-				window.document,
+				document,
 				`<div>
 					<script>if(1<Math['random']()){}else if(Math['random']()>1){console.log("1")}</script>
 					<script><b></b></script>
@@ -202,24 +249,24 @@ describe('XMLParser', () => {
 			expect((<IHTMLElement>root.children[0].children[1]).innerText).toBe('<b></b>');
 			expect((<IHTMLElement>root.children[0].children[2]).innerText).toBe('<b></b>');
 
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
+			expect(new XMLSerializer().serializeToString(root)).toBe(
 				`<div>
 					<script>if(1<Math['random']()){}else if(Math['random']()>1){console.log("1")}</script>
 					<script><b></b></script>
 					<style><b></b></style>
-				</div>`.replace(/[\s]/gm, '')
+				</div>`
 			);
 
 			const root2 = XMLParser.parse(
-				window.document,
+				document,
 				`<html>
-	<head>
-		<title>Title</title>
-	</head>
-	<body>
-		<script type="text/javascript">var vars = []; for (var i=0;i<vars.length;i++) {}</script>
-	</body>
-</html>`
+                    <head>
+                        <title>Title</title>
+                    </head>
+                    <body>
+                        <script type="text/javascript">var vars = []; for (var i=0;i<vars.length;i++) {}</script>
+                    </body>
+                </html>`
 			);
 			expect((<IHTMLElement>root2.children[0].children[1].children[0]).innerText).toBe(
 				'var vars = []; for (var i=0;i<vars.length;i++) {}'
@@ -227,7 +274,7 @@ describe('XMLParser', () => {
 		});
 
 		it('Handles unclosed regular elements.', () => {
-			const root = XMLParser.parse(window.document, `<div>test`);
+			const root = XMLParser.parse(document, `<div>test`);
 
 			expect(root.childNodes.length).toBe(1);
 			expect((<IHTMLElement>root.childNodes[0]).tagName).toBe('DIV');
@@ -236,7 +283,7 @@ describe('XMLParser', () => {
 
 		it('Parses an SVG with "xmlns" set to HTML.', () => {
 			const root = XMLParser.parse(
-				window.document,
+				document,
 				`
 				<div>
 					<svg viewBox="0 0 300 100" stroke="red" fill="grey" xmlns="${NamespaceURI.html}">
@@ -318,7 +365,7 @@ describe('XMLParser', () => {
 			expect(svg.attributes['xmlns'].ownerElement === svg).toBe(true);
 			expect(svg.attributes['xmlns'].ownerDocument === document).toBe(true);
 
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
+			expect(new XMLSerializer().serializeToString(root)).toBe(
 				`
 				<div>
 					<svg viewBox="0 0 300 100" stroke="red" fill="grey" xmlns="${NamespaceURI.html}">
@@ -330,13 +377,13 @@ describe('XMLParser', () => {
 						</svg>
 					</svg>
 				</div>
-			`.replace(/[\s]/gm, '')
+			`
 			);
 		});
 
 		it('Parses a malformed SVG with "xmlns" set to HTML.', () => {
 			const root = XMLParser.parse(
-				window.document,
+				document,
 				`
 				<div>
 					<svg viewBox="0 0 300 100" stroke="red" fill="grey" xmlns="${NamespaceURI.html}">
@@ -358,10 +405,10 @@ describe('XMLParser', () => {
 			`
 			);
 
-			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
+			expect(new XMLSerializer().serializeToString(root).replace(/\s/gm, '')).toBe(
 				`
 				<div>
-					<svg viewBox="0 0 300 100" stroke="red" fill="grey" xmlns="${NamespaceURI.html}">
+					<svg viewBox="0 0 300 100" stroke="red" fill="grey" xmlns="http://www.w3.org/1999/xhtml">
 						<ellipse cx="50" cy="50" r="40">
 						<line cx="50" cy="50" r="40">
 						<path cx="50" cy="50" r="40">
@@ -375,15 +422,15 @@ describe('XMLParser', () => {
 						<svg viewBox="0 0 10 10" x="200" width="100">
 							<circle cx="5" cy="5" r="4"></circle>
 						</svg>
-					</polygon></path></line></ellipse></svg>
-				</div>
-			`.replace(/[\s]/gm, '')
+					</polygon>
+				</path>
+			</line></ellipse></svg></div>`.replace(/\s/gm, '')
 			);
 		});
 
 		it('Parses childless elements with start and end tag names in different case', () => {
 			const root = XMLParser.parse(
-				window.document,
+				document,
 				`
 				<script type="text/JavaScript">console.log('hello')</SCRIPT>
 				`
@@ -393,16 +440,16 @@ describe('XMLParser', () => {
 		});
 
 		it('Handles different value types.', () => {
-			const root1 = XMLParser.parse(window.document, null);
+			const root1 = XMLParser.parse(document, null);
 			expect(new XMLSerializer().serializeToString(root1)).toBe('');
 
-			const root2 = XMLParser.parse(window.document, undefined);
+			const root2 = XMLParser.parse(document, undefined);
 			expect(new XMLSerializer().serializeToString(root2)).toBe('');
 
-			const root3 = XMLParser.parse(window.document, <string>(<unknown>1000));
+			const root3 = XMLParser.parse(document, <string>(<unknown>1000));
 			expect(new XMLSerializer().serializeToString(root3)).toBe('1000');
 
-			const root4 = XMLParser.parse(window.document, <string>(<unknown>false));
+			const root4 = XMLParser.parse(document, <string>(<unknown>false));
 			expect(new XMLSerializer().serializeToString(root4)).toBe('false');
 		});
 
@@ -454,16 +501,13 @@ describe('XMLParser', () => {
 			];
 
 			for (const html of testHTML) {
-				const root = XMLParser.parse(window.document, html);
+				const root = XMLParser.parse(document, html);
 				expect(new XMLSerializer().serializeToString(root)).toBe(html);
 			}
 		});
 
 		it('Parses <template> elements, including its content.', () => {
-			const root = XMLParser.parse(
-				window.document,
-				'<div><template><tr><td></td></tr></template></div>'
-			);
+			const root = XMLParser.parse(document, '<div><template><tr><td></td></tr></template></div>');
 			expect(root.childNodes.length).toBe(1);
 			const template = <IHTMLTemplateElement>root.childNodes[0].childNodes[0];
 			expect(template.childNodes.length).toBe(0);
@@ -473,5 +517,118 @@ describe('XMLParser', () => {
 			expect(template.content.children[0].children.length).toBe(1);
 			expect(template.content.children[0].children[0].tagName).toBe('TD');
 		});
+
+		it('Parses HTML with attributes using colon (:) and value containing ">".', () => {
+			const root = XMLParser.parse(
+				document,
+				'<template><component :is="type" :disabled="index > 1" data-testid="button"/></template>'
+			);
+
+			expect(root.childNodes.length).toBe(1);
+
+			const template = <IHTMLTemplateElement>root.childNodes[0];
+			expect(template.content.childNodes.length).toBe(1);
+			expect((<IHTMLElement>template.content.childNodes[0]).tagName).toBe('COMPONENT');
+			expect((<IHTMLElement>template.content.childNodes[0]).getAttribute(':disabled')).toBe(
+				'index > 1'
+			);
+		});
+
+		it('Doesn\'t close non-void elements when using "/>" when namespace is HTML.', () => {
+			const root = XMLParser.parse(
+				document,
+				`
+                <span key1="value1"/>
+                <span key1="value1" key2/>
+                <span key2/>
+                `
+			);
+
+			expect(new XMLSerializer().serializeToString(root).replace(/\s/gm, '')).toBe(
+				`
+                <span key1="value1">
+                    <span key1="value1" key2="">
+                        <span key2=""></span>
+                    </span>
+                </span>`.replace(/\s/gm, '')
+			);
+		});
+
+		it('Can read text with ">" in it.', () => {
+			const root = XMLParser.parse(document, `<span>1 > 0</span>`);
+
+			expect(new XMLSerializer().serializeToString(root)).toBe(`<span>1 > 0</span>`);
+		});
+
+		it('Parses malformed attributes.', () => {
+			const root = XMLParser.parse(
+				document,
+				`
+                <span key1="value1""></span>
+                <span key1="value1"" key2></span>
+                <span key1 key2 key3="value3""></span>
+                <img key1="value1"" key2/>
+                <img key1="value1""
+                />
+                <img key1="value1""
+                key2/>
+                <span key1 key2 key3="value3"''" " "></span>
+                <span key1="value1 > value2"></span>
+                <img key1="value1 /> value2"/>
+                `
+			);
+
+			expect(root.children.length).toBe(9);
+
+			expect(root.children[0].attributes.length).toBe(1);
+			expect(root.children[0].attributes[0].name).toBe('key1');
+			expect(root.children[0].attributes[0].value).toBe('value1');
+
+			expect(root.children[1].attributes.length).toBe(2);
+			expect(root.children[1].attributes[0].name).toBe('key1');
+			expect(root.children[1].attributes[0].value).toBe('value1');
+			expect(root.children[1].attributes[1].name).toBe('key2');
+			expect(root.children[1].attributes[1].value).toBe('');
+
+			expect(root.children[2].attributes.length).toBe(3);
+			expect(root.children[2].attributes[0].name).toBe('key1');
+			expect(root.children[2].attributes[0].value).toBe('');
+			expect(root.children[2].attributes[1].name).toBe('key2');
+			expect(root.children[2].attributes[1].value).toBe('');
+			expect(root.children[2].attributes[2].name).toBe('key3');
+			expect(root.children[2].attributes[2].value).toBe('value3');
+
+			expect(root.children[3].attributes.length).toBe(2);
+			expect(root.children[3].attributes[0].name).toBe('key1');
+			expect(root.children[3].attributes[0].value).toBe('value1');
+			expect(root.children[3].attributes[1].name).toBe('key2');
+			expect(root.children[3].attributes[1].value).toBe('');
+
+			expect(root.children[4].attributes.length).toBe(1);
+			expect(root.children[4].attributes[0].name).toBe('key1');
+			expect(root.children[4].attributes[0].value).toBe('value1');
+
+			expect(root.children[5].attributes.length).toBe(2);
+			expect(root.children[5].attributes[0].name).toBe('key1');
+			expect(root.children[5].attributes[0].value).toBe('value1');
+			expect(root.children[5].attributes[1].name).toBe('key2');
+			expect(root.children[5].attributes[1].value).toBe('');
+
+			expect(root.children[6].attributes.length).toBe(3);
+			expect(root.children[6].attributes[0].name).toBe('key1');
+			expect(root.children[6].attributes[0].value).toBe('');
+			expect(root.children[6].attributes[1].name).toBe('key2');
+			expect(root.children[6].attributes[1].value).toBe('');
+			expect(root.children[6].attributes[2].name).toBe('key3');
+			expect(root.children[6].attributes[2].value).toBe('value3');
+
+			expect(root.children[7].attributes.length).toBe(1);
+			expect(root.children[7].attributes[0].name).toBe('key1');
+			expect(root.children[7].attributes[0].value).toBe('value1 > value2');
+
+			expect(root.children[8].attributes.length).toBe(1);
+			expect(root.children[8].attributes[0].name).toBe('key1');
+			expect(root.children[8].attributes[0].value).toBe('value1 /> value2');
+		});
 	});
 });
diff --git a/packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts b/packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts
index 193cd0f2..cfc0bafb 100644
--- a/packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts
+++ b/packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts
@@ -6,8 +6,9 @@ export default `
 		</head>
 		<body>
 			<div class="class1 class2" id="id">
-				<!-- Comment 1 !-->
-				<?Question mark comment>
+				<!--Comment 1!-->
+				<?processing instruction?>
+				<?processing-instruction>
 				<!Exclamation mark comment>
 				<b>Bold</b>
 				<!-- Comment 2 !-->
@@ -19,7 +20,7 @@ export default `
 				<!-- Comment 2 !-->
 			</article>
 			<img>
-			<self-closing-custom-tag />
+			<img />
 		</body>
 	</html>
 `.trim();