diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts index 3659ba17d..49f15720a 100644 --- a/packages/happy-dom/src/config/ElementTag.ts +++ b/packages/happy-dom/src/config/ElementTag.ts @@ -20,9 +20,10 @@ import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement'; import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement'; import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement'; import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement'; +import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement'; export default { - A: HTMLElement, + A: HTMLAnchorElement, ABBR: HTMLElement, ADDRESS: HTMLElement, AREA: HTMLElement, diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index af58ea6ab..22547789d 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -77,21 +77,17 @@ export default abstract class AbstractCSSStyleDeclaration { if (this._ownerElement) { const style = new CSSStyleDeclarationPropertyManager({ cssText }); - if (!style.size()) { - delete this._ownerElement['_attributes']['style']; - } else { - if (!this._ownerElement['_attributes']['style']) { - Attr._ownerDocument = this._ownerElement.ownerDocument; - this._ownerElement['_attributes']['style'] = new Attr(); - this._ownerElement['_attributes']['style'].name = 'style'; - } - - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; - } + if (!this._ownerElement['_attributes']['style']) { + Attr._ownerDocument = this._ownerElement.ownerDocument; + this._ownerElement['_attributes']['style'] = new Attr(); + this._ownerElement['_attributes']['style'].name = 'style'; + } - this._ownerElement['_attributes']['style'].value = style.toString(); + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; } + + this._ownerElement['_attributes']['style'].value = style.toString(); } else { this._style = new CSSStyleDeclarationPropertyManager({ cssText }); } diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index 19a022318..0e1d3f546 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -199,7 +199,7 @@ export default class DOMTokenList implements IDOMTokenList { * Updates indices. */ public _updateIndices(): void { - const attr = this._ownerElement.getAttribute('class'); + const attr = this._ownerElement.getAttribute(this._attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; for (let i = list.length - 1, max = this.length; i < max; i++) { diff --git a/packages/happy-dom/src/named-node-map/INamedNodeMap.ts b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts new file mode 100644 index 000000000..a0a904fc8 --- /dev/null +++ b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts @@ -0,0 +1,70 @@ +import IAttr from '../nodes/attr/IAttr'; + +/** + * NamedNodeMap. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap. + */ +export default interface INamedNodeMap extends Iterable { + [index: number]: IAttr; + [Symbol.toStringTag]: string; + readonly length: number; + + /** + * Returns attribute by index. + * + * @param index Index. + */ + item: (index: number) => IAttr; + + /** + * Returns attribute by name. + * + * @param qualifiedName Name. + * @returns Attribute. + */ + getNamedItem: (qualifiedName: string) => IAttr; + + /** + * Returns attribute by name and namespace. + * + * @param namespace Namespace. + * @param localName Local name of the attribute. + * @returns Attribute. + */ + getNamedItemNS: (namespace: string, localName: string) => IAttr; + + /** + * Adds a new attribute node. + * + * @param attr Attribute. + * @returns Replaced attribute. + */ + setNamedItem: (attr: IAttr) => IAttr; + + /** + * Adds a new namespaced attribute node. + * + * @param attr Attribute. + * @returns Replaced attribute. + */ + setNamedItemNS: (attr: IAttr) => IAttr; + + /** + * Removes an attribute. + * + * @param qualifiedName Name of the attribute. + * @returns Removed attribute. + */ + removeNamedItem: (qualifiedName: string) => IAttr; + + /** + * Removes a namespaced attribute. + * + * @param namespace Namespace. + * @param localName Local name of the attribute. + * @returns Removed attribute. + */ + removeNamedItemNS: (namespace: string, localName: string) => IAttr; +} diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts new file mode 100644 index 000000000..19b18968a --- /dev/null +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -0,0 +1,145 @@ +import type Element from '../nodes/element/Element'; +import IAttr from '../nodes/attr/IAttr'; +import INamedNodeMap from './INamedNodeMap'; + +/** + * NamedNodeMap. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap. + */ +export default class NamedNodeMap implements INamedNodeMap { + [index: number]: IAttr; + + /** + * Reference to the element. + */ + #ownerElement: Element; + + /** + * Constructor. + * + * @param element Associated element. + */ + constructor(element: Element) { + this.#ownerElement = element; + } + + /** + * Returns string. + * + * @returns string. + */ + public get [Symbol.toStringTag](): string { + return this.constructor.name; + } + + /** + * Length. + * + * @returns Length. + */ + public get length(): number { + return Object.keys(this.#ownerElement._attributes).length; + } + + /** + * Returns attribute by index. + * + * @param index Index. + */ + public item(index: number): IAttr | null { + if (index < 0) { + return null; + } + const attr = Object.values(this.#ownerElement._attributes)[index]; + return attr ? attr : null; + } + + /** + * Returns attribute by name. + * + * @param qualifiedName Name. + * @returns Attribute. + */ + public getNamedItem(qualifiedName: string): IAttr | null { + return this.#ownerElement.getAttributeNode(qualifiedName); + } + + /** + * Returns attribute by name and namespace. + * + * @param namespace Namespace. + * @param localName Local name of the attribute. + * @returns Attribute. + */ + public getNamedItemNS(namespace: string, localName: string): IAttr | null { + return this.#ownerElement.getAttributeNodeNS(namespace, localName); + } + + /** + * Adds a new attribute node. + * + * @param attr Attribute. + * @returns Replaced attribute. + */ + public setNamedItem(attr: IAttr): IAttr { + return this.#ownerElement.setAttributeNode(attr); + } + + /** + * Adds a new namespaced attribute node. + * + * @param attr Attribute. + * @returns Replaced attribute. + */ + public setNamedItemNS(attr: IAttr): IAttr { + return this.#ownerElement.setAttributeNodeNS(attr); + } + + /** + * Removes an attribute. + * + * @param qualifiedName Name of the attribute. + * @returns Removed attribute. + */ + public removeNamedItem(qualifiedName: string): IAttr | null { + const attr = this.getNamedItem(qualifiedName); + + if (attr) { + this.#ownerElement.removeAttributeNode(attr); + } + return attr; + } + + /** + * Removes a namespaced attribute. + * + * @param namespace Namespace. + * @param localName Local name of the attribute. + * @returns Removed attribute. + */ + public removeNamedItemNS(namespace: string, localName: string): IAttr | null { + const attr = this.getNamedItemNS(namespace, localName); + + if (attr) { + this.#ownerElement.removeAttributeNode(attr); + } + return attr; + } + + /** + * Iterator. + * + * @returns Iterator. + */ + public [Symbol.iterator](): Iterator { + let index = -1; + return { + next: () => { + index++; + return { value: this.item(index), done: index >= this.length }; + } + }; + } +} diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index e41f4c9f6..70063c6ea 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -1,6 +1,7 @@ import Node from '../node/Node'; import ShadowRoot from '../shadow-root/ShadowRoot'; import Attr from '../attr/Attr'; +import NamedNodeMap from '../../named-node-map/NamedNodeMap'; import DOMRect from './DOMRect'; import DOMTokenList from '../../dom-token-list/DOMTokenList'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; @@ -26,6 +27,8 @@ import IText from '../text/IText'; import IDOMRectList from './IDOMRectList'; import DOMRectListFactory from './DOMRectListFactory'; import IAttr from '../attr/IAttr'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap'; + import Event from '../../event/Event'; /** @@ -249,11 +252,8 @@ export default class Element extends Node implements IElement { * * @returns Attributes. */ - public get attributes(): { [k: string | number]: IAttr } & { length: number } { - const attributes = Object.values(this._attributes); - return Object.assign({}, this._attributes, attributes, { - length: attributes.length - }); + public get attributes(): INamedNodeMap { + return Object.assign(new NamedNodeMap(this), Object.values(this._attributes), this._attributes); } /** @@ -840,7 +840,9 @@ export default class Element extends Node implements IElement { this._attributes[name] = attribute; - this._updateDomListIndices(); + if (attribute.name === 'class' && this._classList) { + this._classList._updateIndices(); + } if ( this.attributeChangedCallback && @@ -919,15 +921,26 @@ export default class Element extends Node implements IElement { * Removes an Attr node. * * @param attribute Attribute. + * @returns Removed attribute. */ - public removeAttributeNode(attribute: IAttr): void { + public removeAttributeNode(attribute: IAttr): IAttr { + const removedAttribute = this._attributes[attribute.name]; + + if (removedAttribute !== attribute) { + throw new DOMException( + `Failed to execute 'removeAttributeNode' on 'Element': The node provided is owned by another element.` + ); + } + delete this._attributes[attribute.name]; if (this.isConnected) { this.ownerDocument['_cacheID']++; } - this._updateDomListIndices(); + if (attribute.name === 'class' && this._classList) { + this._classList._updateIndices(); + } if ( this.attributeChangedCallback && @@ -954,15 +967,18 @@ export default class Element extends Node implements IElement { } } } + + return attribute; } /** * Removes an Attr node. * * @param attribute Attribute. + * @returns Removed attribute. */ - public removeAttributeNodeNS(attribute: IAttr): void { - this.removeAttributeNode(attribute); + public removeAttributeNodeNS(attribute: IAttr): IAttr { + return this.removeAttributeNode(attribute); } /** @@ -1021,13 +1037,4 @@ export default class Element extends Node implements IElement { } return name.toLowerCase(); } - - /** - * Updates DOM list indices. - */ - protected _updateDomListIndices(): void { - if (this._classList) { - this._classList._updateIndices(); - } - } } diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts index 20c6c7656..b3fc6ff57 100644 --- a/packages/happy-dom/src/nodes/element/IElement.ts +++ b/packages/happy-dom/src/nodes/element/IElement.ts @@ -1,5 +1,6 @@ import IShadowRoot from '../shadow-root/IShadowRoot'; import IAttr from '../attr/IAttr'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap'; import DOMRect from './DOMRect'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; import INode from './../node/INode'; @@ -29,7 +30,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, slot: string; readonly nodeName: string; readonly localName: string; - readonly attributes: { [k: string | number]: IAttr } & { length: number }; + readonly attributes: INamedNodeMap; // Events oncancel: (event: Event) => void | null; @@ -259,15 +260,17 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, * Removes an Attr node. * * @param attribute Attribute. + * @returns Removed attribute. */ - removeAttributeNode(attribute: IAttr): void; + removeAttributeNode(attribute: IAttr): IAttr; /** * Removes an Attr node. * * @param attribute Attribute. + * @returns Removed attribute. */ - removeAttributeNodeNS(attribute: IAttr): void; + removeAttributeNodeNS(attribute: IAttr): IAttr; /** * Clones a node. diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts new file mode 100644 index 000000000..7feab26eb --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -0,0 +1,439 @@ +import HTMLElement from '../html-element/HTMLElement'; +import DOMTokenList from '../../dom-token-list/DOMTokenList'; +import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; +import IHTMLAnchorElement from './IHTMLAnchorElement'; +import { URL } from 'url'; +import IAttr from '../attr/IAttr'; +import HTMLAnchorElementUtility from './HTMLAnchorElementUtility'; + +/** + * HTML Anchor Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. + */ +export default class HTMLAnchorElement extends HTMLElement implements IHTMLAnchorElement { + private _relList: DOMTokenList = null; + private _url: URL | null = null; + + /** + * Returns download. + * + * @returns download. + */ + public get download(): string { + return this.getAttribute('download') || ''; + } + + /** + * Sets download. + * + * @param download Download. + */ + public set download(download: string) { + this.setAttributeNS(null, 'download', download); + } + + /** + * Returns hash. + * + * @returns Hash. + */ + public get hash(): string { + return this._url?.hash ?? ''; + } + + /** + * Sets hash. + * + * @param hash Hash. + */ + public set hash(hash: string) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { + this._url.hash = hash; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns href. + * + * @returns Href. + */ + public get href(): string | null { + if (this._url) { + return this._url.toString(); + } + + return this.getAttributeNS(null, 'href') || ''; + } + + /** + * Sets href. + * + * @param href Href. + */ + public set href(href: string) { + this.setAttributeNS(null, 'href', href); + } + + /** + * Returns hreflang. + * + * @returns Hreflang. + */ + public get hreflang(): string { + return this.getAttributeNS(null, 'hreflang') || ''; + } + + /** + * Sets hreflang. + * + * @param hreflang Hreflang. + */ + public set hreflang(hreflang: string) { + this.setAttributeNS(null, 'hreflang', hreflang); + } + + /** + * Returns the hyperlink's URL's origin. + * + * @returns Origin. + */ + public get origin(): string { + return this._url?.origin ?? ''; + } + + /** + * Returns ping. + * + * @returns Ping. + */ + public get ping(): string { + return this.getAttributeNS(null, 'ping') || ''; + } + + /** + * Sets ping. + * + * @param ping Ping. + */ + public set ping(ping: string) { + this.setAttributeNS(null, 'ping', ping); + } + + /** + * Returns protocol. + * + * @returns Protocol. + */ + public get protocol(): string { + return this._url?.protocol ?? ''; + } + + /** + * Sets protocol. + * + * @param protocol Protocol. + */ + public set protocol(protocol: string) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { + this._url.protocol = protocol; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns username. + * + * @returns Username. + */ + public get username(): string { + return this._url?.username ?? ''; + } + + /** + * Sets username. + * + * @param username Username. + */ + public set username(username: string) { + if ( + this._url && + !HTMLAnchorElementUtility.isBlobURL(this._url) && + this._url.host && + this._url.protocol != 'file' + ) { + this._url.username = username; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns password. + * + * @returns Password. + */ + public get password(): string { + return this._url?.password ?? ''; + } + + /** + * Sets password. + * + * @param password Password. + */ + public set password(password: string) { + if ( + this._url && + !HTMLAnchorElementUtility.isBlobURL(this._url) && + this._url.host && + this._url.protocol != 'file' + ) { + this._url.password = password; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns pathname. + * + * @returns Pathname. + */ + public get pathname(): string { + return this._url?.pathname ?? ''; + } + + /** + * Sets pathname. + * + * @param pathname Pathname. + */ + public set pathname(pathname: string) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { + this._url.pathname = pathname; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns port. + * + * @returns Port. + */ + public get port(): string { + return this._url?.port ?? ''; + } + + /** + * Sets port. + * + * @param port Port. + */ + public set port(port: string) { + if ( + this._url && + !HTMLAnchorElementUtility.isBlobURL(this._url) && + this._url.host && + this._url.protocol != 'file' + ) { + this._url.port = port; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns host. + * + * @returns Host. + */ + public get host(): string { + return this._url?.host ?? ''; + } + + /** + * Sets host. + * + * @param host Host. + */ + public set host(host: string) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { + this._url.host = host; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns hostname. + * + * @returns Hostname. + */ + public get hostname(): string { + return this._url?.hostname ?? ''; + } + + /** + * Sets hostname. + * + * @param hostname Hostname. + */ + public set hostname(hostname: string) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { + this._url.hostname = hostname; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns referrerPolicy. + * + * @returns Referrer Policy. + */ + public get referrerPolicy(): string { + return this.getAttributeNS(null, 'referrerPolicy') || ''; + } + + /** + * Sets referrerPolicy. + * + * @param referrerPolicy Referrer Policy. + */ + public set referrerPolicy(referrerPolicy: string) { + this.setAttributeNS(null, 'referrerPolicy', referrerPolicy); + } + + /** + * Returns rel. + * + * @returns Rel. + */ + public get rel(): string { + return this.getAttributeNS(null, 'rel') || ''; + } + + /** + * Sets rel. + * + * @param rel Rel. + */ + public set rel(rel: string) { + this.setAttributeNS(null, 'rel', rel); + } + + /** + * Returns rel list. + * + * @returns Rel list. + */ + public get relList(): IDOMTokenList { + if (!this._relList) { + this._relList = new DOMTokenList(this, 'rel'); + } + return this._relList; + } + + /** + * Returns search. + * + * @returns Search. + */ + public get search(): string { + return this._url?.search ?? ''; + } + + /** + * Sets search. + * + * @param search Search. + */ + public set search(search: string) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { + this._url.search = search; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns target. + * + * @returns target. + */ + public get target(): string { + return this.getAttributeNS(null, 'target') || ''; + } + + /** + * Sets target. + * + * @param target Target. + */ + public set target(target: string) { + this.setAttributeNS(null, 'target', target); + } + + /** + * Returns text. + * + * @returns text. + */ + public get text(): string { + return this.textContent; + } + + /** + * Sets text. + * + * @param text Text. + */ + public set text(text: string) { + this.textContent = text; + } + + /** + * Returns type. + * + * @returns Type. + */ + public get type(): string { + return this.getAttributeNS(null, 'type') || ''; + } + + /** + * Sets type. + * + * @param type Type. + */ + public set type(type: string) { + this.setAttributeNS(null, 'type', type); + } + + /** + * @override + */ + public override setAttributeNode(attribute: IAttr): IAttr { + const replacedAttribute = super.setAttributeNode(attribute); + + if (attribute.name === 'rel' && this._relList) { + this._relList._updateIndices(); + } else if (attribute.name === 'href') { + this._url = HTMLAnchorElementUtility.getUrl(this.ownerDocument, attribute.value); + } + + return replacedAttribute; + } + + /** + * @override + */ + public override removeAttributeNode(attribute: IAttr): IAttr { + super.removeAttributeNode(attribute); + + if (attribute.name === 'rel' && this._relList) { + this._relList._updateIndices(); + } else if (attribute.name === 'href') { + this._url = null; + } + + return attribute; + } +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementUtility.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementUtility.ts new file mode 100644 index 000000000..554fe047c --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementUtility.ts @@ -0,0 +1,48 @@ +import IDocument from '../document/IDocument'; +import { URL } from 'url'; + +/** + * HTML Anchor Element utility. + */ +export default class HTMLAnchorElementUtility { + /** + * Returns "true" if it is a blob URL. + * + * According to spec, if element's url is non-null, its scheme is "blob", and it has an opaque path, then the process of updating properties on the URL should be terminated. + * + * @see https://html.spec.whatwg.org/multipage/links.html#reinitialise-url + * @param url + * @param url URL. + * @returns "true" if blob URL. + */ + public static isBlobURL(url: URL): boolean { + return ( + url && url.protocol === 'blob:' && url.pathname.length > 1 && url.pathname.includes('://') + ); + } + + /** + * Returns URL. + * + * @see https://html.spec.whatwg.org/multipage/links.html#dom-hyperlink-href + * @see https://html.spec.whatwg.org/multipage/links.html#hyperlink + * @param document Document. + * @param href Href. + * @returns URL. + */ + public static getUrl(document: IDocument, href: string | null): URL { + if (!href) { + return null; + } + + const documentUrl = document.location.href; + + try { + return new URL(href.trim(), documentUrl); + } catch (TypeError) { + // Ignore error + } + + return null; + } +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts new file mode 100644 index 000000000..b436b9c90 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts @@ -0,0 +1,21 @@ +import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; +import IHTMLElement from '../html-element/IHTMLElement'; +import IHTMLHyperlinkElementUtils from './IHTMLHyperlinkElementUtils'; + +/** + * HTML Anchor Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. + */ +export default interface IHTMLAnchorElement extends IHTMLElement, IHTMLHyperlinkElementUtils { + readonly relList: IDOMTokenList; + download: string; + ping: string; + hreflang: string; + referrerPolicy: string; + rel: string; + target: string; + text: string; + type: string; +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts new file mode 100644 index 000000000..6d23f0564 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts @@ -0,0 +1,19 @@ +/** + * HTMLHyperlinkElementUtils. + * + * Reference: + * https://html.spec.whatwg.org/multipage/links.html#htmlhyperlinkelementutils. + */ +export default interface IHTMLHyperlinkElementUtils { + readonly origin: string; + href: string; + protocol: string; + username: string; + password: string; + host: string; + hostname: string; + port: string; + pathname: string; + search: string; + hash: string; +} diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index beb210e7a..262eb88a5 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -200,6 +200,16 @@ export default class HTMLElement extends Element implements IHTMLElement { return this._style; } + /** + * Sets style. + * + * @param cssText Style as text. + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style#setting_styles + */ + public set style(cssText: string | CSSStyleDeclaration) { + this.style.cssText = typeof cssText === 'string' ? cssText : ''; + } + /** * Returns data set. * @@ -411,11 +421,7 @@ export default class HTMLElement extends Element implements IHTMLElement { } /** - * The setAttributeNode() method adds a new Attr node to the specified element. - * * @override - * @param attribute Attribute. - * @returns Replaced attribute. */ public setAttributeNode(attribute: IAttr): IAttr { const replacedAttribute = super.setAttributeNode(attribute); @@ -428,25 +434,20 @@ export default class HTMLElement extends Element implements IHTMLElement { } /** - * Removes an Attr node. - * * @override - * @param attribute Attribute. */ - public removeAttributeNode(attribute: IAttr): void { + public removeAttributeNode(attribute: IAttr): IAttr { super.removeAttributeNode(attribute); if (attribute.name === 'style' && this._style) { this._style.cssText = ''; } + + return attribute; } /** - * Clones a node. - * * @override - * @param [deep=false] "true" to clone deep. - * @returns Cloned node. */ public cloneNode(deep = false): IHTMLElement { const clone = super.cloneNode(deep); diff --git a/packages/happy-dom/src/nodes/html-element/IHTMLElement.ts b/packages/happy-dom/src/nodes/html-element/IHTMLElement.ts index f79f8b019..35ab764da 100644 --- a/packages/happy-dom/src/nodes/html-element/IHTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/IHTMLElement.ts @@ -9,7 +9,6 @@ import IElement from '../element/IElement'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement. */ export default interface IHTMLElement extends IElement { - style: CSSStyleDeclaration; dataset: { [key: string]: string }; tabIndex: number; offsetHeight: number; @@ -48,6 +47,9 @@ export default interface IHTMLElement extends IElement { ontransitionrun: (event: Event) => void | null; ontransitionstart: (event: Event) => void | null; + get style(): CSSStyleDeclaration; + set style(cssText: CSSStyleDeclaration | string); + /** * Triggers a click event. */ diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 22ccefadf..956bba562 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -180,17 +180,17 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle } /** - * The setAttributeNode() method adds a new Attr node to the specified element. - * * @override - * @param attribute Attribute. - * @returns Replaced attribute. */ - public setAttributeNode(attribute: IAttr): IAttr { + public override setAttributeNode(attribute: IAttr): IAttr { const replacedAttribute = super.setAttributeNode(attribute); const rel = this.getAttributeNS(null, 'rel'); const href = this.getAttributeNS(null, 'href'); + if (attribute.name === 'rel' && this._relList) { + this._relList._updateIndices(); + } + if ( (attribute.name === 'rel' || attribute.name === 'href') && href !== null && @@ -233,6 +233,19 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle return replacedAttribute; } + /** + * @override + */ + public override removeAttributeNode(attribute: IAttr): IAttr { + super.removeAttributeNode(attribute); + + if (attribute.name === 'rel' && this._relList) { + this._relList._updateIndices(); + } + + return attribute; + } + /** * @override */ @@ -280,15 +293,4 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle } } } - - /** - * Updates DOM list indices. - */ - protected _updateDomListIndices(): void { - super._updateDomListIndices(); - - if (this._relList) { - this._relList._updateIndices(); - } - } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index a2b7a615b..8a56a4c97 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,6 +1,8 @@ +import IAttr from '../attr/IAttr'; import HTMLElement from '../html-element/HTMLElement'; import IHTMLElement from '../html-element/IHTMLElement'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement'; +import HTMLSelectElement from '../html-select-element/HTMLSelectElement'; import IHTMLOptionElement from './IHTMLOptionElement'; /** @@ -11,6 +13,8 @@ import IHTMLOptionElement from './IHTMLOptionElement'; */ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptionElement { public _index: number; + public _selectedness = false; + public _dirtyness = false; /** * Returns inner text, which is the rendered appearance of text. @@ -58,7 +62,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Selected. */ public get selected(): boolean { - return this.getAttributeNS(null, 'selected') !== null; + return this._selectedness; } /** @@ -67,10 +71,13 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @param selected Selected. */ public set selected(selected: boolean) { - if (!selected) { - this.removeAttributeNS(null, 'selected'); - } else { - this.setAttributeNS(null, 'selected', ''); + const selectElement = this._getSelectElement(); + + this._dirtyness = true; + this._selectedness = Boolean(selected); + + if (selectElement) { + selectElement._resetOptionSelectednes(this._selectedness ? this : null); } } @@ -102,7 +109,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Value. */ public get value(): string { - return this.getAttributeNS(null, 'value') || ''; + return this.getAttributeNS(null, 'value') || this.textContent; } /** @@ -113,4 +120,61 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio public set value(value: string) { this.setAttributeNS(null, 'value', value); } + + /** + * @override + */ + public setAttributeNode(attribute: IAttr): IAttr { + const replacedAttribute = super.setAttributeNode(attribute); + + if ( + !this._dirtyness && + attribute.name === 'selected' && + replacedAttribute?.value !== attribute.value + ) { + const selectElement = this._getSelectElement(); + + this._selectedness = true; + + if (selectElement) { + selectElement._resetOptionSelectednes(this); + } + } + + return replacedAttribute; + } + + /** + * @override + */ + public removeAttributeNode(attribute: IAttr): IAttr { + super.removeAttributeNode(attribute); + + if (!this._dirtyness && attribute.name === 'selected') { + const selectElement = this._getSelectElement(); + + this._selectedness = false; + + if (selectElement) { + selectElement._resetOptionSelectednes(); + } + } + + return attribute; + } + + /** + * Returns select element. + * + * @returns Select element. + */ + private _getSelectElement(): HTMLSelectElement { + const parentNode = this.parentNode; + if (parentNode?.tagName === 'SELECT') { + return parentNode; + } + if ((parentNode?.parentNode)?.tagName === 'SELECT') { + return parentNode.parentNode; + } + } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts index 9395783e6..8226dde2a 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts @@ -2,7 +2,6 @@ import DOMException from '../../exception/DOMException'; import HTMLCollection from '../element/HTMLCollection'; import IHTMLOptGroupElement from '../html-opt-group-element/IHTMLOptGroupElement'; import IHTMLSelectElement from '../html-select-element/IHTMLSelectElement'; -import HTMLOptionElement from './HTMLOptionElement'; import IHTMLOptionElement from './IHTMLOptionElement'; import IHTMLOptionsCollection from './IHTMLOptionsCollection'; @@ -34,14 +33,7 @@ export default class HTMLOptionsCollection * @returns SelectedIndex. */ public get selectedIndex(): number { - for (let i = 0; i < this.length; i++) { - const item = this[i]; - if (item instanceof HTMLOptionElement && item.selected) { - return i; - } - } - - return -1; + return this._selectElement.selectedIndex; } /** @@ -50,12 +42,7 @@ export default class HTMLOptionsCollection * @param selectedIndex SelectedIndex. */ public set selectedIndex(selectedIndex: number) { - for (let i = 0; i < this.length; i++) { - const item = this[i]; - if (item instanceof HTMLOptionElement) { - this[i].selected = i === selectedIndex; - } - } + this._selectElement.selectedIndex = selectedIndex; } /** diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 50ead16f0..b7437746c 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -1,5 +1,3 @@ -import DOMException from '../../exception/DOMException'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; import HTMLElement from '../html-element/HTMLElement'; import IHTMLElement from '../html-element/IHTMLElement'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement'; @@ -169,13 +167,14 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @returns Value. */ public get value(): string { - if (this.options.selectedIndex === -1) { - return ''; + for (let i = 0, max = this.options.length; i < max; i++) { + const option = this.options[i]; + if (option._selectedness) { + return option.value; + } } - const option = this.options[this.options.selectedIndex]; - - return option instanceof HTMLOptionElement ? option.value : ''; + return ''; } /** @@ -184,9 +183,15 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @param value Value. */ public set value(value: string) { - this.options.selectedIndex = this.options.findIndex( - (o) => o instanceof HTMLOptionElement && o.value === value - ); + for (let i = 0, max = this.options.length; i < max; i++) { + const option = this.options[i]; + if (option.value === value) { + option._selectedness = true; + option._dirtyness = true; + } else { + option._selectedness = false; + } + } } /** @@ -195,23 +200,31 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @returns Value. */ public get selectedIndex(): number { - return this.options.selectedIndex; + for (let i = 0, max = this.options.length; i < max; i++) { + if ((this.options[i])._selectedness) { + return i; + } + } + return -1; } /** * Sets value. * - * @param value Value. + * @param selectedIndex Selected index. */ - public set selectedIndex(value: number) { - if (value > this.options.length - 1 || value < -1) { - throw new DOMException( - 'Select elements selected index must be valid', - DOMExceptionNameEnum.indexSizeError - ); - } + public set selectedIndex(selectedIndex: number) { + if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { + for (let i = 0, max = this.options.length; i < max; i++) { + (this.options[i])._selectedness = false; + } - this.options.selectedIndex = value; + const selectedOption = this.options[selectedIndex]; + if (selectedOption) { + selectedOption._selectedness = true; + selectedOption._dirtyness = true; + } + } } /** @@ -299,6 +312,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec } this._updateIndexProperties(previousLength, this.options.length); + this._resetOptionSelectednes(); } return super.appendChild(node); @@ -342,6 +356,10 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec this._updateIndexProperties(previousLength, this.options.length); } + if (newNode.nodeType === NodeTypeEnum.elementNode) { + this._resetOptionSelectednes(); + } + return returnValue; } @@ -355,17 +373,89 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec if (element.tagName === 'OPTION' || element.tagName === 'OPTION') { const index = this.options.indexOf(node); + if (index !== -1) { this.options.splice(index, 1); } } this._updateIndexProperties(previousLength, this.options.length); + this._resetOptionSelectednes(); } return super.removeChild(node); } + /** + * Resets the option selectedness. + * + * Based on: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js + * + * @param [newOption] Optional new option element to be selected. + * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm + */ + public _resetOptionSelectednes(newOption?: IHTMLOptionElement): void { + if (this.hasAttributeNS(null, 'multiple')) { + return; + } + + const selected: HTMLOptionElement[] = []; + + for (let i = 0, max = this.options.length; i < max; i++) { + if (newOption) { + (this.options[i])._selectedness = this.options[i] === newOption; + } + + if ((this.options[i])._selectedness) { + selected.push(this.options[i]); + } + } + + const size = this._getDisplaySize(); + + if (size === 1 && !selected.length) { + for (let i = 0, max = this.options.length; i < max; i++) { + const option = this.options[i]; + + let disabled = option.hasAttributeNS(null, 'disabled'); + const parentNode = option.parentNode; + if ( + parentNode && + parentNode.nodeType === NodeTypeEnum.elementNode && + parentNode.tagName === 'OPTGROUP' && + parentNode.hasAttributeNS(null, 'disabled') + ) { + disabled = true; + } + + if (!disabled) { + option._selectedness = true; + break; + } + } + } else if (selected.length >= 2) { + for (let i = 0, max = this.options.length; i < max; i++) { + (this.options[i])._selectedness = i === selected.length - 1; + } + } + } + + /** + * Returns display size. + * + * @returns Display size. + */ + protected _getDisplaySize(): number { + if (this.hasAttributeNS(null, 'size')) { + const size = parseInt(this.getAttributeNS(null, 'size')); + if (!isNaN(size) && size >= 0) { + return size; + } + } + return this.hasAttributeNS(null, 'multiple') ? 4 : 1; + } + /** * Updates index properties. * diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index 12e91da93..0cafdcf0a 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -148,8 +148,8 @@ export default class NodeUtility { * @param elementB */ public static attributeListsEqual(elementA: IElement, elementB: IElement): boolean { - const listA = Object.values(elementA.attributes); - const listB = Object.values(elementB.attributes); + const listA = >Object.values(elementA['_attributes']); + const listB = >Object.values(elementB['_attributes']); const lengthA = listA.length; const lengthB = listB.length; diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 9de04377d..755721541 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -97,9 +97,10 @@ export default class ParentNodeUtility { ): IHTMLCollection { const upperTagName = tagName.toUpperCase(); const matches = HTMLCollectionFactory.create(); + const includeAll = tagName === '*'; for (const child of parentNode.children) { - if (child.tagName === upperTagName) { + if (includeAll || child.tagName === upperTagName) { matches.push(child); } for (const match of this.getElementsByTagName(child, tagName)) { @@ -125,9 +126,10 @@ export default class ParentNodeUtility { ): IHTMLCollection { const upperTagName = tagName.toUpperCase(); const matches = HTMLCollectionFactory.create(); + const includeAll = tagName === '*'; for (const child of parentNode.children) { - if (child.tagName === upperTagName && child.namespaceURI === namespaceURI) { + if ((includeAll || child.tagName === upperTagName) && child.namespaceURI === namespaceURI) { matches.push(child); } for (const match of this.getElementsByTagNameNS(child, namespaceURI, tagName)) { diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index 12136ecbd..664e0c10d 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -74,11 +74,7 @@ export default class SVGElement extends Element implements ISVGElement { } /** - * The setAttributeNode() method adds a new Attr node to the specified element. - * * @override - * @param attribute Attribute. - * @returns Replaced attribute. */ public setAttributeNode(attribute: IAttr): IAttr { const replacedAttribute = super.setAttributeNode(attribute); @@ -91,16 +87,15 @@ export default class SVGElement extends Element implements ISVGElement { } /** - * Removes an Attr node. - * * @override - * @param attribute Attribute. */ - public removeAttributeNode(attribute: IAttr): void { + public removeAttributeNode(attribute: IAttr): IAttr { super.removeAttributeNode(attribute); if (attribute.name === 'style' && this._style) { this._style.cssText = ''; } + + return attribute; } } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index ad0958c60..7bf78ad8c 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -104,15 +104,22 @@ export default class QuerySelector { } const selector = new SelectorItem(selectorParts[0]); - const result = selector.match(currentNode); + const result = selector.match(currentNode); - if ((targetNode === currentNode || !currentNode.parentNode) && !result.matches) { + if (result.matches && selectorParts.length === 1) { + return { + priorityWeight: priorityWeight + result.priorityWeight, + matches: true + }; + } + + if (!currentNode.parentElement || (targetNode === currentNode && !result.matches)) { return { priorityWeight: 0, matches: false }; } return this.matchesSelector( - isDirectChild ? currentNode.parentNode : targetNode, - currentNode.parentNode, + isDirectChild ? currentNode.parentElement : targetNode, + currentNode.parentElement, result.matches ? selectorParts.slice(1) : selectorParts, priorityWeight + result.priorityWeight ); diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 3d9a61eb9..94dee772f 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -95,6 +95,7 @@ import MediaQueryList from '../match-media/MediaQueryList'; import DOMRect from '../nodes/element/DOMRect'; import Window from './Window'; import Attr from '../nodes/attr/Attr'; +import NamedNodeMap from '../named-node-map/NamedNodeMap'; import { URLSearchParams } from 'url'; import { Performance } from 'perf_hooks'; import IElement from '../nodes/element/IElement'; @@ -134,6 +135,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { readonly HTMLBaseElement: typeof HTMLBaseElement; readonly HTMLDialogElement: typeof HTMLDialogElement; readonly Attr: typeof Attr; + readonly NamedNodeMap: typeof NamedNodeMap; readonly SVGSVGElement: typeof SVGSVGElement; readonly SVGElement: typeof SVGElement; readonly Image: typeof Image; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 5120800a6..8f988f74d 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -108,6 +108,7 @@ import { Buffer } from 'buffer'; import Base64 from '../base64/Base64'; import IDocument from '../nodes/document/IDocument'; import Attr from '../nodes/attr/Attr'; +import NamedNodeMap from '../named-node-map/NamedNodeMap'; import IElement from '../nodes/element/IElement'; import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction'; @@ -167,6 +168,7 @@ export default class Window extends EventTarget implements IWindow { public readonly HTMLBaseElement = HTMLBaseElement; public readonly HTMLDialogElement = HTMLDialogElement; public readonly Attr = Attr; + public readonly NamedNodeMap = NamedNodeMap; public readonly SVGSVGElement = SVGSVGElement; public readonly SVGElement = SVGElement; public readonly Text = Text; @@ -250,20 +252,20 @@ export default class Window extends EventTarget implements IWindow { // Public Properties public readonly document: Document; - public readonly customElements: CustomElementRegistry = new CustomElementRegistry(); - public readonly location = new Location(); - public readonly history = new History(); - public readonly navigator = new Navigator(); + public readonly customElements: CustomElementRegistry; + public readonly location: Location; + public readonly history: History; + public readonly navigator: Navigator; public readonly console = console; public readonly self = this; public readonly top = this; public readonly parent = this; public readonly window = this; public readonly globalThis = this; - public readonly screen = new Screen(); + public readonly screen: Screen; public readonly devicePixelRatio = 1; - public readonly sessionStorage = new Storage(); - public readonly localStorage = new Storage(); + public readonly sessionStorage: Storage; + public readonly localStorage: Storage; public readonly performance = PerfHooks.performance; public readonly innerWidth: number; public readonly innerHeight: number; @@ -348,6 +350,14 @@ export default class Window extends EventTarget implements IWindow { constructor(options?: { innerWidth?: number; innerHeight?: number; url?: string }) { super(); + this.customElements = new CustomElementRegistry(); + this.location = new Location(); + this.navigator = new Navigator(); + this.history = new History(); + this.screen = new Screen(); + this.sessionStorage = new Storage(); + this.localStorage = new Storage(); + this.innerWidth = options?.innerWidth ? options.innerWidth : 0; this.innerHeight = options?.innerHeight ? options.innerHeight : 0; diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index 98ed92be7..9cb8e787d 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -42,76 +42,80 @@ export default class XMLParser { let lastTextIndex = 0; let match: RegExpExecArray; - while ((match = markupRegexp.exec(data))) { - const tagName = match[2].toLowerCase(); - const isStartTag = !match[1]; + if (data !== null && data !== undefined) { + data = String(data); - if (parent && match.index !== lastTextIndex) { - const text = data.substring(lastTextIndex, match.index); - this.appendTextAndCommentNodes(document, parent, text); - } - - if (isStartTag) { - const namespaceURI = - tagName === 'svg' - ? NamespaceURI.svg - : (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') { - (newElement)._evaluateScript = evaluateScripts; - } + while ((match = markupRegexp.exec(data))) { + const tagName = match[2].toLowerCase(); + const isStartTag = !match[1]; - // An assumption that the same rule should be applied for the HTMLLinkElement is made here. - if (tagName === 'link') { - (newElement)._evaluateCSS = evaluateScripts; + if (parent && match.index !== lastTextIndex) { + const text = data.substring(lastTextIndex, match.index); + this.appendTextAndCommentNodes(document, parent, text); } - this.setAttributes(newElement, match[3]); + if (isStartTag) { + const namespaceURI = + tagName === 'svg' + ? NamespaceURI.svg + : (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') { + (newElement)._evaluateScript = evaluateScripts; + } - if (!match[4] && !VoidElements.includes(tagName)) { - // Some elements are not allowed to be nested (e.g. "" is not allowed.). - // Therefore we will auto-close the tag. - if (parentUnnestableTagName === tagName) { - stack.pop(); - parent = parent.parentNode || root; + // An assumption that the same rule should be applied for the HTMLLinkElement is made here. + if (tagName === 'link') { + (newElement)._evaluateCSS = evaluateScripts; } - parent = parent.appendChild(newElement); - parentUnnestableTagName = this.getUnnestableTagName(parent); - stack.push(parent); - } else { - parent.appendChild(newElement); - } - lastTextIndex = markupRegexp.lastIndex; - - // Tags which contain non-parsed content - // For example: