openrosa-xpath-evaluator
Advanced tools
Comparing version 1.5.0 to 2.0.0-beta.0
{ | ||
"name": "openrosa-xpath-evaluator", | ||
"version": "1.5.0", | ||
"version": "2.0.0-beta.0", | ||
"description": "Wrapper for browsers' XPath evaluator with added support for OpenRosa extensions.", | ||
"main": "src/openrosa-xpath.js", | ||
"main": "src/orxe.js", | ||
"scripts": { | ||
"jshint": "jshint src/*.js test/*.js", | ||
"test": "karma start karma.config.js --single-run", | ||
"travis": "npm run jshint && npm test" | ||
"lint": "eslint --ignore-path .gitignore src", | ||
"lint:watch": "esw --ignore-path .gitignore . -w", | ||
"prebuild": "rimraf dist", | ||
"build": "npm run clear && webpack", | ||
"test": "karma start --single-run", | ||
"travis": "npm run lint && npm run test" | ||
}, | ||
"author": "Alex", | ||
"license": "MIT", | ||
"license": "Apache-2.0", | ||
"dependencies": { | ||
"node-forge": "^0.9.0" | ||
}, | ||
"devDependencies": { | ||
"chai": "^3.5.0", | ||
"jshint": "^2.9.5", | ||
"karma": "^1.7.0", | ||
"karma-chai": "^0.1.0", | ||
"karma-chrome-launcher": "^2.2.0", | ||
"karma-firefox-launcher": "^1.0.1", | ||
"karma-mocha": "^0.2.2", | ||
"karma-requirejs": "^0.2.6", | ||
"mocha": "^2.5.3", | ||
"requirejs": "^2.3.4" | ||
"@babel/cli": "^7.5.5", | ||
"@babel/core": "^7.5.5", | ||
"@babel/preset-env": "^7.5.5", | ||
"@babel/register": "^7.5.5", | ||
"babel-loader": "^8.0.6", | ||
"chai": "^4.2.0", | ||
"eslint-watch": "^6.0.0", | ||
"karma": "^4.3.0", | ||
"karma-babel-preprocessor": "^8.0.1", | ||
"karma-chrome-launcher": "^3.1.0", | ||
"karma-coverage": "^2.0.1", | ||
"karma-firefox-launcher": "^1.2.0", | ||
"karma-mocha": "^1.3.0", | ||
"karma-mocha-reporter": "^2.2.5", | ||
"karma-sourcemap-loader": "^0.3.7", | ||
"karma-webpack": "^4.0.2", | ||
"lodash": "^4.17.15", | ||
"mocha": "^6.2.0", | ||
"puppeteer": "^1.19.0", | ||
"terser-webpack-plugin": "^1.4.1", | ||
"webpack": "^4.39.3", | ||
"webpack-cli": "^3.3.7" | ||
} | ||
} |
245
README.md
@@ -14,12 +14,39 @@ Openrosa XForms Evaluator | ||
# TODO | ||
Check that UUID generation is correct (random? UUID vX?) | ||
## Getting Started | ||
# Limitations | ||
1. Include with `npm install openrosa-xpath-evaluator --save` or manually download and add [dist/orxe.min.js](https://raw.github.com/medic/openrosa-xpath-evaluator/master/dist/orxe.min.js) file. | ||
Any expression made requesting a node-type result will be delegated to the underlying xpath evaluator. | ||
2. Include orxe.min.js in the \<head> of your HTML document. | ||
NOTE: Make sure HTML document is in strict mode i.e. it has a !DOCTYPE declaration at the top! | ||
Also, the expression parser is currently very basic and will fail for some xpath expressions. Some examples of expressions that are and are not supported follow. | ||
2. Initialize orxe: | ||
```js | ||
// bind XPath methods to document and window objects | ||
// NOTE: This will overwrite native XPath implementation if it exists | ||
orxe.bindDomLevel3XPath(); | ||
``` | ||
3. You can now use XPath expressions to query the DOM: | ||
```js | ||
var result = document.evaluate( | ||
'//ul/li/text()', // XPath expression | ||
document, // context node | ||
null, // namespace resolver | ||
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE | ||
); | ||
// loop through results | ||
for (var i = 0; i < result.snapshotLength; i++) { | ||
var node = result.snapshotItem(i); | ||
alert(node.nodeValue); | ||
} | ||
``` | ||
# External Libraries | ||
This library does not depend on any external libraries. | ||
But the odk digest function can be supported by installing the node-forge library. | ||
## Supported XPath expressions: | ||
@@ -90,6 +117,214 @@ | ||
* `(x/y)[1]` | ||
* `today() < (today() + 1) | ||
* `today() < '1970-06-03' | ||
* `today() + 1 < '1970-06-03' | ||
* `today() + 1 > '1970-06-03' | ||
* `../some-path='some-value'` | ||
* `'some-value'=../some-path` | ||
* `/simple/xpath/to/node < today() + 1` | ||
* `"aardvark" < "aligator"` | ||
* `self::node()` | ||
* `namespace::node()` | ||
* `child::node()` | ||
* `descendant::node()` | ||
* `descendant-or-self::node()` | ||
* `parent::node()` | ||
* `following-sibling::node()` | ||
* `preceding-sibling::node()` | ||
* `namespace::node()` | ||
* `preceding-sibling::node()` | ||
* `following::node()` | ||
* `preceding::node()` | ||
* `attribute::node()` | ||
* `boolean('a')` | ||
* `boolean('')` | ||
* `boolean(true())` | ||
* `boolean(false())` | ||
* `boolean(1)` | ||
* `boolean(-1)` | ||
* `boolean(1 div 0)` | ||
* `boolean(0.1)` | ||
* `boolean('0.0001')` | ||
* `boolean(0)` | ||
* `boolean(0.0)` | ||
* `boolean(number(''))` | ||
* `boolean(/xhtml:html)` | ||
* `boolean(/asdf)` | ||
* `boolean(//xhtml:article)` | ||
* `boolean(self::node())` | ||
* `ceiling(-1.55)` | ||
* `ceiling(2.44)` | ||
* `ceiling(0.001)` | ||
* `ceiling(1.5)` | ||
* `id('ComparisonOperatorCaseNodesetNegative5to5')/* < * ` | ||
* `lang('en')` | ||
* `lang('EN-us')` | ||
* `attribute::*` | ||
* `namespace::*` | ||
* `child::*` | ||
* `ancestor-or-self::*` | ||
* `namespace::ns2:*` | ||
* `namespace::ns2:ns2` | ||
* `attribute::attrib3` | ||
* `child::node()` | ||
* `child::text()` | ||
* `child::comment()` | ||
* `child::processing-instruction()` | ||
* `child::processing-instruction('custom-process-instruct')` | ||
* `id('FunctionNodesetIdCaseSimple')` | ||
* `last()` | ||
* `xhtml:p[last()]` | ||
* `last(1)` | ||
* `*[position()=last()]` | ||
* `count(xhtml:p)` | ||
* `local-name(namespace::node())` | ||
* `local-name(1, 2)` | ||
* `local-name(1)` | ||
* `namespace-uri(1, 2)` | ||
* `namespace-uri(1)` | ||
* `number(-1.0)` | ||
* `number(1)` | ||
* `number(0.199999)` | ||
* `sum(1, 2)` | ||
* `ceiling(-1.55)` | ||
* `round(-1.55)` | ||
* `sum(self::*)` | ||
* `sum(*)` | ||
* `sum(node())` | ||
* `string('As Df')` | ||
* `string(attribute::node()[1])` | ||
* `string(namespace-uri(/*))` | ||
* `string(namespace::node())` | ||
* `starts-with('a', '')` | ||
* `contains('asdf', 'sd')` | ||
* `substring-before('ab', 'a')` | ||
* `substring-after('aab', 'a')` | ||
* `substring('12345', 2)` | ||
* `string-length('a')` | ||
* `normalize-space(' a')` | ||
* `translate('aabb', 'ab', 'ba')` | ||
* `id('eee40') | id('eee20') | id('eee25') | id('eee10') | id('eee30') | id('eee50')` | ||
* `id('eee40')/attribute::*[1] | id('eee30')` | ||
* `id('nss25')/namespace::*` | ||
* `id('nss40')/namespace::* | id('nss40')/namespace::*` | ||
* `abs(10.5)` | ||
* `area("7.9377 -11.5845 0 0;7.9324 -11.5902 0 0;7.927 -11.5857 0 0;7.925 -11.578 0 0;7.9267 -11.5722 0 0;7.9325 -11.5708 0 0;7.9372 -11.5737 0 0;7.9393 -11.579 0 0;7.9377 -11.5845 0 0")` | ||
* `distance("7.9377 -11.5845 0 0;7.9324 -11.5902 0 0;7.927 -11.5857 0 0;7.925 -11.578 0 0;7.9267 -11.5722 0 0;7.9325 -11.5708 0 0;7.9372 -11.5737 0 0;7.9393 -11.579 0 0;7.9377 -11.5845 0 0")` | ||
* `boolean-from-string('whatever')` | ||
* `checklist(-1, 2, 2>1)` | ||
* `coalesce(/simple/xpath/to/node, "whatever")` | ||
* `coalesce("FIRST", "whatever")` | ||
* `count-non-empty(//xhtml:div[@id="FunctionCountNonEmpty"]/xhtml:div)` | ||
* `count-selected(self::node())` | ||
* `date-time('1970-01-01')` | ||
* `number(date('1970-01-01'))` | ||
* `date('2100-01-02') > now()` | ||
* `today() > ('2012-01-01' + 10)` | ||
* `decimal-date-time("1969-12-31T00:00:00Z")` | ||
* `decimal-date("1969-12-31")` | ||
* `decimal-time("06:60:00.000-07:00")` | ||
* `digest("abc", "MD5", "hex")` | ||
* `ends-with("ba", "a")` | ||
* `false("a")` | ||
* `floor(-1.005)` | ||
* `format-date-time("2001-12-31", "%b %e, %Y")` | ||
* `if(true(), 5, "abc")` | ||
* `if(self::node(), "exists", "does not exist")` | ||
* `int(/simple/xpath/to/node)` | ||
* `int(7.922021953507237e-12)` | ||
* `join(" :: ", //item)` | ||
* `join(" ", "This", "is", "a", "sentence.")` | ||
* `max(/simple/xpath/to/node)` | ||
* `max(self::*)` | ||
* `max(*)` | ||
* `min(/simple/xpath/to/node)` | ||
* `not(true())` | ||
* `not(false())` | ||
* `once("aa")` | ||
* `once(. * 10)` | ||
* `position(..)` | ||
* `position(.)` | ||
* `position(../..)` | ||
* `pow(/simple/xpath/to/node, 0)` | ||
* `pow(2.5, 2)` | ||
* `random()` | ||
* `randomize(//xhtml:div[@id="FunctionRandomize"]/xhtml:div, 'a')` | ||
* `regex(/simple/xpath/to/node, "[0-9]{3}")` | ||
* `round("-50.55", "-2")` | ||
* `selected-at('zero one two three', '4')` | ||
* `selected("apple baby crimson", " baby ")` | ||
* `substr(/simple/xpath/to/node, 5)` | ||
* `sum(*)` | ||
* `sum(self::*)` | ||
* `sum(node())` | ||
* `sum(*)` | ||
* `sum(/root/item)` | ||
* `sin(2)` | ||
* `cos(2)` | ||
* `tan(2)` | ||
* `acos(0.5)` | ||
* `asin(0.5)` | ||
* `atan(0.5)` | ||
* `log(2)` | ||
* `log10("a")` | ||
* `pi()` | ||
* `exp(2)` | ||
* `exp10(2)` | ||
* `sqrt(4)` | ||
* `uuid()` | ||
* `uuid(6)` | ||
* `weighted-checklist(-1, 2, 2>1, 2)` | ||
## Unsupported XPath expressions: | ||
(Add any examples of known-unsupported expressions here and to `test/extended-xpath.spec.js`.) | ||
## Support for custom functions: | ||
To support custom functions, this library can be extended with the following. | ||
``` | ||
orxe.customXPathFunction.add('comment-status', function(a) { | ||
if(arguments.length !== 1) throw new Error('Invalid args'); | ||
const curValue = a.v[0]; // {t: 'arr', v: [{'status': 'good'}]} | ||
const status = JSON.parse(curValue).status; | ||
return new orxe.customXPathFunction.type.StringType(status); | ||
}); | ||
``` | ||
The arguments passed to the custom function (string, number, xpath) will determine the | ||
arguments passed by the library to the function implementation. | ||
The argument format will be any of these: | ||
``` | ||
{t: 'arr', v:[]} | ||
{t: 'num', v:123} | ||
{t: 'str', v:'123'} | ||
``` | ||
The return types currently supported are these: | ||
``` | ||
orxe.customXPathFunction.type.StringType | ||
orxe.customXPathFunction.type.NumberType | ||
orxe.customXPathFunction.type.BooleanType | ||
orxe.customXPathFunction.type.DateType | ||
``` | ||
## Configuration support | ||
The library can be configured with: | ||
``` | ||
orxe.config = { | ||
allowStringComparison: false, | ||
includeTimeForTodayString: false, | ||
returnCurrentTimeForToday: false | ||
}; | ||
``` | ||
#### allowStringComparison (default: false) | ||
This flag allows comparing expressions like this: 'bcd' > 'abc'. | ||
#### includeTimeForTodayString (default: false) | ||
This flag allows the inclusion of time for today() expressions that expect XPathResult.STRING_TYPE. | ||
#### returnCurrentTimeForToday (default: false) | ||
This flag allows time to be considered for today() expressions that expect XPathResult.ANY_TYPE, XPathResult.NUMBER_TYPE, etc. |
@@ -0,1 +1,10 @@ | ||
var config = require('./config'); | ||
var shuffle = require('./utils/shuffle'); | ||
var {isNamespaceExpr, handleNamespaceExpr} = require('./utils/ns'); | ||
var {handleOperation} = require('./utils/operation'); | ||
var {isNativeFunction, preprocessNativeArgs} = require('./utils/native'); | ||
var {DATE_STRING, dateToDays} = require('./utils/date'); | ||
var {toNodes, toSnapshotResult} = require('./utils/result'); | ||
var {inputArgs, preprocessInput} = require('./utils/input'); | ||
var xpr = require('./xpr'); | ||
/* | ||
@@ -16,5 +25,13 @@ * From http://www.w3.org/TR/xpath/#section-Expressions XPath infix | ||
var FUNCTION_NAME = /^[a-z]/; | ||
var NUMERIC_COMPARATOR = /(>|<)/; | ||
var BOOLEAN_COMPARATOR = /(=)/; | ||
var BOOLEAN_FN_COMPARATOR = /(true\(\)|false\(\))/; | ||
var COMPARATOR = /(=|<|>)/; | ||
var INVALID_ARGS = new Error('invalid args'); | ||
var TOO_MANY_ARGS = new Error('too many args'); | ||
var TOO_FEW_ARGS = new Error('too few args'); | ||
// TODO remove all the checks for cur.t==='?' - what else woudl it be? | ||
var ExtendedXpathEvaluator = function(wrapped, extensions) { | ||
var ExtendedXPathEvaluator = function(wrapped, extensions) { | ||
var | ||
@@ -34,3 +51,3 @@ extendedFuncs = extensions.func || {}, | ||
}, | ||
toExternalResult = function(r) { | ||
toExternalResult = function(r, rt) { | ||
if(extendedProcessors.toExternalResult) { | ||
@@ -40,11 +57,65 @@ var res = extendedProcessors.toExternalResult(r); | ||
} | ||
// returns promise | ||
if(r.v && typeof r.v.then === 'function' && rt === XPathResult.STRING_TYPE) { | ||
return {resultType: XPathResult.STRING_TYPE, stringValue: r.v}; | ||
} | ||
if((r.t === 'arr' && rt === XPathResult.NUMBER_TYPE && DATE_STRING.test(r.v[0])) || | ||
(r.t === 'str' && rt === XPathResult.NUMBER_TYPE && DATE_STRING.test(r.v))) { | ||
var val = r.t === 'arr' ? r.v[0] : r.v; | ||
var days = dateToDays(val); | ||
return { | ||
resultType:XPathResult.NUMBER_TYPE, | ||
numberValue:days, | ||
stringValue:days | ||
}; | ||
} | ||
if(r.t === 'num') return { resultType:XPathResult.NUMBER_TYPE, numberValue:r.v, stringValue:r.v.toString() }; | ||
if(r.t === 'bool') return { resultType:XPathResult.BOOLEAN_TYPE, booleanValue:r.v, stringValue:r.v.toString() }; | ||
if(r.t === 'bool')return { resultType:XPathResult.BOOLEAN_TYPE, booleanValue:r.v, stringValue:r.v.toString() }; | ||
if(rt > 3) { | ||
r = shuffle(r[0], r[1]); | ||
return toSnapshotResult(r, XPathResult.UNORDERED_SNAPSHOT_TYPE); | ||
} | ||
if(!r.t && Array.isArray(r)) { | ||
if(rt === XPathResult.NUMBER_TYPE) { | ||
var v = parseInt(r[0].textContent); | ||
return { resultType:XPathResult.NUMBER_TYPE, numberValue:v, stringValue:v.toString() }; | ||
} else if(rt === XPathResult.STRING_TYPE) { | ||
return { resultType:XPathResult.STRING_TYPE, stringValue: r.length ? r[0] : '' }; | ||
} | ||
} | ||
return { resultType:XPathResult.STRING_TYPE, stringValue: r.v===null ? '' : r.v.toString() }; | ||
}, | ||
callFn = function(name, args) { | ||
callFn = function(name, args, rt) { | ||
if(extendedFuncs.hasOwnProperty(name)) { | ||
// if(rt && (/^(date|true|false|now$|today$|randomize$)/.test(name))) args.push(rt); | ||
if(rt && (/^(date|now$|today$|randomize$)/.test(name))) args.push(rt); | ||
if(/^(true$|false$)/.test(name)) args.push(rt || XPathResult.BOOLEAN_TYPE); | ||
return callExtended(name, args); | ||
} | ||
return callNative(name, args); | ||
if(name === 'normalize-space' && args.length) { | ||
var res = args[0].v; | ||
res = res.replace(/\f/g, '\\f'); | ||
res = res.replace(/\r\v/g, '\v'); | ||
res = res.replace(/\v/g, '\\v'); | ||
res = res.replace(/\s+/g, ' '); | ||
res = res.replace(/^\s+|\s+$/g, ''); | ||
res = res.replace(/\\v/g, '\v'); | ||
res = res.replace(/\\f/g, '\f'); | ||
return {t: 'str', v: res}; | ||
} | ||
if(name === 'string' && args.length > 0 && ( | ||
args[0].v === Number.POSITIVE_INFINITY || | ||
args[0].v === Number.NEGATIVE_INFINITY || | ||
args[0].v !== args[0].v )) {//NaN | ||
return { t:'str', v: args[0].v }; | ||
} | ||
return callNative(name, preprocessNativeArgs(name, args)); | ||
}, | ||
@@ -85,5 +156,55 @@ callExtended = function(name, args) { | ||
*/ | ||
this.evaluate = function(input, cN, nR, rT, r) { | ||
if(rT > 3) return wrapped(input, cN, nR, rT, r); // we don't try to handle node expressions | ||
this.evaluate = function(input, cN, nR, rT) { | ||
input = preprocessInput(input, rT); | ||
if(isNamespaceExpr(input)) return handleNamespaceExpr(input, cN); | ||
if(isNativeFunction(input)) { | ||
var args = inputArgs(input); | ||
if(args.length && args[0].length && !isNaN(args[0])) { throw INVALID_ARGS; } | ||
if(input === 'lang()') throw TOO_FEW_ARGS; | ||
if(/^lang\(/.test(input) && cN.nodeType === 2) cN = cN.ownerElement; | ||
const res = wrapped(input, cN); | ||
if(rT === XPathResult.NUMBER_TYPE && | ||
(res.resultType === XPathResult.UNORDERED_NODE_ITERATOR_TYPE || | ||
res.resultType === XPathResult.UNORDERED_NODE_ITERATOR_TYPE)) { | ||
var val = parseInt(res.iterateNext().textContent); | ||
return { | ||
resultType: XPathResult.NUMBER_TYPE, | ||
numberValue: val, | ||
stringValue: val | ||
}; | ||
} | ||
return res; | ||
} | ||
if((rT > 3 && !input.startsWith('randomize')) || | ||
/^count\(|boolean\(/.test(input)) { | ||
if(input.startsWith('count(')) { | ||
if(input.indexOf(',') > 0) throw TOO_MANY_ARGS; | ||
if(input === 'count()') throw TOO_FEW_ARGS; | ||
if(!isNaN(/\((.*)\)/.exec(input)[1])) throw INVALID_ARGS;//firefox | ||
} | ||
if(input.startsWith('boolean(')) { //firefox | ||
if(input === 'boolean()') throw TOO_FEW_ARGS; | ||
var bargs = input.substring(8, input.indexOf(')')).split(','); | ||
if(bargs.length > 1) throw TOO_MANY_ARGS; | ||
} | ||
if(input === '/') cN = cN.ownerDocument || cN; | ||
return wrapped(input, cN); | ||
} | ||
if(rT === XPathResult.BOOLEAN_TYPE && input.indexOf('(') < 0 && | ||
input.indexOf('/') < 0 && input.indexOf('=') < 0 && | ||
input.indexOf('!=') < 0) { | ||
input = input.replace(/(\n|\r|\t)/g, ''); | ||
input = input.replace(/"(\d)"/g, '$1'); | ||
input = input.replace(/'(\d)'/g, '$1'); | ||
input = "boolean-from-string("+input+")"; | ||
} | ||
if(rT === XPathResult.NUMBER_TYPE && input.indexOf('string-length') < 0) { | ||
input = input.replace(/(\n|\r|\t)/g, ''); | ||
} | ||
var i, cur, stack = [{ t:'root', tokens:[] }], | ||
@@ -106,18 +227,3 @@ peek = function() { return stack[stack.length-1]; }, | ||
} | ||
switch(op.v) { | ||
case '+': return lhs.v + rhs.v; | ||
case '-': return lhs.v - rhs.v; | ||
case '*': return lhs.v * rhs.v; | ||
case '/': return lhs.v / rhs.v; | ||
case '%': return lhs.v % rhs.v; | ||
case '=': return lhs.v == rhs.v; | ||
case '<': return lhs.v < rhs.v; | ||
case '>': return lhs.v > rhs.v; | ||
case '<=': return lhs.v <= rhs.v; | ||
case '>=': return lhs.v >= rhs.v; | ||
case '!=': return lhs.v != rhs.v; | ||
case '&': return lhs.v && rhs.v; | ||
case '|': return lhs.v || rhs.v; | ||
} | ||
return handleOperation(lhs, op, rhs, config); | ||
}, | ||
@@ -139,3 +245,2 @@ evalOpAt = function(tokens, opIndex) { | ||
tokens = peek().tokens; | ||
for(j=OP_PRECEDENCE.length-1; j>=0; --j) { | ||
@@ -152,3 +257,18 @@ ops = OP_PRECEDENCE[j]; | ||
handleXpathExpr = function() { | ||
var evaluated = toInternalResult(wrapped(cur.v)); | ||
var expr = cur.v; | ||
var evaluated; | ||
if(['position'].includes(peek().v)) { | ||
evaluated = wrapped(expr); | ||
} else { | ||
if(rT > 3 || (cur.v.indexOf('position()=') >= 0 && | ||
stack.length === 1 && !/^[a-z]*[(|[]{1}/.test(cur.v))) { | ||
evaluated = toNodes(wrapped(expr)); | ||
} else { | ||
if(expr.startsWith('$')) { | ||
evaluated = expr; | ||
} else { | ||
evaluated = toInternalResult(wrapped(expr, cN)); | ||
} | ||
} | ||
} | ||
peek().tokens.push(evaluated); | ||
@@ -190,6 +310,9 @@ newCurrent(); | ||
} | ||
if (cur.t === 'num') { | ||
if(DIGIT.test(c)) { | ||
if(cur.t === 'num') { | ||
if(DIGIT.test(c) || ['e', '"', "'"].includes(c) || | ||
(c === '-' && input[i-1] === 'e')) { | ||
cur.string += c; | ||
continue; | ||
} else if(c === ' ' && cur.string === '-') { | ||
continue; | ||
} else if(c === '.' && !cur.decimal) { | ||
@@ -201,5 +324,5 @@ cur.decimal = 1; | ||
if(isNum(c)) { | ||
if(cur.v === '') { | ||
cur = { t:'num', string:c }; | ||
} else cur.v += c; | ||
if(cur.v === '') { | ||
cur = { t:'num', string:c }; | ||
} else cur.v += c; | ||
} else switch(c) { | ||
@@ -216,3 +339,9 @@ case "'": | ||
stack.push(cur); | ||
if(cur.v === 'once') { | ||
newCurrent(); | ||
cur.v = '.'; | ||
handleXpathExpr(); | ||
} | ||
newCurrent(); | ||
break; | ||
@@ -228,2 +357,3 @@ case ')': | ||
} | ||
if(cur.v !== '') handleXpathExpr(); | ||
@@ -234,3 +364,14 @@ backtrack(); | ||
if(cur.v) { | ||
peek().tokens.push(callFn(cur.v, cur.tokens)); | ||
var expectedReturnType = rT; | ||
if(rT === XPathResult.BOOLEAN_TYPE) { | ||
if(NUMERIC_COMPARATOR.test(input) && !BOOLEAN_FN_COMPARATOR.test(input)) expectedReturnType = XPathResult.NUMBER_TYPE; | ||
if(BOOLEAN_COMPARATOR.test(input)) expectedReturnType = XPathResult.BOOLEAN_TYPE; | ||
if(COMPARATOR.test(input) && cur.t === 'fn' && /^(date|date-time)$/.test(cur.v)) { | ||
expectedReturnType = XPathResult.STRING_TYPE; | ||
} | ||
} | ||
var res = callFn(cur.v, cur.tokens, expectedReturnType); | ||
if(cur.v === 'node' && res.t === 'arr' && res.v.length > 0) | ||
res.v = [res.v[0]]; // only interested in first element | ||
peek().tokens.push(res); | ||
} else { | ||
@@ -249,2 +390,8 @@ if(cur.tokens.length !== 1) err(); | ||
cur.v += c; | ||
if(cur.v === './*') handleXpathExpr(); | ||
} else if(cur.v === '' && | ||
([')', ''].includes(nextChar()) || | ||
input.substring(i+1).trim() === ')')) { | ||
cur.v = c; | ||
handleXpathExpr(); | ||
} else { | ||
@@ -255,9 +402,19 @@ pushOp(c); | ||
case '-': | ||
if(cur.v !== '') { | ||
var prev = prevToken(); | ||
if(cur.v !== '' && nextChar() !== ' ' && input.charAt(i-1) !== ' ') { | ||
// function name expr | ||
cur.v += c; | ||
} else if(peek().tokens.length === 0 || prevToken().t === 'op') { | ||
} else if((peek().tokens.length === 0 && cur.v === '') || | ||
(prev && prev.t === 'op') || | ||
// two argument function | ||
(prev && prev.t === 'num' && stack.length > 1 && stack[1].t === 'fn') || | ||
// negative argument | ||
(prev && prev.t !== 'num' && isNum(nextChar()))) { | ||
// -ve number | ||
cur = { t:'num', string:'-' }; | ||
} else { | ||
if(cur.v !== '') { | ||
if(!DIGIT.test(cur.v) && input[i-1] !== ' ') throw INVALID_ARGS; | ||
peek().tokens.push(cur); | ||
} | ||
pushOp(c); | ||
@@ -304,3 +461,7 @@ } | ||
case 'or': pushOp('|'); break; | ||
default: if(!FUNCTION_NAME.test(cur.v)) handleXpathExpr(); | ||
default: { | ||
var op = cur.v.toLowerCase(); | ||
if(/^(mod|div|and|or)$/.test(op)) throw INVALID_ARGS; | ||
if(!FUNCTION_NAME.test(cur.v)) handleXpathExpr(); | ||
} | ||
} | ||
@@ -311,2 +472,13 @@ break; | ||
/* falls through */ | ||
case '.': | ||
if(cur.v === '' && nextChar() === ')') { | ||
cur.v = c; | ||
handleXpathExpr(); | ||
break; | ||
} | ||
if(cur.v === '' && isNum(nextChar())) { | ||
cur = { t:'num', string:c }; | ||
break; | ||
} | ||
/* falls through */ | ||
default: | ||
@@ -316,7 +488,4 @@ cur.v += c; | ||
} | ||
if(cur.t === 'num') finaliseNum(); | ||
if(cur.t === '?' && cur.v !== '') handleXpathExpr(); | ||
if(cur.t !== '?' || cur.v !== '' || (cur.tokens && cur.tokens.length)) err('Current item not evaluated!'); | ||
@@ -328,11 +497,25 @@ if(stack.length > 1) err('Stuff left on stack.'); | ||
if(stack[0].tokens.length > 1) err('Too many tokens.'); | ||
return toExternalResult(stack[0].tokens[0], rT); | ||
}; | ||
return toExternalResult(stack[0].tokens[0]); | ||
this.customXPathFunction = { | ||
type: { | ||
StringType: xpr.string, | ||
NumberType: xpr.number, | ||
BooleanType: xpr.boolean, | ||
DateType: xpr.date | ||
}, | ||
add: function(name, fnObj) { | ||
extendedFuncs[name] = fnObj; | ||
}, | ||
remove: function(name) { | ||
delete extendedFuncs[name]; | ||
}, | ||
all: function() { | ||
return extendedFuncs; | ||
} | ||
}; | ||
}; | ||
if(typeof define === 'function') { | ||
define(function() { return ExtendedXpathEvaluator; }); | ||
} else if(typeof module === 'object' && typeof module.exports === 'object') { | ||
module.exports = ExtendedXpathEvaluator; | ||
} | ||
module.exports = ExtendedXPathEvaluator; |
@@ -1,320 +0,294 @@ | ||
define(['src/extended-xpath', 'chai', 'lodash'], function(ExtendedXpathEvaluator, chai, _) { | ||
var docs = '', | ||
DATE_MATCH = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d\\d 20\\d\\d \\d\\d:\\d\\d:\\d\\d GMT([+-]\\d\\d\\d\\d \(.+\))?', | ||
examples = { | ||
'false': | ||
/false/, | ||
'true': | ||
/true/, | ||
'"double-string"': | ||
/^double-string$/, | ||
"'single-string'": | ||
/^single-string$/, | ||
'"string(shhh)"': | ||
/^string\(shhh\)$/, | ||
'date()': | ||
new RegExp('^' + DATE_MATCH + '$'), | ||
'upcase("spOtTy")': | ||
/^SPOTTY$/, | ||
'concat("single")': | ||
/^single$/, | ||
'concat("a","b")': | ||
/^ab$/, | ||
'concat("a","b","c")': | ||
/^abc$/, | ||
'concat("a", "b", "c")': | ||
/^abc$/, | ||
'"plus" + "one"': | ||
/^plusone$/, | ||
'"plus" + "one" + "plus" + "two"': | ||
/^plusoneplustwo$/, | ||
'upcase("what") + upcase("ever")': | ||
/^WHATEVER$/, | ||
'downcase("Fox"+"Trot")': | ||
/^foxtrot$/, | ||
'downcase("Fox" + "Trot")': | ||
/^foxtrot$/, | ||
'concat(date())': | ||
new RegExp('^' + DATE_MATCH + '$'), | ||
'concat(date(), "X")': | ||
new RegExp('^' + DATE_MATCH + 'X$'), | ||
'concat("X", date())': | ||
new RegExp('^X' + DATE_MATCH + '$'), | ||
'"Today\'s date: " + date()': | ||
new RegExp('^Today\'s date: ' + DATE_MATCH + '$'), | ||
'concat("Report::", "Today\'s date: " + date())': | ||
new RegExp('^Report::Today\'s date: ' + DATE_MATCH + '$'), | ||
'concat(concat(upcase("Big") + downcase("Little")) + "Average", " by ", concat("Some", " ", "Author"))': | ||
/^BIGlittleAverage by Some Author$/, | ||
'/xpath/expression': | ||
/^<xpath:\/xpath\/expression>$/, | ||
'"string-prefix-" + /xpath/expression': | ||
/^string-prefix-<xpath:\/xpath\/expression>$/, | ||
'/xpath/expression + "-string-suffix"': | ||
/^<xpath:\/xpath\/expression>-string-suffix$/, | ||
'concat("Evaluates to: ", /xpath/expression)': | ||
/^Evaluates to: <xpath:\/xpath\/expression>$/, | ||
'3': | ||
/^3$/, | ||
'3.1416': | ||
/^3.1416$/, | ||
'-3': | ||
/^-3$/, | ||
'-3.1416': | ||
/^-3.1416$/, | ||
'1 + 1': | ||
/^2$/, | ||
'1 - 1': | ||
/^0$/, | ||
'10 div 100': | ||
/^0.1$/, | ||
'random()': | ||
/^(0\.\d+)|(\d\.\d+e-\d)$/, | ||
'random() div 10': | ||
/^(0\.0\d+)|(\d\.\d+e-\d)$/, | ||
'12 mod 5': | ||
/^2$/, | ||
'reverse("hello " + "friend")': | ||
/^dneirf olleh$/, | ||
'reverse("hello ") + reverse("friend")': | ||
/^ ollehdneirf$/, | ||
'native_function()': | ||
/^<xpath:native_function\(\)>$/, | ||
'native_function(3)': | ||
/^<xpath:native_function\(3\)>$/, | ||
'native_function("string-arg")': | ||
/^<xpath:native_function\("string-arg"\)>$/, | ||
'native_function(\'string-with-escaped-"-arg\')': | ||
/^<xpath:native_function\('string-with-escaped-"-arg'\)>$/, | ||
'native_function(1, 2, 3, "a", \'b\', "c")': | ||
/^<xpath:native_function\(1, 2, 3, "a", "b", "c"\)>$/, | ||
'native-function()': | ||
/^<xpath:native-function\(\)>$/, | ||
'native-function(3)': | ||
/^<xpath:native-function\(3\)>$/, | ||
'native-function("string-arg")': | ||
/^<xpath:native-function\("string-arg"\)>$/, | ||
'native-function(1, 2, 3, "a", \'b\', "c")': | ||
/^<xpath:native-function\(1, 2, 3, "a", "b", "c"\)>$/, | ||
'native-function1(native-function2() + native-function3()) + native-function4(native-function5() + native-function6())': | ||
/^<xpath:native-function1\("<xpath:native-function2\(\)><xpath:native-function3\(\)>"\)><xpath:native-function4\("<xpath:native-function5\(\)><xpath:native-function6\(\)>"\)>$/, | ||
'native-function-with-space-before-bracket ()': | ||
/^<xpath:native-function-with-space-before-bracket\(\)>$/, | ||
'3 * 2 + 1': | ||
/^7$/, | ||
'1 + 2 * 3': | ||
/^7$/, | ||
'1 > 0': | ||
/^true$/, | ||
'1 > 1': | ||
/^false$/, | ||
'1 < 1': | ||
/^false$/, | ||
'1 > -1': | ||
/^true$/, | ||
'1 < -1': | ||
/^false$/, | ||
'-1 > 1': | ||
/^false$/, | ||
'-1 < 1': | ||
/^true$/, | ||
'-1 > -1': | ||
/^false$/, | ||
'-1 < -1': | ||
/^false$/, | ||
'-1 < -2': | ||
/^false$/, | ||
const ExtendedXPathEvaluator = require('../src/extended-xpath'); | ||
const assert = chai.assert; | ||
var docs = '', | ||
DATE_MATCH = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d\\d 20\\d\\d \\d\\d:\\d\\d:\\d\\d GMT([+-]\\d\\d\\d\\d \(.+\))?', | ||
examples = { | ||
'false': | ||
/false/, | ||
'true': | ||
/true/, | ||
'"double-string"': | ||
/^double-string$/, | ||
"'single-string'": | ||
/^single-string$/, | ||
'"string(shhh)"': | ||
/^string\(shhh\)$/, | ||
'date()': | ||
new RegExp('^' + DATE_MATCH + '$'), | ||
'upcase("spOtTy")': | ||
/^SPOTTY$/, | ||
'concat("single")': | ||
/^single$/, | ||
'concat("a","b")': | ||
/^ab$/, | ||
'concat("a","b","c")': | ||
/^abc$/, | ||
'concat("a", "b", "c")': | ||
/^abc$/, | ||
'"plus" + "one"': | ||
/^plusone$/, | ||
'"plus" + "one" + "plus" + "two"': | ||
/^plusoneplustwo$/, | ||
'upcase("what") + upcase("ever")': | ||
/^WHATEVER$/, | ||
'downcase("Fox"+"Trot")': | ||
/^foxtrot$/, | ||
'downcase("Fox" + "Trot")': | ||
/^foxtrot$/, | ||
'concat(date())': | ||
new RegExp('^' + DATE_MATCH + '$'), | ||
'concat(date(), "X")': | ||
new RegExp('^' + DATE_MATCH + 'X$'), | ||
'concat("X", date())': | ||
new RegExp('^X' + DATE_MATCH + '$'), | ||
'"Today\'s date: " + date()': | ||
new RegExp('^Today\'s date: ' + DATE_MATCH + '$'), | ||
'concat("Report::", "Today\'s date: " + date())': | ||
new RegExp('^Report::Today\'s date: ' + DATE_MATCH + '$'), | ||
'concat(concat(upcase("Big") + downcase("Little")) + "Average", " by ", concat("Some", " ", "Author"))': | ||
/^BIGlittleAverage by Some Author$/, | ||
'/xpath/expression': | ||
/^<xpath:\/xpath\/expression>$/, | ||
'"string-prefix-" + /xpath/expression': | ||
/^string-prefix-<xpath:\/xpath\/expression>$/, | ||
'/xpath/expression + "-string-suffix"': | ||
/^<xpath:\/xpath\/expression>-string-suffix$/, | ||
'concat("Evaluates to: ", /xpath/expression)': | ||
/^Evaluates to: <xpath:\/xpath\/expression>$/, | ||
'3': | ||
/^3$/, | ||
'3.1416': | ||
/^3.1416$/, | ||
'-3': | ||
/^-3$/, | ||
'-3.1416': | ||
/^-3.1416$/, | ||
'1 + 1': | ||
/^2$/, | ||
'1 - 1': | ||
/^0$/, | ||
'10 div 100': | ||
/^0.1$/, | ||
'random()': | ||
/^(0\.\d+)|(\d\.\d+e-\d)$/, | ||
'random() div 10': | ||
/^(0\.0\d+)|(\d\.\d+e-\d)$/, | ||
'12 mod 5': | ||
/^2$/, | ||
'reverse("hello " + "friend")': | ||
/^dneirf olleh$/, | ||
'reverse("hello ") + reverse("friend")': | ||
/^ ollehdneirf$/, | ||
'native_function()': | ||
/^<xpath:native_function\(\)>$/, | ||
'native_function(3)': | ||
/^<xpath:native_function\(3\)>$/, | ||
'native_function("string-arg")': | ||
/^<xpath:native_function\("string-arg"\)>$/, | ||
'native_function(\'string-with-escaped-"-arg\')': | ||
/^<xpath:native_function\('string-with-escaped-"-arg'\)>$/, | ||
'native_function(1, 2, 3, "a", \'b\', "c")': | ||
/^<xpath:native_function\(1, 2, 3, "a", "b", "c"\)>$/, | ||
'native-function()': | ||
/^<xpath:native-function\(\)>$/, | ||
'native-function(3)': | ||
/^<xpath:native-function\(3\)>$/, | ||
'native-function("string-arg")': | ||
/^<xpath:native-function\("string-arg"\)>$/, | ||
'native-function(1, 2, 3, "a", \'b\', "c")': | ||
/^<xpath:native-function\(1, 2, 3, "a", "b", "c"\)>$/, | ||
'native-function1(native-function2() + native-function3()) + native-function4(native-function5() + native-function6())': | ||
/^<xpath:native-function1\("<xpath:native-function2\(\)><xpath:native-function3\(\)>"\)><xpath:native-function4\("<xpath:native-function5\(\)><xpath:native-function6\(\)>"\)>$/, | ||
'native-function-with-space-before-bracket ()': | ||
/^<xpath:native-function-with-space-before-bracket\(\)>$/, | ||
'3 * 2 + 1': | ||
/^7$/, | ||
'1 + 2 * 3': | ||
/^7$/, | ||
'1 > 0': | ||
/^true$/, | ||
'1 > 1': | ||
/^false$/, | ||
'1 < 1': | ||
/^false$/, | ||
'1 > -1': | ||
/^true$/, | ||
'1 < -1': | ||
/^false$/, | ||
'-1 > 1': | ||
/^false$/, | ||
'-1 < 1': | ||
/^true$/, | ||
'-1 > -1': | ||
/^false$/, | ||
'-1 < -1': | ||
/^false$/, | ||
'-1 < -2': | ||
/^false$/, | ||
}, | ||
trickyStandardXpath_supported = [ | ||
'/model/instance[1]//*', | ||
'/model/instance[1]/*/meta/*', | ||
'./author', | ||
'author', | ||
'first.name', | ||
'/bookstore', | ||
'//author', | ||
'author/first-name', | ||
'bookstore//title', | ||
'bookstore/*/title', | ||
'bookstore//book/excerpt//emph', | ||
'.//title', | ||
'author/*', | ||
'book/*/last-name', | ||
'@style', | ||
'price/@exchange', | ||
'price/@exchange/total', | ||
'book[@style]', | ||
'book/@style', | ||
'./first-name', | ||
'first-name', | ||
'author[1]', | ||
'author[first-name][3]', | ||
'my:book', | ||
'x/y[1]', | ||
'x[1]/y[2]', | ||
'book[excerpt]', | ||
'book[excerpt]/title', | ||
'book[excerpt]/author[degree]', | ||
'book[author/degree]', | ||
'author[degree][award]', | ||
'ancestor::book[1]', | ||
'ancestor::book[author][1]', | ||
'ancestor::author[parent::book][1]', | ||
'../../some-path', | ||
'*/*', | ||
'*[@specialty]', | ||
'@*', | ||
'@my:*', | ||
'my:*', | ||
'author[degree and award]', | ||
'author[(degree or award) and publication]', | ||
'author[degree and not(publication)]', | ||
'author[not(degree or award) and publication]', | ||
'author[. = "Matthew Bob"]', | ||
'author[last-name = "Bob" and ../price > 50]', | ||
'author[not(last-name = "Bob")]', | ||
'author[first-name = "Bob"]', | ||
'author[last-name = "Bob" and first-name = "Joe"]', | ||
'author[* = "Bob"]', | ||
'author[last-name = "Bob"]', | ||
'author[last-name[1] = "Bob"]', | ||
'author[last-name [position()=1]= "Bob"]', | ||
'book[last()]', | ||
'book/author[last()]', | ||
'book[position() <= 3]', | ||
'book[/bookstore/@specialty=@style]', | ||
'degree[position() < 3]', | ||
'degree[@from != "Harvard"]', | ||
'p/text()[2]', | ||
'price[@intl = "Canada"]', | ||
'x/y[position() = 1]', | ||
'(book/author)[last()]', | ||
'(x/y)[1]', | ||
], | ||
trickyStandardXpath_unsupported = [ | ||
], | ||
xp = { | ||
str: function(v) { return { t:'str', v:v }; }, | ||
num: function(v) { return { t:'num', v:v }; }, | ||
}, | ||
_document = function(line) { | ||
docs += line + '\n'; | ||
}, | ||
extendedXPathEvaluator = new ExtendedXPathEvaluator( | ||
function wrappedXpathEvaluator(xpath) { | ||
return { resultType:XPathResult.STRING_TYPE, stringValue:'<xpath:' + xpath + '>' }; | ||
}, | ||
trickyStandardXpath_supported = [ | ||
'/model/instance[1]//*', | ||
'/model/instance[1]/*/meta/*', | ||
'./author', | ||
'author', | ||
'first.name', | ||
'/bookstore', | ||
'//author', | ||
'author/first-name', | ||
'bookstore//title', | ||
'bookstore/*/title', | ||
'bookstore//book/excerpt//emph', | ||
'.//title', | ||
'author/*', | ||
'book/*/last-name', | ||
'@style', | ||
'price/@exchange', | ||
'price/@exchange/total', | ||
'book[@style]', | ||
'book/@style', | ||
'./first-name', | ||
'first-name', | ||
'author[1]', | ||
'author[first-name][3]', | ||
'my:book', | ||
'x/y[1]', | ||
'x[1]/y[2]', | ||
'book[excerpt]', | ||
'book[excerpt]/title', | ||
'book[excerpt]/author[degree]', | ||
'book[author/degree]', | ||
'author[degree][award]', | ||
'ancestor::book[1]', | ||
'ancestor::book[author][1]', | ||
'ancestor::author[parent::book][1]', | ||
'../../some-path', | ||
'*/*', | ||
'*[@specialty]', | ||
'@*', | ||
'@my:*', | ||
'my:*', | ||
'author[degree and award]', | ||
'author[(degree or award) and publication]', | ||
'author[degree and not(publication)]', | ||
'author[not(degree or award) and publication]', | ||
'author[. = "Matthew Bob"]', | ||
'author[last-name = "Bob" and ../price > 50]', | ||
'author[not(last-name = "Bob")]', | ||
'author[first-name = "Bob"]', | ||
'author[last-name = "Bob" and first-name = "Joe"]', | ||
'author[* = "Bob"]', | ||
'author[last-name = "Bob"]', | ||
'author[last-name[1] = "Bob"]', | ||
'author[last-name [position()=1]= "Bob"]', | ||
'book[last()]', | ||
'book/author[last()]', | ||
'book[position() <= 3]', | ||
'book[/bookstore/@specialty=@style]', | ||
'degree[position() < 3]', | ||
'degree[@from != "Harvard"]', | ||
'p/text()[2]', | ||
'price[@intl = "Canada"]', | ||
'x/y[position() = 1]', | ||
'(book/author)[last()]', | ||
'(x/y)[1]', | ||
], | ||
trickyStandardXpath_unsupported = [ | ||
], | ||
xp = { | ||
str: function(v) { return { t:'str', v:v }; }, | ||
num: function(v) { return { t:'num', v:v }; }, | ||
}, | ||
_document = function(line) { | ||
docs += line + '\n'; | ||
}, | ||
extendedXpathEvaluator = new ExtendedXpathEvaluator( | ||
function wrappedXpathEvaluator(xpath) { | ||
return { resultType:XPathResult.STRING_TYPE, stringValue:'<xpath:' + xpath + '>' }; | ||
{ | ||
func: { | ||
upcase: function(it) { return xp.str(it.v.toUpperCase()); }, | ||
downcase: function(it) { return xp.str(it.v.toLowerCase()); }, | ||
date: function() { return xp.str(new Date().toString()); }, | ||
concat: function() { | ||
var i, acc = ''; | ||
for(i=0; i<arguments.length; ++i) acc += arguments[i].v; | ||
return xp.str(acc); | ||
}, | ||
random: function() { return xp.num(Math.random()); }, | ||
reverse: function(it) { return xp.str(it.v.split('').reverse().join('')); }, | ||
}, | ||
{ | ||
func: { | ||
upcase: function(it) { return xp.str(it.v.toUpperCase()); }, | ||
downcase: function(it) { return xp.str(it.v.toLowerCase()); }, | ||
date: function() { return xp.str(new Date().toString()); }, | ||
concat: function() { | ||
var i, acc = ''; | ||
for(i=0; i<arguments.length; ++i) acc += arguments[i].v; | ||
return xp.str(acc); | ||
}, | ||
random: function() { return xp.num(Math.random()); }, | ||
reverse: function(it) { return xp.str(it.v.split('').reverse().join('')); }, | ||
}, | ||
} | ||
); | ||
} | ||
); | ||
describe('ExtendedXpathEvaluator', function() { | ||
describe('ExtendedXpathEvaluator', function() { | ||
describe('should delegate to the wrpaped evaluator when', function() { | ||
_.forIn({ | ||
UNORDERED_NODE_ITERATOR_TYPE: 4, | ||
RDERED_NODE_ITERATOR_TYPE: 5, | ||
UNORDERED_NODE_SNAPSHOT_TYPE: 6, | ||
ORDERED_NODE_SNAPSHOT_TYPE: 7, | ||
ANY_UNORDERED_NODE_TYPE: 8, | ||
FIRST_ORDERED_NODE_TYPE: 9, | ||
}, function(typeVal, typeName) { | ||
it(typeName + ' is requested', function() { | ||
// given | ||
var wrappedCalls = [], | ||
evaluator = new ExtendedXpathEvaluator(function() { | ||
wrappedCalls.push(Array.prototype.slice.call(arguments)); | ||
}, | ||
{ func:{} }), | ||
someExpr = '/a/b/c', someContextNode = {}, someNamespaceResolver = {}, someResult = {}; | ||
// when | ||
evaluator.evaluate(someExpr, someContextNode, someNamespaceResolver, typeVal, someResult); | ||
// given | ||
assert.deepEqual(wrappedCalls, [[someExpr, someContextNode, someNamespaceResolver, typeVal, someResult]]); | ||
}); | ||
}); | ||
_.map(examples, function(expected, expr) { | ||
it(expr + ' should be evaluated', function() { | ||
if(typeof expected === 'string') { | ||
assert.equal( | ||
extendedXPathEvaluator.evaluate(expr).stringValue, | ||
expected); | ||
} else { | ||
assert.match( | ||
extendedXPathEvaluator.evaluate(expr).stringValue, | ||
expected); | ||
} | ||
}); | ||
}); | ||
_.map(examples, function(expected, expr) { | ||
it(expr + ' should be evaluated', function() { | ||
if(typeof expected === 'string') { | ||
assert.equal( | ||
extendedXpathEvaluator.evaluate(expr).stringValue, | ||
expected); | ||
} else { | ||
assert.match( | ||
extendedXpathEvaluator.evaluate(expr).stringValue, | ||
expected); | ||
} | ||
}); | ||
describe('Supported XPath expressions', function() { | ||
it('has a documentation header', function() { | ||
_document('## Supported XPath expressions:'); | ||
_document(''); | ||
}); | ||
describe('Supported XPath expressions', function() { | ||
it('has a documentation header', function() { | ||
_document('## Supported XPath expressions:'); | ||
_document(''); | ||
}); | ||
_.each(trickyStandardXpath_supported, function(expr) { | ||
it(expr + ' should be delegated to the regular XPath evaluator', function() { | ||
_document('* `' + expr + '`'); | ||
_.each(trickyStandardXpath_supported, function(expr) { | ||
it(expr + ' should be delegated to the regular XPath evaluator', function() { | ||
_document('* `' + expr + '`'); | ||
assert.equal( | ||
extendedXpathEvaluator.evaluate(expr).stringValue, | ||
'<xpath:' + expr + '>'); | ||
}); | ||
assert.equal( | ||
extendedXPathEvaluator.evaluate(expr).stringValue, | ||
'<xpath:' + expr + '>'); | ||
}); | ||
}); | ||
it('has a documentation footer', function() { | ||
_document(''); | ||
_document(''); | ||
}); | ||
it('has a documentation footer', function() { | ||
_document(''); | ||
_document(''); | ||
}); | ||
}); | ||
// Documents standard XPath expressions which are not currently supported | ||
describe('Unsupported XPath expressions', function() { | ||
it('has a documentation header', function() { | ||
_document('## Unsupported XPath expressions:'); | ||
_document(''); | ||
}); | ||
// Documents standard XPath expressions which are not currently supported | ||
describe('Unsupported XPath expressions', function() { | ||
it('has a documentation header', function() { | ||
_document('## Unsupported XPath expressions:'); | ||
_document(''); | ||
}); | ||
_.each(trickyStandardXpath_unsupported, function(expr) { | ||
it(expr + ' should not be parsed correctly', function() { | ||
_document('* `' + expr + '`'); | ||
_.each(trickyStandardXpath_unsupported, function(expr) { | ||
it(expr + ' should not be parsed correctly', function() { | ||
_document('* `' + expr + '`'); | ||
// expect | ||
try { | ||
extendedXpathEvaluator.evaluate(expr); | ||
assert.notEqual( | ||
extendedXpathEvaluator.evaluate(expr).stringValue, | ||
'<xpath:' + expr + '>'); | ||
} catch(e) { | ||
if(e.message.indexOf('Too many tokens.') === 0) { | ||
// expected | ||
} else throw e; | ||
} | ||
}); | ||
// expect | ||
try { | ||
extendedXPathEvaluator.evaluate(expr); | ||
assert.notEqual( | ||
extendedXPathEvaluator.evaluate(expr).stringValue, | ||
'<xpath:' + expr + '>'); | ||
} catch(e) { | ||
if(e.message.indexOf('Too many tokens.') === 0) { | ||
// expected | ||
} else throw e; | ||
} | ||
}); | ||
}); | ||
}); | ||
describe('DOCUMENTATION', function() { | ||
it('supports the following:', function() { | ||
console.log('\n' + docs); | ||
}); | ||
describe('DOCUMENTATION', function() { | ||
it('supports the following:', function() { | ||
console.log('\n' + docs); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
616993
99
10428
329
1
22
3
2
+ Addednode-forge@^0.9.0
+ Addednode-forge@0.9.2(transitive)