From 6c5e0f9064becd607d279343555ad95ccb2e2fce Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sun, 23 Feb 2025 18:32:26 +0100 Subject: [PATCH] fix: [#1605] Fixes issue related to the use of filtering in TreeWalker, which caused it not to work according to spec (#1737) * fix: [#1605] Fixes issue when using filtering in TreeWalker, which caused it not to work according to spec * fix: [#1605] Fixes issue when using filtering in TreeWalker, which caused it not to work according to spec --- .../happy-dom/src/tree-walker/TreeWalker.ts | 293 ++++++++++++------ .../test/tree-walker/TreeWalker.test.ts | 124 +++++++- 2 files changed, 316 insertions(+), 101 deletions(-) diff --git a/packages/happy-dom/src/tree-walker/TreeWalker.ts b/packages/happy-dom/src/tree-walker/TreeWalker.ts index f4a34b733..9e787540f 100644 --- a/packages/happy-dom/src/tree-walker/TreeWalker.ts +++ b/packages/happy-dom/src/tree-walker/TreeWalker.ts @@ -5,8 +5,21 @@ import NodeFilterMask from './NodeFilterMask.js'; import DOMException from '../exception/DOMException.js'; import NodeFilter from './NodeFilter.js'; +enum TraverseChildrenTypeEnum { + first = 'first', + last = 'last' +} + +enum TraverseSiblingsTypeEnum { + next = 'next', + previous = 'previous' +} + /** * The TreeWalker object represents the nodes of a document subtree and a position within them. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker + * @see https://dom.spec.whatwg.org/#interface-treewalker */ export default class TreeWalker { public root: Node = null; @@ -32,52 +45,20 @@ export default class TreeWalker { this.currentNode = root; } - /** - * Moves the current Node to the next visible node in the document order. - * - * @returns Current node. - */ - public nextNode(): Node { - if (!this.firstChild()) { - while (!this.nextSibling() && this.parentNode()) {} - this.currentNode = this.currentNode === this.root ? null : this.currentNode || null; - } - return this.currentNode; - } - - /** - * Moves the current Node to the previous visible node in the document order, and returns the found node. It also moves the current node to this one. If no such node exists, or if it is before that the root node defined at the object construction, returns null and the current node is not changed. - * - * @returns Current node. - */ - public previousNode(): Node { - while (!this.previousSibling() && this.parentNode()) {} - this.currentNode = this.currentNode === this.root ? null : this.currentNode || null; - return this.currentNode; - } - /** * Moves the current Node to the first visible ancestor node in the document order, and returns the found node. It also moves the current node to this one. If no such node exists, or if it is before that the root node defined at the object construction, returns null and the current node is not changed. * * @returns Current node. */ public parentNode(): Node { - if ( - this.currentNode !== this.root && - this.currentNode && - this.currentNode[PropertySymbol.parentNode] - ) { - this.currentNode = this.currentNode[PropertySymbol.parentNode]; - - if (this[PropertySymbol.filterNode](this.currentNode) === NodeFilter.FILTER_ACCEPT) { + let node = this.currentNode; + while (node !== null && node !== this.root) { + node = node.parentNode; + if (node !== null && this[PropertySymbol.filterNode](node) === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; return this.currentNode; } - - this.parentNode(); } - - this.currentNode = null; - return null; } @@ -87,19 +68,7 @@ export default class TreeWalker { * @returns Current node. */ public firstChild(): Node { - const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.nodeArray] : []; - - if (childNodes.length > 0) { - this.currentNode = childNodes[0]; - - if (this[PropertySymbol.filterNode](this.currentNode) === NodeFilter.FILTER_ACCEPT) { - return this.currentNode; - } - - return this.nextSibling(); - } - - return null; + return this.#traverseChildren(TraverseChildrenTypeEnum.first); } /** @@ -108,19 +77,16 @@ export default class TreeWalker { * @returns Current node. */ public lastChild(): Node { - const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.nodeArray] : []; - - if (childNodes.length > 0) { - this.currentNode = childNodes[childNodes.length - 1]; - - if (this[PropertySymbol.filterNode](this.currentNode) === NodeFilter.FILTER_ACCEPT) { - return this.currentNode; - } - - return this.previousSibling(); - } + return this.#traverseChildren(TraverseChildrenTypeEnum.last); + } - return null; + /** + * Moves the current Node to its next sibling, if any, and returns the found sibling. If there is no such node, null is returned and the current node is not changed. + * + * @returns Current node. + */ + public nextSibling(): Node { + return this.#traverseSiblings(TraverseSiblingsTypeEnum.next); } /** @@ -129,24 +95,46 @@ export default class TreeWalker { * @returns Current node. */ public previousSibling(): Node { - if ( - this.currentNode !== this.root && - this.currentNode && - this.currentNode[PropertySymbol.parentNode] - ) { - const siblings = (this.currentNode[PropertySymbol.parentNode])[ - PropertySymbol.nodeArray - ]; - const index = siblings.indexOf(this.currentNode); - - if (index > 0) { - this.currentNode = siblings[index - 1]; - - if (this[PropertySymbol.filterNode](this.currentNode) === NodeFilter.FILTER_ACCEPT) { - return this.currentNode; + return this.#traverseSiblings(TraverseSiblingsTypeEnum.previous); + } + + /** + * Moves the current Node to the previous visible node in the document order, and returns the found node. It also moves the current node to this one. If no such node exists, or if it is before that the root node defined at the object construction, returns null and the current node is not changed. + * + * @returns Current node. + */ + public previousNode(): Node { + let node = this.currentNode; + + while (node !== this.root) { + let sibling = node.previousSibling; + + while (sibling !== null) { + let node = sibling; + let result = this[PropertySymbol.filterNode](node); + + while (result !== NodeFilter.FILTER_REJECT && node[PropertySymbol.nodeArray].length) { + node = node.lastChild; + result = this[PropertySymbol.filterNode](node); } - return this.previousSibling(); + if (result === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; + return node; + } + + sibling = node.previousSibling; + } + + if (node === this.root || node.parentNode === null) { + return null; + } + + node = node.parentNode; + + if (this[PropertySymbol.filterNode](node) === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; + return node; } } @@ -154,33 +142,51 @@ export default class TreeWalker { } /** - * Moves the current Node to its next sibling, if any, and returns the found sibling. If there is no such node, null is returned and the current node is not changed. + * Moves the current Node to the next visible node in the document order. * * @returns Current node. */ - public nextSibling(): Node { - if ( - this.currentNode !== this.root && - this.currentNode && - this.currentNode[PropertySymbol.parentNode] - ) { - const siblings = (this.currentNode[PropertySymbol.parentNode])[ - PropertySymbol.nodeArray - ]; - const index = siblings.indexOf(this.currentNode); - - if (index + 1 < siblings.length) { - this.currentNode = siblings[index + 1]; - - if (this[PropertySymbol.filterNode](this.currentNode) === NodeFilter.FILTER_ACCEPT) { - return this.currentNode; + public nextNode(): Node | null { + let node = this.currentNode; + let result = NodeFilter.FILTER_ACCEPT; + + while (true) { + while (result !== NodeFilter.FILTER_REJECT && node[PropertySymbol.nodeArray].length) { + node = node.firstChild; + result = this[PropertySymbol.filterNode](node); + + if (result === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; + return node; } + } - return this.nextSibling(); + while (node !== null) { + if (node === this.root) { + return null; + } + + const sibling = node.nextSibling; + + if (sibling !== null) { + node = sibling; + break; + } + + node = node.parentNode; } - } - return null; + if (node === null) { + return null; + } + + result = this[PropertySymbol.filterNode](node); + + if (result === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; + return node; + } + } } /** @@ -207,4 +213,95 @@ export default class TreeWalker { return NodeFilter.FILTER_ACCEPT; } + + /** + * Traverses children. + * + * @param type Type. + * @returns Node. + */ + #traverseChildren(type: TraverseChildrenTypeEnum): Node | null { + let node: Node = this.currentNode; + node = type === TraverseChildrenTypeEnum.first ? node.firstChild : node.lastChild; + + while (node !== null) { + const result = this[PropertySymbol.filterNode](node); + + if (result === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; + return node; + } + + if (result === NodeFilter.FILTER_SKIP) { + const child = type === TraverseChildrenTypeEnum.first ? node.firstChild : node.lastChild; + + if (child !== null) { + node = child; + continue; + } + } + + while (node !== null) { + const sibling = + type === TraverseChildrenTypeEnum.first ? node.nextSibling : node.previousSibling; + if (sibling !== null) { + node = sibling; + break; + } + const parent = node.parentNode; + if (parent === null || parent === this.root || parent === this.currentNode) { + return null; + } + node = parent; + } + } + + return null; + } + + /** + * Traverses siblings. + * + * @param type Type. + * @returns Node. + */ + #traverseSiblings(type: TraverseSiblingsTypeEnum): Node | null { + let node: Node = this.currentNode; + + if (node === this.root) { + return null; + } + + while (true) { + let sibling = + type === TraverseSiblingsTypeEnum.next ? node.nextSibling : node.previousSibling; + + while (sibling !== null) { + const node = sibling; + const result = this[PropertySymbol.filterNode](node); + + if (result === NodeFilter.FILTER_ACCEPT) { + this.currentNode = node; + return node; + } + + sibling = type === TraverseSiblingsTypeEnum.next ? node.firstChild : node.lastChild; + + if (result === NodeFilter.FILTER_REJECT || sibling === null) { + sibling = + type === TraverseSiblingsTypeEnum.next ? node.nextSibling : node.previousSibling; + } + } + + node = node.parentNode; + + if (node === null || node === this.root) { + return null; + } + + if (this[PropertySymbol.filterNode](node) === NodeFilter.FILTER_ACCEPT) { + return null; + } + } + } } diff --git a/packages/happy-dom/test/tree-walker/TreeWalker.test.ts b/packages/happy-dom/test/tree-walker/TreeWalker.test.ts index cdffc7828..f22f5a5f6 100644 --- a/packages/happy-dom/test/tree-walker/TreeWalker.test.ts +++ b/packages/happy-dom/test/tree-walker/TreeWalker.test.ts @@ -185,17 +185,65 @@ describe('TreeWalker', () => { it('Returns the parent node.', () => { const treeWalker = document.createTreeWalker(document.body); const node = treeWalker.nextNode(); - expect(treeWalker.parentNode() === node.parentNode).toBe(true); + expect(treeWalker.parentNode() === node?.parentNode).toBe(true); }); - }); - describe('parentNode()', () => { it('Returns null if there is no parent.', () => { const treeWalker = document.createTreeWalker(document.body); treeWalker.nextNode(); treeWalker.parentNode(); expect(treeWalker.parentNode() === null).toBe(true); }); + + it('Returns parent node in a hierarchy', () => { + const div = document.createElement('div'); + div.innerHTML = ` + + +
+ Span + B1 +
+
+ B2 +
+
+ `; + const treeWalker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT); + + treeWalker.currentNode = div.querySelector('b'); + + expect(treeWalker.parentNode()).toBe(div.querySelector('div')); + expect(treeWalker.parentNode()).toBe(div.querySelector('span')); + expect(treeWalker.parentNode()).toBe(div); + expect(treeWalker.parentNode()).toBe(null); + }); + + it('Returns parent node in a hierarchy using filtering', () => { + const div = document.createElement('div'); + div.innerHTML = ` + + +
+ Span + B1 +
+
+ B2 +
+
+ `; + const treeWalker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, { + acceptNode: (node: Node) => + (node).tagName === 'DIV' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP + }); + + treeWalker.currentNode = div.querySelector('b'); + + expect(treeWalker.parentNode()).toBe(div.querySelector('div')); + expect(treeWalker.parentNode()).toBe(div); + expect(treeWalker.parentNode()).toBe(null); + }); }); describe('firstChild()', () => { @@ -205,5 +253,75 @@ describe('TreeWalker', () => { expect(node).toBeInstanceOf(Element); expect(treeWalker.firstChild()).toBe((node).firstElementChild); }); + + it('Returns first sibling when it matches', () => { + const div = document.createElement('div'); + div.innerHTML = ` + + +
+ Span + B1 +
+
+ B2 +
+
+ `; + const treeWalker = document.createTreeWalker(div, NodeFilter.SHOW_ALL, { + acceptNode: (node: Node) => + node.nodeType === Node.ELEMENT_NODE && + ((node).tagName === 'ARTICLE' || (node).tagName === 'B') + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP + }); + + const firstChild = treeWalker.firstChild(); + + expect(firstChild.tagName).toBe('B'); + expect(firstChild.textContent).toBe('B1'); + + expect(treeWalker.firstChild()).toBe(null); + + const nextSibling = treeWalker.nextSibling(); + + expect(nextSibling.tagName).toBe('ARTICLE'); + expect(nextSibling.textContent.trim()).toBe('B2'); + }); + }); + + describe('lastChild()', () => { + it('Returns last sibling when it matches', () => { + const div = document.createElement('div'); + div.innerHTML = ` + + +
+ Span + B1 +
+
+ B2 +
+
+ `; + const treeWalker = document.createTreeWalker(div, NodeFilter.SHOW_ALL, { + acceptNode: (node: Node) => + node.nodeType === Node.ELEMENT_NODE && + ((node).tagName === 'ARTICLE' || (node).tagName === 'B') + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP + }); + + const lastChild = treeWalker.lastChild(); + + expect(lastChild.tagName).toBe('ARTICLE'); + expect(lastChild.textContent.trim()).toBe('B2'); + + const previousSibling = treeWalker.previousSibling(); + + expect(previousSibling.tagName).toBe('B'); + expect(previousSibling.textContent.trim()).toBe('B1'); + }); }); });