Skip to content

Commit

Permalink
Merge pull request #1169 from takenspc/task/1168-normalize-whitespace
Browse files Browse the repository at this point in the history
#1168@patch: Normalize whitespace in processing of DOMTokenList.
  • Loading branch information
capricorn86 authored Jan 14, 2024
2 parents 2d0c458 + ccda746 commit ee4eb6f
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 24 deletions.
67 changes: 43 additions & 24 deletions packages/happy-dom/src/dom-token-list/DOMTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ 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.
*
* Reference:
* 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;

Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
Expand All @@ -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<string> {
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();
}

/**
Expand All @@ -103,19 +109,15 @@ 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);
}

/**
* Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object.
*
*/
public keys(): IterableIterator<number> {
const attr = this.#ownerElement.getAttribute(this.#attributeName);
const list = attr ? Array.from(new Set(attr.split(' '))) : [];
return list.keys();
return this.#getTokenList().keys();
}

/**
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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];
Expand All @@ -210,7 +209,27 @@ export default class DOMTokenList implements IDOMTokenList {
this[i] = list[i];
}

(<number>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;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ');
});
});
});

0 comments on commit ee4eb6f

Please sign in to comment.