Skip to content

Commit

Permalink
Add a TreeWalker implementation
Browse files Browse the repository at this point in the history
Closes half of jsdom#317.
  • Loading branch information
garycourt authored and nhunzaker committed Dec 22, 2016
1 parent 2d7ce48 commit ca3b6c2
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 6 deletions.
14 changes: 14 additions & 0 deletions lib/jsdom/living/nodes/Document-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const DOMImplementation = require("../generated/DOMImplementation");
const ParentNodeImpl = require("./ParentNode-impl").implementation;
const HTMLElement = require("../generated/HTMLElement");
const HTMLUnknownElement = require("../generated/HTMLUnknownElement");
const TreeWalker = require("../generated/TreeWalker");

function clearChildNodes(node) {
for (let child = domSymbolTree.firstChild(node); child; child = domSymbolTree.firstChild(node)) {
Expand Down Expand Up @@ -676,6 +677,19 @@ class DocumentImpl extends NodeImpl {
});
}

// TODO: Add callback interface support to `webidl2js`
createTreeWalker(root, whatToShow, filter) {
if (!isNodeImpl(root)) {
throw new TypeError("First argument to createTreeWalker must be a Node");
}

return TreeWalker.createImpl([], {
root,
whatToShow,
filter
});
}

importNode(node, deep) {
if (!isNodeImpl(node)) {
throw new TypeError("First argument to importNode must be a Node");
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/nodes/Document.idl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ interface Document : Node {

// NodeFilter.SHOW_ALL = 0xFFFFFFFF
// [NewObject] NodeIterator createNodeIterator(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null); // implemented in node-iterator.js; TODO move to Document-impl.js
// [NewObject] TreeWalker createTreeWalker(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
[NewObject] TreeWalker createTreeWalker(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
};

[OverrideBuiltins]
Expand Down
257 changes: 257 additions & 0 deletions lib/jsdom/living/traversal/TreeWalker-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"use strict";

const DOMException = require("../../web-idl/DOMException");
const idlUtils = require("../generated/utils");
const conversions = require("webidl-conversions");

//FIXME: Once NodeFilter is ported to IDL method, uncomment these.
const FILTER_ACCEPT = 1; //NodeFilter.FILTER_ACCEPT;
const FILTER_REJECT = 2; //NodeFilter.FILTER_REJECT;
const FILTER_SKIP = 3; //NodeFilter.FILTER_SKIP;
const FIRST = false;
const LAST = true;
const NEXT = false;
const PREVIOUS = true;

function isNull(o) {
return o === null || typeof o === "undefined";
}

class TreeWalkerImpl {
constructor(args, privateData) {
this.root = privateData.root;
this.whatToShow = privateData.whatToShow;
this.filter = privateData.filter;
this.currentNode = this.root;
};

get currentNode() {
return this._currentNode;
};

set currentNode(node) {
if (isNull(node)) {
throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot set currentNode to null");
}

this._currentNode = node;
};

parentNode() {
let node = this._currentNode;
while (!isNull(node) && node !== this.root) {
node = node.parentNode;

if (!isNull(node) && this._filterNode(node) === FILTER_ACCEPT) {
return (this._currentNode = node);
}
}
return null;
};

firstChild() {
return this._traverseChildren(FIRST);
};

lastChild() {
return this._traverseChildren(LAST);
};

previousSibling() {
return this._traverseSiblings(PREVIOUS);
};

nextSibling() {
return this._traverseSiblings(NEXT);
};

previousNode() {
let node = this._currentNode;

while (node !== this.root) {
let sibling = node.previousSibling;

while (!isNull(sibling)) {
node = sibling;
let result = this._filterNode(node);

while (result !== FILTER_REJECT && node.hasChildNodes()) {
node = node.lastChild;
result = this._filterNode(node);
}

if (result === FILTER_ACCEPT) {
return (this._currentNode = node);
}

sibling = node.previousSibling;
}

if (node === this.root || isNull(node.parentNode)) {
return null;
}

node = node.parentNode;

if (this._filterNode(node) === FILTER_ACCEPT) {
return (this._currentNode = node);
}
}

return null;
};

nextNode() {
let node = this._currentNode;
let result = FILTER_ACCEPT;

while (true) {
while (result !== FILTER_REJECT && node.hasChildNodes()) {
node = node.firstChild;
result = this._filterNode(node);
if (result === FILTER_ACCEPT) {
return (this._currentNode = node);
}
}

do {
if (node === this.root) {
return null;
}

const sibling = node.nextSibling;

if (!isNull(sibling)) {
node = sibling;
break;
}

node = node.parentNode;
} while (!isNull(node));

if (isNull(node)) {
return null;
}

result = this._filterNode(node);

if (result === FILTER_ACCEPT) {
return (this._currentNode = node);
}
}
};

toString() {
return "[object TreeWalker]";
};

_filterNode(node) {
const n = node.nodeType - 1;

if (!((1 << n) & this.whatToShow)) {
return FILTER_SKIP;
}

const filter = this.filter;

if (isNull(filter)) {
return FILTER_ACCEPT;
}

let result;

if (typeof filter === "function") {
result = filter(idlUtils.wrapperForImpl(node));
} else {
result = filter.acceptNode(idlUtils.wrapperForImpl(node));
}

result = conversions["unsigned short"](result);

return result;
};

_traverseChildren(type) {
let node = this._currentNode;
node = type === FIRST ? node.firstChild : node.lastChild;

if (isNull(node)) {
return null;
}

main: while (true) {
const result = this._filterNode(node);

if (result === FILTER_ACCEPT) {
return (this._currentNode = node);
}

if (result === FILTER_SKIP) {
const child = type === FIRST ? node.firstChild : node.lastChild;

if (!isNull(child)) {
node = child;
continue main;
}
}

while (true) {
const sibling = type === FIRST ? node.nextSibling : node.previousSibling;

if (!isNull(sibling)) {
node = sibling;
continue main;
}

const parent = node.parentNode;

if (isNull(parent) || parent === this.root || parent === this._currentNode) {
return null;
}

node = parent;
}
}
};

_traverseSiblings(type) {
let node = this._currentNode;

if (node === this.root) {
return null;
}

while (true) {
let sibling = type === NEXT ? node.nextSibling : node.previousSibling;

while (!isNull(sibling)) {
node = sibling;
const result = this._filterNode(node);

if (result === FILTER_ACCEPT) {
return (this._currentNode = node);
}

sibling = type === NEXT ? node.firstChild : node.lastChild;

if (result === FILTER_REJECT || isNull(sibling)) {
sibling = type === NEXT ? node.nextSibling : node.previousSibling;
}
}

node = node.parentNode;

if (isNull(node) || node === this.root) {
return null;
}

if (this._filterNode(node) === FILTER_ACCEPT) {
return null;
}
}
};
}

module.exports = {
implementation: TreeWalkerImpl
};
15 changes: 15 additions & 0 deletions lib/jsdom/living/traversal/TreeWalker.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Exposed=Window]
interface TreeWalker {
[SameObject] readonly attribute Node root;
readonly attribute unsigned long whatToShow;
readonly attribute NodeFilter? filter;
attribute Node currentNode;

Node? parentNode();
Node? firstChild();
Node? lastChild();
Node? previousSibling();
Node? nextSibling();
Node? previousNode();
Node? nextNode();
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"st": "^0.5.5",
"watchify": "^3.7.0",
"wd": "0.3.12",
"webidl2js": "^4.2.0"
"webidl2js": "^4.3.0"
},
"browser": {
"canvas": false,
Expand Down
10 changes: 6 additions & 4 deletions scripts/webidl/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ function onlyIDL(filePath) {
return path.extname(filePath) === ".idl";
}

doConversion(path.resolve(__dirname, "../../lib/jsdom/living/events")).done();
doConversion(path.resolve(__dirname, "../../lib/jsdom/living/attributes")).done();
doConversion(path.resolve(__dirname, "../../lib/jsdom/living/window")).done();
doConversion(path.resolve(__dirname, "../../lib/jsdom/living/nodes")).done();
doConversion(path.resolve(__dirname, "../../lib/jsdom/living/traversal"))
.then(() => doConversion(path.resolve(__dirname, "../../lib/jsdom/living/events")))
.then(() => doConversion(path.resolve(__dirname, "../../lib/jsdom/living/attributes")))
.then(() => doConversion(path.resolve(__dirname, "../../lib/jsdom/living/window")))
.then(() => doConversion(path.resolve(__dirname, "../../lib/jsdom/living/nodes")))
.done();
10 changes: 10 additions & 0 deletions test/web-platform-tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ describe("Web Platform Tests", () => {
"dom/nodes/Node-cloneNode.html",
"dom/traversal/NodeFilter-constants.html",
"dom/traversal/NodeIterator.html",
"dom/traversal/TreeWalker-acceptNode-filter.html",
"dom/traversal/TreeWalker-basic.html",
"dom/traversal/TreeWalker-currentNode.html",
"dom/traversal/TreeWalker-previousNodeLastChildReject.html",
"dom/traversal/TreeWalker-previousSiblingLastChildSkip.html",
"dom/traversal/TreeWalker-traversal-reject.html",
"dom/traversal/TreeWalker-traversal-skip-most.html",
"dom/traversal/TreeWalker-traversal-skip.html",
"dom/traversal/TreeWalker-walking-outside-a-tree.html",
"dom/traversal/TreeWalker.html",
"domparsing/insert-adjacent.html",
"html/browsers/browsing-the-web/history-traversal/PopStateEvent.html",
"html/browsers/browsing-the-web/history-traversal/hashchange_event.html",
Expand Down

0 comments on commit ca3b6c2

Please sign in to comment.