9

When I use querySelectorAll, I can find 138 td nodes in my sample document.

Array.from(document.querySelectorAll('td')).length
138

When I do the same with XPath, I get no result:

Array.from(document.evaluate(".//td", document.body, null, XPathResult.ANY_TYPE, null)).length
0

Although there is at least one match:

document.evaluate(".//td", document.body, null, XPathResult.ANY_TYPE, null).iterateNext().nodeName
"TD"

The problem seems to be that Array.from can not iterate over a XPathResult. Even this returns 0:

Array.from(document.evaluate('.', document.body, null, XPathResult.ANY_TYPE, null)).length
0

How to make a XPathResult suitable for Array.from?

ceving
  • 21,900
  • 13
  • 104
  • 178

3 Answers3

15

Unfortunately you can't. Array.from can convert two types of objects into arrays:

  1. Those that are "array-like" that have a .length property.
  2. Those that implement the iterator protocol and allow you to get all of their elements.

XPathResult doesn't do any of these. You could do this by manually iterating over the result and storing the results in an array such as:

const nodes = [];
let node = xPathResult.iterateNext();
while (node) {
  nodes.push(node);
  node = xPathResult.iterateNext();
}

...but if you're going to loop over the nodes anyway, you can probably do whatever array operations you wanted to do in the loop.

Explosion Pills
  • 188,624
  • 52
  • 326
  • 405
  • 1
    To avoid duplication and (imo) make it clearer, you could instead do `const nodes = []; let node; while (node = xPathResult.iterateNext()) {nodes.push(node);}`. Too bad you can't just say `while (const node = xPathResult.iterateNext()) {...}` since the value isn't used outside the loop. – Chinoto Vokro Jun 27 '20 at 19:02
5

Building up on the answer from @JamesTheAwesomeDude, you can use Array.from (or the spread operator) if you polyfill the iterator onto XPathResult. This iterator is just a bit better because it can operate on all types of XPathResult:

if (!XPathResult.prototype[Symbol.iterator]) XPathResult.prototype[Symbol.iterator] = function* () {
    switch (this.resultType) {
        case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
        case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
            let result;
            while ( (result = this.iterateNext()) != null ) yield result;
            break;
        case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
        case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
            for (let i=0; i < this.snapshotLength; i++) yield this.snapshotItem(i);
            break;
        default:
            yield this.singleNodeValue;
            break;
    }
};
Sérgio Carvalho
  • 1,135
  • 1
  • 8
  • 8
1

As the existing answer states, this is not "supported" per se, because (for some bizarre reason) XPathResult doesn't support JS' standard iteration protocols at all.

You could always DIY it, I suppose; this would, naturally, work with your use-case, Array.from:

function xpr2iter (xpr) {
  // Produce a JavaScript iterator for an *ITERATOR_TYPE XPathResult
  // or a JavaScript iterable for a *SNAPSHOT_TYPE XPathResult
  switch (xpr.resultType) {
    case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
    case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
      return {
        [Symbol.iterator] () {
          var i = 0;
          return {
            next() {
              var node = xpr.snapshotItem(i++);
              return {value: node, done: !node};
            }
          };
        },
        at(i) {
          return xpr.snapshotItem(i) || undefined;
        }
      };
    case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
    case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
      return {
        next() {
          var node = xpr.iterateNext();
          return {value: node, done: !node};
        },
        [Symbol.iterator] () {
          return this;
        },
      };
  }
}


// As an example, pull the top child elements
// -- should just be the <head> and <body>:
let example1_xpr = document.evaluate('/html/*', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
// -- render into an Array:
let example1_arr = Array.from(xpr2iter(example1_xpr));

// Can be rendered to an array!
console.log("length:", example1_arr.length);
console.log("map demo (e => e.tagName):", example1_arr.map(e => e.tagName));

...however, it also supports for...of directly without instantiating a whole Array:

function xpr2iter (xpr) {
  // Produce a JavaScript iterator for an *ITERATOR_TYPE XPathResult
  // or a JavaScript iterable for a *SNAPSHOT_TYPE XPathResult
  switch (xpr.resultType) {
    case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
    case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
      return {
        [Symbol.iterator] () {
          var i = 0;
          return {
            next() {
              var node = xpr.snapshotItem(i++);
              return {value: node, done: !node};
            }
          };
        },
        at(i) {
          return xpr.snapshotItem(i) || undefined;
        }
      };
    case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
    case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
      return {
        next() {
          var node = xpr.iterateNext();
          return {value: node, done: !node};
        },
        [Symbol.iterator] () {
          return this;
        },
      };
  }
}

let example2_xpr = document.evaluate('/html/*', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);

for( let e of xpr2iter(example2_xpr) ) {
  // It's iterable!
  console.log(e.tagName);
}

Therefore, this solution is slightly more general (and it should also use slightly less memory when iterating over obscenely large amounts of nodes without instantiating an Array).


I can find 138 td nodes in my sample document.

In fact, juuust in case this is an A/B problem and you're only trying to find the number of matches, that can be got directly by the .snapshotLength property of non-iterator types (or you can "manually" count an iterator type, if you prefer):

function xpr2iter (xpr) {
  // Produce a JavaScript iterator for an *ITERATOR_TYPE XPathResult
  // or a JavaScript iterable for a *SNAPSHOT_TYPE XPathResult
  switch (xpr.resultType) {
    case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
    case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
      return {
        [Symbol.iterator] () {
          var i = 0;
          return {
            next() {
              var node = xpr.snapshotItem(i++);
              return {value: node, done: !node};
            }
          };
        },
        at(i) {
          return xpr.snapshotItem(i) || undefined;
        }
      };
    case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
    case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
      return {
        next() {
          var node = xpr.iterateNext();
          return {value: node, done: !node};
        },
        [Symbol.iterator] () {
          return this;
        },
      };
  }
}

let example3_xpr = document.evaluate('/html/*', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);

let example3_n = example3_xpr.snapshotLength;

console.log("number of matches:", example3_n);


let example4_xpr = document.evaluate('/html/*', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);

let example4_n = 0;
for( let e of xpr2iter(example4_xpr) )
  example4_n ++;

console.log("number of matches:", example4_n);