From c54734d9f474817a4573cafbd153d3c2c34cb0d0 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 10 Nov 2022 00:20:40 +0100 Subject: [PATCH] #204@trivial: Improves performance and fixes some minor bugs related to blob urls. --- .../src/dom-token-list/DOMTokenList.ts | 2 +- .../happy-dom/src/nodes/element/Element.ts | 17 +-- .../html-anchor-element/HTMLAnchorElement.ts | 108 +++++++----------- .../HTMLAnchorElementUtility.ts | 48 ++++++++ .../html-link-element/HTMLLinkElement.ts | 34 +++--- .../HTMLAnchorElement.test.ts | 73 ++++++++++++ 6 files changed, 187 insertions(+), 95 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementUtility.ts 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/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index a7f3140dc..70063c6ea 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -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 && @@ -936,7 +938,9 @@ export default class Element extends Node implements IElement { this.ownerDocument['_cacheID']++; } - this._updateDomListIndices(); + if (attribute.name === 'class' && this._classList) { + this._classList._updateIndices(); + } if ( this.attributeChangedCallback && @@ -1033,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/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index c2c4da280..7feab26eb 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -3,6 +3,8 @@ 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. @@ -14,15 +16,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho private _relList: DOMTokenList = null; private _url: URL | null = null; - /** - * Constructor - */ - constructor() { - super(); - - this._setUrl(); - } - /** * Returns download. * @@ -47,7 +40,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Hash. */ public get hash(): string { - this._reinitializeUrl(); return this._url?.hash ?? ''; } @@ -57,8 +49,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param hash Hash. */ public set hash(hash: string) { - this._reinitializeUrl(); - if (this._url) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { this._url.hash = hash; this.setAttributeNS(null, 'href', this._url.toString()); } @@ -70,8 +61,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Href. */ public get href(): string | null { - this._reinitializeUrl(); - if (this._url) { return this._url.toString(); } @@ -86,8 +75,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set href(href: string) { this.setAttributeNS(null, 'href', href); - - this._setUrl(); } /** @@ -114,7 +101,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Origin. */ public get origin(): string { - this._reinitializeUrl(); return this._url?.origin ?? ''; } @@ -142,7 +128,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Protocol. */ public get protocol(): string { - this._reinitializeUrl(); return this._url?.protocol ?? ''; } @@ -152,8 +137,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param protocol Protocol. */ public set protocol(protocol: string) { - this._reinitializeUrl(); - if (this._url) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { this._url.protocol = protocol; this.setAttributeNS(null, 'href', this._url.toString()); } @@ -165,7 +149,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Username. */ public get username(): string { - this._reinitializeUrl(); return this._url?.username ?? ''; } @@ -175,8 +158,12 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param username Username. */ public set username(username: string) { - this._reinitializeUrl(); - if (this._url && this._url.host && this._url.protocol != 'file') { + 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()); } @@ -188,7 +175,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Password. */ public get password(): string { - this._reinitializeUrl(); return this._url?.password ?? ''; } @@ -198,8 +184,12 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param password Password. */ public set password(password: string) { - this._reinitializeUrl(); - if (this._url && this._url.host && this._url.protocol != 'file') { + 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()); } @@ -211,7 +201,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Pathname. */ public get pathname(): string { - this._reinitializeUrl(); return this._url?.pathname ?? ''; } @@ -221,8 +210,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param pathname Pathname. */ public set pathname(pathname: string) { - this._reinitializeUrl(); - if (this._url) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { this._url.pathname = pathname; this.setAttributeNS(null, 'href', this._url.toString()); } @@ -234,7 +222,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Port. */ public get port(): string { - this._reinitializeUrl(); return this._url?.port ?? ''; } @@ -244,8 +231,12 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param port Port. */ public set port(port: string) { - this._reinitializeUrl(); - if (this._url && this._url.host && this._url.protocol != 'file') { + 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()); } @@ -257,7 +248,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Host. */ public get host(): string { - this._reinitializeUrl(); return this._url?.host ?? ''; } @@ -267,8 +257,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param host Host. */ public set host(host: string) { - this._reinitializeUrl(); - if (this._url) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { this._url.host = host; this.setAttributeNS(null, 'href', this._url.toString()); } @@ -280,7 +269,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Hostname. */ public get hostname(): string { - this._reinitializeUrl(); return this._url?.hostname ?? ''; } @@ -290,8 +278,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param hostname Hostname. */ public set hostname(hostname: string) { - this._reinitializeUrl(); - if (this._url) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { this._url.hostname = hostname; this.setAttributeNS(null, 'href', this._url.toString()); } @@ -351,7 +338,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Search. */ public get search(): string { - this._reinitializeUrl(); return this._url?.search ?? ''; } @@ -361,8 +347,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param search Search. */ public set search(search: string) { - this._reinitializeUrl(); - if (this._url) { + if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { this._url.search = search; this.setAttributeNS(null, 'href', this._url.toString()); } @@ -423,43 +408,32 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho } /** - * Updates DOM list indices. + * @override */ - protected override _updateDomListIndices(): void { - super._updateDomListIndices(); + public override setAttributeNode(attribute: IAttr): IAttr { + const replacedAttribute = super.setAttributeNode(attribute); - if (this._relList) { + if (attribute.name === 'rel' && this._relList) { this._relList._updateIndices(); - } - } - - /** - * Reinitialize URL from href attribute - */ - private _reinitializeUrl(): void { - // If element's url is non-null, its scheme is "blob", and it has an opaque path, then terminate these steps. - if (this._url?.protocol === 'blob' && this._url.pathname.length > 1) { - return; + } else if (attribute.name === 'href') { + this._url = HTMLAnchorElementUtility.getUrl(this.ownerDocument, attribute.value); } - this._setUrl(); + return replacedAttribute; } /** - * Initialize URL from href attribute + * @override */ - private _setUrl(): void { - const hrefAttr = this.getAttributeNS(null, 'href'); - if (!hrefAttr) { - this._url = null; - } - - const documentUrl = this.ownerDocument?.location?.href; + public override removeAttributeNode(attribute: IAttr): IAttr { + super.removeAttributeNode(attribute); - try { - this._url = new URL(hrefAttr.trim(), documentUrl); - } catch (TypeError) { + 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-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/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index 1a856cfce..b6d18fd28 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -3,6 +3,8 @@ import IWindow from '../../../src/window/IWindow'; import IDocument from '../../../src/nodes/document/IDocument'; import IHTMLAnchorElement from '../../../src/nodes/html-anchor-element/IHTMLAnchorElement'; +const BLOB_URL = 'blob:https://mozilla.org'; + describe('HTMLAnchorElement', () => { let window: IWindow; let document: IDocument; @@ -82,6 +84,14 @@ describe('HTMLAnchorElement', () => { element.href = 'test'; expect(element.getAttribute('href')).toBe('test'); }); + + it('Can be set after a blob URL has been defined.', () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + expect(element.href).toBe(BLOB_URL); + element.href = 'https://example.com/'; + expect(element.href).toBe('https://example.com/'); + }); }); describe('get origin()', () => { @@ -123,6 +133,13 @@ describe('HTMLAnchorElement', () => { expect(element.protocol).toBe('http:'); expect(element.href).toBe('http://www.example.com/path?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.protocol = 'http'; + expect(element.protocol).toBe('blob:'); + }); }); describe('get username()', () => { @@ -144,6 +161,13 @@ describe('HTMLAnchorElement', () => { expect(element.username).toBe('user2'); expect(element.href).toBe('https://user2:pw@www.example.com/path?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.username = 'user2'; + expect(element.username).toBe(''); + }); }); describe('get password()', () => { @@ -165,6 +189,13 @@ describe('HTMLAnchorElement', () => { expect(element.password).toBe('pw2'); expect(element.href).toBe('https://user:pw2@www.example.com/path?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.password = 'pw2'; + expect(element.password).toBe(''); + }); }); describe('get host()', () => { @@ -186,6 +217,13 @@ describe('HTMLAnchorElement', () => { expect(element.host).toBe('abc.example2.com'); expect(element.href).toBe('https://abc.example2.com/path?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.host = 'abc.example2.com'; + expect(element.host).toBe(''); + }); }); describe('get hostname()', () => { @@ -207,6 +245,13 @@ describe('HTMLAnchorElement', () => { expect(element.hostname).toBe('abc.example2.com'); expect(element.href).toBe('https://abc.example2.com/path?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.hostname = 'abc.example2.com'; + expect(element.hostname).toBe(''); + }); }); describe('get port()', () => { @@ -231,6 +276,13 @@ describe('HTMLAnchorElement', () => { expect(element.port).toBe('8080'); expect(element.href).toBe('https://www.example.com:8080/path?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.port = '8080'; + expect(element.port).toBe(''); + }); }); describe('get pathname()', () => { @@ -252,6 +304,13 @@ describe('HTMLAnchorElement', () => { expect(element.pathname).toBe('/path2'); expect(element.href).toBe('https://www.example.com/path2?q1=a#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.pathname = '/path2'; + expect(element.pathname).toBe(BLOB_URL.split(':').slice(1).join(':')); + }); }); describe('get search()', () => { @@ -273,6 +332,13 @@ describe('HTMLAnchorElement', () => { expect(element.search).toBe('?q1=b'); expect(element.href).toBe('https://www.example.com/path?q1=b#xyz'); }); + + it("Can't be modified on blob URLs.", () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.search = '?q1=b'; + expect(element.search).toBe(''); + }); }); describe('get hash()', () => { @@ -294,5 +360,12 @@ describe('HTMLAnchorElement', () => { expect(element.hash).toBe('#fgh'); expect(element.href).toBe('https://www.example.com/path?q1=a#fgh'); }); + + it('Can be modified on blob URLs.', () => { + const element = document.createElement('a'); + element.href = BLOB_URL; + element.hash = '#fgh'; + expect(element.hash).toBe(''); + }); }); });