diff --git a/test.js b/test.js index e05babc..4fe9aaf 100644 --- a/test.js +++ b/test.js @@ -1301,6 +1301,43 @@ describe('xpath', () => { }); }); + describe('error handling', () => { + it('should reject unspecified expression', () => { + for (let expr of [null, undefined, '']) { + assert.throws(() => xpath.parse(expr), { + message: 'XPath expression unspecified.', + }); + } + }); + + it('should reject non-string expression', () => { + for (let expr of [{}, []]) { + assert.throws(() => xpath.parse(expr), { + message: 'XPath expression must be a string.', + }); + } + }); + it('should reject non-nodes', () => { + for (let node of ['', 0, 45, true, false, [], {}]) { + assert.throws(() => xpath.parse('/*').select({ node }), { + message: 'Context node does not appear to be a valid DOM node.', + }); + } + }); + + it('should handle unspecified nodes', () => { + assert.throws( + () => xpath.parse('my:field').select(), { + message: 'Context node not found when evaluating XPath step: child::my:field', + }); + + assert.throws( + () => xpath.parse('/*').select(), { + message: 'Context node not found when determining document root.', + }); + }) + }); + describe('Node type tests', () => { it('should correctly identify a Node of type Element', () => { var doc = parseXml(''); diff --git a/xpath.js b/xpath.js index eaef6ff..c3b298f 100644 --- a/xpath.js +++ b/xpath.js @@ -101,6 +101,28 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; (function (exports) { "use strict"; + // namespace nodes are not part of the DOM spec, so we use a custom nodetype for them. + // should NOT be used externally + var NAMESPACE_NODE_NODETYPE = '__namespace'; + + var isNil = function (x) { + return x === null || x === undefined; + }; + + var isValidNodeType = function (nodeType) { + return nodeType === NAMESPACE_NODE_NODETYPE || + (Number.isInteger(nodeType) + && nodeType >= 1 + && nodeType <= 11 + ); + }; + + var isNodeLike = function (value) { + return value + && isValidNodeType(value.nodeType) + && typeof value.nodeName === "string"; + }; + // functional helpers function curry(func) { var slice = Array.prototype.slice, @@ -167,7 +189,7 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; var prototypeConcat = Array.prototype.concat; - var sortNodes = function(nodes, reverse) { + var sortNodes = function (nodes, reverse) { var ns = new XNodeSet(); ns.addArray(nodes); @@ -221,7 +243,7 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, - NAMESPACE_NODE: '__namespace', // not part of DOM model + NAMESPACE_NODE: NAMESPACE_NODE_NODETYPE, }; // XPathParser /////////////////////////////////////////////////////////////// @@ -1246,6 +1268,13 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; XPathParser.ACCEPT = 'a'; XPathParser.prototype.parse = function (s) { + if (!s) { + throw new Error('XPath expression unspecified.'); + } + if (typeof s !== 'string'){ + throw new Error('XPath expression must be a string.'); + } + var types; var values; var res = this.tokenize(s); @@ -1324,6 +1353,12 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; } XPath.prototype.evaluate = function (c) { + var node = c.expressionContextNode; + + if (!(isNil(node) || isNodeLike(node))) { + throw new Error("Context node does not appear to be a valid DOM node."); + } + c.contextNode = c.expressionContextNode; c.contextSize = 1; c.contextPosition = 1; @@ -1823,7 +1858,9 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; PathExpr.getRoot = function (xpc, nodes) { var firstNode = nodes[0]; - if (firstNode.nodeType === NodeTypes.DOCUMENT_NODE) { + // xpc.virtualRoot could possibly provide a root even if firstNode is null, + // so using a guard here instead of throwing. + if (firstNode && firstNode.nodeType === NodeTypes.DOCUMENT_NODE) { return firstNode; } @@ -1831,6 +1868,10 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; return xpc.virtualRoot; } + if (!firstNode) { + throw new Error('Context node not found when determining document root.'); + } + var ownerDoc = firstNode.ownerDocument; if (ownerDoc) { @@ -1860,7 +1901,10 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; }; PathExpr.applyStep = function (step, xpc, node) { - var self = this; + if (!node) { + throw new Error('Context node not found when evaluating XPath step: ' + step); + } + var newNodes = []; xpc.contextNode = node; @@ -3149,7 +3193,7 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; } // xml namespace node comes before others. namespace nodes before non-namespace nodes - if (n1.isXPathNamespace){ + if (n1.isXPathNamespace) { if (n1.nodeValue === XPath.XML_NAMESPACE_URI) { return -1; } @@ -4490,6 +4534,7 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; this.context.caseInsensitive = XPathExpression.detectHtmlDom(n); var result = this.xpath.evaluate(this.context); + return new XPathResult(result, t); } @@ -4900,21 +4945,21 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; DivOperation: DivOperation, ModOperation: ModOperation, UnaryMinusOperation: UnaryMinusOperation, - + FunctionCall: FunctionCall, VariableReference: VariableReference, - + XPathContext: XPathContext, - + XNodeSet: XNodeSet, XBoolean: XBoolean, XString: XString, XNumber: XNumber, - + NamespaceResolver: NamespaceResolver, FunctionResolver: FunctionResolver, VariableResolver: VariableResolver, - + Utilities: Utilities, } ); @@ -4966,15 +5011,6 @@ var xpath = (typeof exports === 'undefined') ? {} : exports; return exports.select(e, doc, true); }; - var isNodeLike = function (value) { - return value - && typeof value.nodeType === "number" - && Number.isInteger(value.nodeType) - && value.nodeType >= 1 - && value.nodeType <= 11 - && typeof value.nodeName === "string"; - }; - var isArrayOfNodes = function (value) { return Array.isArray(value) && value.every(isNodeLike); };