diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index 04b40e89c..a36a24102 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -2,6 +2,8 @@ import Element from '../nodes/element/Element.js'; import * as PropertySymbol from '../PropertySymbol.js'; import IDOMTokenList from './IDOMTokenList.js'; +const ATTRIBUTE_SPLIT_REGEXP = /[\t\f\n\r ]+/; + /** * DOM Token List. * @@ -9,7 +11,7 @@ import IDOMTokenList from './IDOMTokenList.js'; * https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList. */ export default class DOMTokenList implements IDOMTokenList { - public readonly length = 0; + #length = 0; #ownerElement: Element; #attributeName: string; @@ -25,6 +27,15 @@ export default class DOMTokenList implements IDOMTokenList { this[PropertySymbol.updateIndices](); } + /** + * Returns length. + * + * @returns Length. + */ + public get length(): number { + return this.#length; + } + /** * Set value. * @@ -58,8 +69,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; @@ -82,18 +92,14 @@ 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(' '))) : []; - return list.values(); + return this.#getTokenList().values(); } /** * 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(' '))) : []; - return list.entries(); + return this.#getTokenList().entries(); } /** @@ -103,9 +109,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(' '))) : []; - return list.forEach(callback, thisArg); + return this.#getTokenList().forEach(callback, thisArg); } /** @@ -113,9 +117,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(' '))) : []; - return list.keys(); + return this.#getTokenList().keys(); } /** @@ -124,8 +126,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); @@ -145,8 +146,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); @@ -165,8 +165,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); } /** @@ -199,8 +199,7 @@ export default class DOMTokenList implements IDOMTokenList { * Updates indices. */ public [PropertySymbol.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]; @@ -210,7 +209,27 @@ export default class DOMTokenList implements IDOMTokenList { this[i] = list[i]; } - (this.length) = list.length; + this.#length = list.length; + } + + /** + * Returns token list from attribute value. + * + * @see https://infra.spec.whatwg.org/#split-on-ascii-whitespace + */ + #getTokenList(): string[] { + const attr = this.#ownerElement.getAttribute(this.#attributeName); + if (!attr) { + return []; + } + // 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.trim().split(ATTRIBUTE_SPLIT_REGEXP)) { + if (!list.includes(item)) { + list.push(item); + } + } + return list; } /** 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 '); + }); + }); });