Skip to content

Instantly share code, notes, and snippets.

@James-E-A
Last active June 26, 2025 19:56
Show Gist options
  • Save James-E-A/093d3835cdaa991e6fad0e360dd9e6c6 to your computer and use it in GitHub Desktop.
Save James-E-A/093d3835cdaa991e6fad0e360dd9e6c6 to your computer and use it in GitHub Desktop.
Get element(s) by xpath
/**
* Get an element by XPath.
* Includes workaround for browser bug not allowing SVG elements to be selected; use the virtual "_" prefix to reference the context node's namespace.
* @param {string} expression - A string representing the XPath to be evaluated.
* @param {Node|Document} [contextNode] - The context node.
* @param {XPathEvaluator|Document} [document] - The document via which evaluate() will be called. Defaults to the context node's document, if specified; otherwise globalThis.document. Required if context node and globalThis.document are both absent.
* @param {boolean} [ordered=true] - Whether to return the "first" node selected by the expression. If false, allows the XPath engine to return an arbitrary node selected by the expression.
* @returns {?Node}
*/
export function getElementByXPath(expression, contextNode, document, ordered=true) {
if (document === undefined) document = (contextNode ? contextNode.getRootNode() : globalThis.document); // TypeError if bad parameters
if (contextNode === undefined) contextNode = document;
var result = document.evaluate(
expression,
contextNode,
(prefix) => (prefix === '_' ? contextNode.namespaceURI : document.lookupNamespaceURI(prefix)),
ordered ? XPathResult.FIRST_ORDERED_NODE_TYPE : XPathResult.ANY_UNORDERED_NODE_TYPE
);
return result.singleNodeValue || null;
}
/**
* Get multiple elements by XPath.
* Includes workaround for browser bug not allowing SVG elements to be selected; use the virtual "_" prefix to reference the context node's namespace.
* @param {string} expression - A string representing the XPath to be evaluated.
* @param {Node|Document} [contextNode] - The context node.
* @param {XPathEvaluator|Document} [document] - The document via which evaluate() will be called. Defaults to the context node's document, if specified; otherwise globalThis.document. Required if context node and globalThis.document are both absent.
* @param {boolean} [ordered=true] - Whether to return the nodes in strict order. If false, allows the XPath engine to return the elements in arbitrary order.
* @param {boolean} [snapshot=true] - If true, immediately return an Array containing all selected elements; if false, return a "lazy" Iterator yielding all selected elements.
* @returns {Array.<Node>|Iterable.<Node>}
*/
export function getElementsByXPath(expression, contextNode, document, ordered=true, snapshot=true) {
if (document === undefined) document = (contextNode ? contextNode.getRootNode() : globalThis.document); // TypeError if bad parameters
if (contextNode === undefined) contextNode = document;
var result = document.evaluate(
expression,
contextNode,
(prefix) => (prefix === '_' ? contextNode.namespaceURI : document.lookupNamespaceURI(prefix)),
snapshot ? (ordered ? XPathResult.ORDERED_NODE_SNAPSHOT_TYPE : XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE)
: (ordered ? XPathResult.ORDERED_NODE_ITERATOR_TYPE : XPathResult.UNORDERED_NODE_ITERATOR_TYPE)
);
return xpathresultToNativeType(result);
}
/**
* @param {string} expression
* @param {Node|Document} [contextNode]
* @param {XPathEvaluator|Document} [document]
* @param {number} [resultType]
* @returns {?Node|Array.<Node>|Iterable.<Node>|?number|?string|?boolean}
*/
export function getByXPath(expression, contextNode, document, resultType=XPathResult.ANY_TYPE) {
if (document === undefined) document = (contextNode ? contextNode.getRootNode() : globalThis.document); // TypeError if bad parameters
if (contextNode === undefined) contextNode = document;
var result = document.evaluate(
expression,
contextNode,
(prefix) => (prefix === '_' ? contextNode.namespaceURI : document.lookupNamespaceURI(prefix)),
resultType
);
return xpathresultToNativeType(result);
}
/**
* Convert any XPathResult object into its corresponding Javascript object.
* @param {XPathResult} result
* @returns {?Node|Array.<Node>|Iterable.<Node>|?number|?string|?boolean}
*/
export function xpathresultToNativeType(result) {
switch (result.resultType) {
case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
// XPath Snapshot, as JS Array
return Array.from(
// TODO benchmark further
{ length: result.snapshotLength },
(_0, i) => result.snapshotItem(i)
);
case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
// XPath Iterator, as JS Generator
return (function* (xp_it) {
// TODO benchmark
for (
var value = xp_it.iterateNext() ;
value !== null ;
value = xp_it.iterateNext()
) {
yield value;
}
})(result);
case XPathResult.ANY_UNORDERED_NODE_TYPE:
case XPathResult.FIRST_ORDERED_NODE_TYPE:
// Single node value
return result.singleNodeValue || null;
case XPathResult.NUMBER_TYPE:
return result.numberValue ?? null;
case XPathResult.STRING_TYPE:
return result.stringValue ?? null;
case XPathResult.BOOLEAN_TYPE:
return result.booleanValue ?? null;
default:
throw new DOMException(`Unsupported XPathResult type: ${result.resultType}`, 'NotSupportedError');
}
}
@James-E-A
Copy link
Author

TODO maybe benchmark the billion different ways one could call Array.from

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment