From 5824775c58a4abb450f9b42a05670bb6eefa2e6a Mon Sep 17 00:00:00 2001 From: Takeshi Kurosawa Date: Fri, 24 Nov 2023 16:32:21 +0900 Subject: [PATCH 1/4] #1168@patch: Normalize whitespace in processing of DOMTokenList. --- .../src/dom-token-list/DOMTokenList.ts | 40 ++++++++++--------- .../test/dom-token-list/DOMTokenList.test.ts | 8 ++++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index 52f9cd4ef..2494aa9f4 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -57,8 +57,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param newToken NewToken. */ public replace(token: string, newToken: string): boolean { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); const index = list.indexOf(token); if (index === -1) { return false; @@ -81,8 +80,7 @@ export default class DOMTokenList implements IDOMTokenList { * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. */ public values(): IterableIterator { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); return list.values(); } @@ -90,8 +88,7 @@ export default class DOMTokenList implements IDOMTokenList { * Returns an iterator, allowing you to go through all key/value pairs contained in this object. */ public entries(): IterableIterator<[number, string]> { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); return list.entries(); } @@ -102,8 +99,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param thisArg */ public forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); return list.forEach(callback, thisArg); } @@ -112,8 +108,7 @@ export default class DOMTokenList implements IDOMTokenList { * */ public keys(): IterableIterator { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); return list.keys(); } @@ -123,8 +118,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param tokens Tokens. */ public add(...tokens: string[]): void { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); for (const token of tokens) { const index = list.indexOf(token); @@ -144,8 +138,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param tokens Tokens. */ public remove(...tokens: string[]): void { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); for (const token of tokens) { const index = list.indexOf(token); @@ -164,8 +157,8 @@ export default class DOMTokenList implements IDOMTokenList { * @returns TRUE if it contains. */ public contains(className: string): boolean { - const attr = this._ownerElement.getAttribute(this._attributeName); - return (attr ? attr.split(' ') : []).includes(className); + const list = this._getTokenList(); + return list.includes(className); } /** @@ -198,8 +191,7 @@ export default class DOMTokenList implements IDOMTokenList { * Updates indices. */ public _updateIndices(): void { - const attr = this._ownerElement.getAttribute(this._attributeName); - const list = attr ? Array.from(new Set(attr.split(' '))) : []; + const list = this._getTokenList(); for (let i = list.length - 1, max = this.length; i < max; i++) { delete this[i]; @@ -212,6 +204,18 @@ export default class DOMTokenList implements IDOMTokenList { (this.length) = list.length; } + /** + * Returns token list from attribute value. + * + * @see https://infra.spec.whatwg.org/#split-on-ascii-whitespace + */ + private _getTokenList(): string[] { + let attr = this._ownerElement.getAttribute(this._attributeName) ?? ''; + attr = attr.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, ''); + attr = attr.replace(/[\t\n\f\r ]+/g, ' '); + return attr === '' ? [] : Array.from(new Set(attr.split(' '))); + } + /** * Returns DOMTokenList value. */ diff --git a/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts b/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts index b57aa0149..3951c7c7a 100644 --- a/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts +++ b/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts @@ -211,4 +211,12 @@ describe('DOMTokenList', () => { expect(element.classList.toString()).toEqual('class1 class2 class3'); }); }); + + describe('whitespace handling', () => { + it('Normalizes whitespace to a single space', () => { + element.className = ' class1 class2\nclass3 '; + expect(Array.from(element.classList.values())).toEqual(['class1', 'class2', 'class3']); + expect(element.classList.toString()).toEqual(' class1 class2\nclass3 '); + }); + }); }); From 1f5a30137f4ad0ee944bf5c1b2732a41be14347d Mon Sep 17 00:00:00 2001 From: Takeshi Kurosawa Date: Tue, 28 Nov 2023 21:18:29 +0900 Subject: [PATCH 2/4] #1168@trivial: Merge replace() and split(). * String.prototype.split() accepts RegExp. * RegExp's g flag is ignored in split() so that g flag was removed. --- packages/happy-dom/src/dom-token-list/DOMTokenList.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index 2494aa9f4..0276df7a0 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -212,8 +212,7 @@ export default class DOMTokenList implements IDOMTokenList { private _getTokenList(): string[] { let attr = this._ownerElement.getAttribute(this._attributeName) ?? ''; attr = attr.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, ''); - attr = attr.replace(/[\t\n\f\r ]+/g, ' '); - return attr === '' ? [] : Array.from(new Set(attr.split(' '))); + return attr === '' ? [] : Array.from(new Set(attr.split(/[\t\n\f\r ]+/))); } /** From abc3ad403949e345b51a63b7d63fcc45ebded908 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 15 Jan 2024 00:32:22 +0100 Subject: [PATCH 3/4] #1168@trivial: Fixes problem with length property. --- packages/happy-dom/src/dom-token-list/DOMTokenList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index 266987f5e..c5d4aa5a9 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -209,7 +209,7 @@ export default class DOMTokenList implements IDOMTokenList { this[i] = list[i]; } - (this.length) = list.length; + this.#length = list.length; } /** From ccda74695947c56f180c2b5e8df7c2d8f9d615c1 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 15 Jan 2024 00:38:24 +0100 Subject: [PATCH 4/4] #1168@trivial: Fixes problem with whitespace before and/or after in DOMTokenList. --- packages/happy-dom/src/dom-token-list/DOMTokenList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index c5d4aa5a9..a36a24102 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -224,7 +224,7 @@ export default class DOMTokenList implements IDOMTokenList { } // It is possible to make this statement shorter by using Array.from() and Set, but this is faster when comparing using a bench test. const list = []; - for (const item of attr.split(ATTRIBUTE_SPLIT_REGEXP)) { + for (const item of attr.trim().split(ATTRIBUTE_SPLIT_REGEXP)) { if (!list.includes(item)) { list.push(item); }