6

I'm writing a Chrome extension that will search the DOM and highlight all email addresses on the page. I found this to look for at symbols on the page but it only returns correctly when there is one email address, it breaks when there are multiple addresses found.

found = document.evaluate('//*[contains(text(),"@")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);

What is the correct way to have this return multiples if more than one is found?

Ryan Grush
  • 2,076
  • 3
  • 37
  • 64
  • You can use `innerHTML` to search for strings with `RegExp` and wrap them with elements – PitaJ Sep 08 '15 at 21:59
  • 1
    Check out [this answer](http://stackoverflow.com/questions/12997691/select-nodes-from-an-xml-that-contains-certain-string-in-one-of-its-attributes) for using XPath to select nodes containing certain text. – PitaJ Sep 08 '15 at 22:08
  • 1
    @PitaJ The question and answer at that page are good, but that’s about attribute nodes, and the OP seems to want to get the *element* nodes containing an `@` character. In which case, the XPath expression in the question is fine. – sideshowbarker Sep 08 '15 at 22:32
  • @sideshowbarker yeah, I thought it was relevant at least. His XPath works for me to select every node with that text. He just needs to while loop through the list (incrementing the zero at the very end) – PitaJ Sep 08 '15 at 22:34
  • 1
    @RyanGrush do be careful about how you use `document.evaluate()` and XPath—because it’s a bit of a performance fun-gun, in that it allows you to construct very sophisticated queries, but at the risk of those queries possibly being extremely costly to evaluate in terms of CPU cycles (vs CSS Selectors which by design are limited to not letting you do those kinds of complex queries, due to the performance hit). So in those cases, you’d want to use it async; e.g., from a worker—so that it doesn’t bring your entire UI to its knees while the query is getting evaluated. – sideshowbarker Sep 09 '15 at 00:20
  • @sideshowbarker I was worried about it being costly. So are you suggesting I send it to a server to process the content and then back to the extension? I'd love to use CSS selectors but obviously I wouldn't know what to look for from the extension. – Ryan Grush Sep 09 '15 at 15:43

2 Answers2

8

If you want to handle multiple results, don’t call .snapshotItem(0) on document.evaluate() but instead loop through the results using a for loop and snapshotLength():

Example: Loop through results using snapshotLength() with snapshotItem()

var nodesSnapshot = document.evaluate('//*[contains(text(),"@")]',
    document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null );

for ( var i=0 ; i < nodesSnapshot.snapshotLength; i++ )
{
  console.dir( nodesSnapshot.snapshotItem(i) );
}

Either that, or specify the XPathResult.UNORDERED_NODE_ITERATOR_TYPE argument (instead of XPathResult.ORDERED_NODE_SNAPSHOT_TYPE), and use a while loop with iterateNext():

Example: Iterate over results using iterateNext()

var iterator = document.evaluate('//*[contains(text(),"@")]',
    document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );

try {
  var thisNode = iterator.iterateNext(); 
  while (thisNode) {
    console.dir( thisNode );
    thisNode = iterator.iterateNext();
  } 
}
catch (e) {
  console.log( 'Error: Document tree modified during iteration ' + e );
}

In cases that are sorta the reverse of the one in this question—cases when you really do want just get the first matching node—you can specify the XPathResult.FIRST_ORDERED_NODE_TYPE value, to return just a single node, and then use the property (not method) singleNodeValue:

Example: Use XPathResult.FIRST_ORDERED_NODE_TYPE and singleNodeValue

var firstMatchingNode = document.evaluate('// [contains(text(),"@")]',
    document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null );

console.dir( firstMatchingNode.singleNodeValue );

Getting text or counts back instead, or testing true/false conditions

Note that among the other values (constants) you can specify as the second-to-last argument to document.evaluate() to get other results types, you can make it directly return:

  • a single string (XPathResult.STRING_TYPE) slurped from some part of the document
  • a number representing a count of some kind (XPathResult.NUMBER_TYPE); for example, a count of the number of e-mail addresses found in the document
  • a boolean value (XPathResult.BOOLEAN_TYPE) representing some true/false aspect of the document; e.g., an indicator whether or not the document contains any e-mail addresses

Of course to get those other result types back, the XPath expression you give as the first argument to document.evaluate() needs to be an expression that will actually return a string, or a number, or a boolean value (instead of returning a set of attribute nodes or element nodes).


More at MDN

The examples above are all based on the MDN Introduction to using XPath in JavaScript tutorial, which is highly recommended to anybody trying to work with XPath and document.evaluate().

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
  • 1
    I guess due to being architected in a bygone era by and for people coding DOM stuff in Java or whatever, document.evaluate and the rest of the DOM3 XPath API are a weirdly designed bizarro-superman API that’s not consistent with other APIs for the Web platform. But once you’ve gotten past the quirkiness of it and are familiar with it, it gets the job done. – sideshowbarker Sep 08 '15 at 23:19
0

Through the code below, you can have your XPath selector results as an array.

const xpath = `//*[contains(text(),"@")]`;//your special XPath
const elements = Array.from((function*(){ let iterator = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); let current = iterator.iterateNext(); while(current){ yield current; current = iterator.iterateNext(); }  })());
//Use the simple array

Also, you can have it as a function, for more calls...

function getElementsByXPath(xpath) {
    return Array.from((function*(){ let iterator = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); let current = iterator.iterateNext(); while(current){ yield current; current = iterator.iterateNext(); }  })());
}

Enjoy...

MiMFa
  • 981
  • 11
  • 14