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?
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?
Share Improve this question edited Sep 9, 2015 at 15:42 Ryan Grush asked Sep 8, 2015 at 21:57 Ryan GrushRyan Grush 2,1384 gold badges41 silver badges66 bronze badges 6-
You can use
innerHTML
to search for strings withRegExp
and wrap them with elements – PitaJ Commented Sep 8, 2015 at 21:59 - 1 Check out this answer for using XPath to select nodes containing certain text. – PitaJ Commented Sep 8, 2015 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 ♦ Commented Sep 8, 2015 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 Commented Sep 8, 2015 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 plex 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 ♦ Commented Sep 9, 2015 at 0:20
2 Answers
Reset to default 9If 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 remended to anybody trying to work with XPath and document.evaluate()
.
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...