openrosa-xpath-evaluator
Advanced tools
Comparing version 2.0.1 to 2.0.2
@@ -6,2 +6,10 @@ Change Log | ||
[2.0.2] - 2021-01-18 | ||
------------------------ | ||
##### Changed | ||
- The uuid() function implementation has improved with a reduced chance of collisions. | ||
##### Fixed | ||
- Nested expressions with dead branches cause an exception. | ||
[2.0.1] - 2021-01-07 | ||
@@ -8,0 +16,0 @@ ------------------------ |
{ | ||
"name": "openrosa-xpath-evaluator", | ||
"version": "2.0.1", | ||
"version": "2.0.2", | ||
"description": "Wrapper for browsers' XPath evaluator with added support for OpenRosa extensions.", | ||
@@ -30,3 +30,3 @@ "homepage": "https://enketo.org", | ||
"chai": "^4.2.0", | ||
"eslint": "^7.17.0", | ||
"eslint": "^7.18.0", | ||
"karma": "^5.2.3", | ||
@@ -41,3 +41,3 @@ "karma-chrome-launcher": "^3.1.0", | ||
"puppeteer": "^5.5.0", | ||
"webpack": "^4.39.3" | ||
"webpack": "^4.46.0" | ||
}, | ||
@@ -44,0 +44,0 @@ "peerDependencies": { |
@@ -27,23 +27,23 @@ Openrosa XForms Evaluator | ||
```js | ||
const orxe = require('openrosa-xpath-evaluator'); | ||
const evaluate = orxe(); | ||
``` | ||
```js | ||
const orxe = require('openrosa-xpath-evaluator'); | ||
const evaluate = orxe(); | ||
``` | ||
## Querying | ||
```js | ||
var result = evaluate( | ||
'//ul/li/text()', // XPath expression | ||
document, // context node | ||
null, // namespace resolver | ||
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE | ||
); | ||
```js | ||
var result = 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); | ||
} | ||
``` | ||
// loop through results | ||
for (var i = 0; i < result.snapshotLength; i++) { | ||
var node = result.snapshotItem(i); | ||
alert(node.nodeValue); | ||
} | ||
``` | ||
@@ -50,0 +50,0 @@ |
@@ -5,3 +5,4 @@ { | ||
"browser": true, | ||
"commonjs": true | ||
"commonjs": true, | ||
"es6": true | ||
}, | ||
@@ -8,0 +9,0 @@ "parserOptions": { |
@@ -124,8 +124,18 @@ const { handleOperation } = require('./utils/operation'); | ||
pushOp = function(t) { | ||
const peeked = peek(); | ||
const { tokens } = peeked; | ||
let prev; | ||
if(t <= AND) { | ||
evalOps(t); | ||
const prev = asBoolean(prevToken()); | ||
if(t === OR ? prev : !prev) peek().dead = true; | ||
prev = asBoolean(tokens[tokens.length-1]); | ||
if((t === OR ? prev : !prev) && peeked.t !== 'fn') peeked.dead = true; | ||
} | ||
peek().tokens.push({ t:'op', v:t }); | ||
tokens.push({ t:'op', v:t }); | ||
if(t <= AND) { | ||
if(t === OR ? prev : !prev) tokens.push(D); | ||
} | ||
newCurrent(); | ||
@@ -193,11 +203,9 @@ }, | ||
if(peek().dead) { | ||
if(tokens[2] === D) { | ||
const nextComma = tokens.indexOf(','); | ||
tokens.splice(0, nextComma === -1 ? tokens.length : nextComma, { t:'bool', v:asBoolean(tokens[0]) }); | ||
} | ||
if(tokens.length < 2) return; | ||
if(tokens[2] === D && tokens[1].v >= lastOp) { | ||
const endExpr = tokens.indexOf(',', 2); | ||
tokens.splice(0, endExpr === -1 ? tokens.length : endExpr, { t:'bool', v:asBoolean(tokens[0]) }); | ||
} | ||
if(tokens.length < 2) return; | ||
for(let j=UNION; j>=lastOp; j-=0b100) { | ||
@@ -216,3 +224,2 @@ let i = 1; | ||
if(peek().dead) { | ||
peek().tokens.push(D); | ||
newCurrent(); | ||
@@ -252,2 +259,13 @@ return; | ||
}, | ||
isDeadFnArg = () => { | ||
const peeked = peek(); | ||
if(peeked.t === 'fn') { | ||
const { tokens } = peeked; | ||
for(let i=tokens.length-1; i>=0 && tokens[i] !== ','; --i) { | ||
if(tokens[i] === D) { | ||
return true; | ||
} | ||
} | ||
} | ||
}, | ||
isNum = function(c) { | ||
@@ -287,7 +305,7 @@ return c >= '0' && c <= '9'; | ||
const head = peek(); | ||
if(head.dead) { | ||
const { tokens } = head; | ||
if(head.dead || tokens[2] === D) { | ||
newCurrent(); | ||
continue; | ||
} | ||
const { tokens } = head; | ||
let contextNodes; | ||
@@ -369,2 +387,4 @@ if(tokens.length && tokens[tokens.length-1].t === 'arr') { | ||
peek().tokens.push(D); | ||
} else if(isDeadFnArg()) { | ||
/* do nothing */ | ||
} else if(cur.v) { | ||
@@ -371,0 +391,0 @@ peek().tokens.push(callFn(cur.v, cur.tokens)); |
@@ -30,11 +30,2 @@ require('./date-extensions'); | ||
}, | ||
_uuid_part = function(c) { | ||
const r = Math.random()*16|0, | ||
v = c == 'x' ? r : r&0x3|0x8; | ||
return v.toString(16); | ||
}, | ||
uuid = function() { | ||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' | ||
.replace(/[xy]/g, _uuid_part); | ||
}, | ||
format_date = function(date, format) { | ||
@@ -280,3 +271,2 @@ date = asDate(date); | ||
// I think we should just be able to return: XPR.string(asString(r || this.cN).replace(/[\t\r\n ]+/g, ' ').trim()); | ||
// TODO check XPath 3.0 spec for normalize-space()? https://www.w3.org/TR/xpath-functions-30/#func-normalize-space | ||
if(arguments.length > 1) throw new Error('too many args'); | ||
@@ -633,1 +623,12 @@ | ||
} | ||
/** | ||
* Implementation from https://stackoverflow.com/a/2117523, added in revision | ||
* https://stackoverflow.com/revisions/2117523/11, licensed under CC by SA 3.0 | ||
* (https://creativecommons.org/licenses/by-sa/3.0/), see | ||
* https://stackoverflow.com/posts/2117523/timeline. Formatting may have been | ||
* changed. | ||
*/ | ||
function uuid() { | ||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); | ||
} |
@@ -16,3 +16,3 @@ const { initDoc, assertStringValue } = require('../helpers'); | ||
describe('should evaluate node', () => { | ||
describe('with node evaluation', () => { | ||
const doc = initDoc(` | ||
@@ -58,3 +58,3 @@ <div id="FunctionChecklistCase"> | ||
}); | ||
it(`should evaluate an "or" expression that checks values of nodes (3)`, () => { | ||
@@ -69,4 +69,76 @@ const node = doc.getElementById('FunctionChecklistCase0'); | ||
}); | ||
}); | ||
describe('should deal with nesting and lengthy or/and clauses (with booleans)', ()=> { | ||
[ | ||
[ 'if( false() and true(), "A", if(false(), "B", "C") )', | ||
'C' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", "C") )', | ||
'C' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", true() or false()) )', | ||
'true' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", false() or true()) )', | ||
'true' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", false() and true()) )', | ||
'false' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", true() and false()) )', | ||
'false' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", false() and false()) )', | ||
'false' ], | ||
[ 'if( false() and explode-a(), "A", if(false() and explode-b(), "B", true() and true()) )', | ||
'true' ], | ||
[ 'if( false() and explode-a(), "A", if(true() or explode-b(), false() and explode-c(), true() or explode-d()) )', | ||
'false' ], | ||
[ 'if( true() or true() and false(), "A", if(true() or true() and false(), true() or true() and false(), "B") )', | ||
'A' ], | ||
[ 'if( true() or true() and false(), "A", if(true() or true() and false(), true() or true() and false(), true() or true() and explode-d()) )', | ||
'A' ], | ||
[ 'if( true() or true() and false(), "A", if(true() or true() and false(), true() or true() and explode-c(), true() or true() and explode-d()) )', | ||
'A' ], | ||
[ 'if( true() or true() and false(), "A", if(true() or true() and explode-b(), true() or true() and explode-c(), true() or true() and explode-d()) )', | ||
'A' ], | ||
[ 'if( true() or true() and explode-a(), "A", if(true() or true() and explode-b(), true() or true() and explode-c(), true() or true() and explode-d()) )', | ||
'A' ], | ||
].forEach(([ expr, expected ]) => { | ||
it(`should evaluate "${expr}" as "${expected}"`, () => { | ||
assertStringValue(expr, expected); | ||
}); | ||
}); | ||
}); | ||
describe('should deal with nesting and lengthy or/and clauses (with derived values)', ()=> { | ||
const doc = initDoc(` | ||
<data> | ||
<a/> | ||
<b/> | ||
<c>1</c> | ||
<d>0</d> | ||
</data>`); | ||
// TODO: this is a lazy test taken directly from a real form. It probably should be removed the minimal test cases below it seem to be sufficient, but it will be helpful during bug fixing. None of these nodes exist in the doc. | ||
it(`long sequence of "and" clauses and nested if() with long sequence of "or clauses" (non-minimized test case)`, () => { | ||
assertStringValue(doc, null, 'if( /model/instance[1]/data/page-welcome/GRP_ELIG/AGE_IC ="1" and /model/instance[1]/data/page-welcome/GRP_ELIG/INC_TEMP ="1" and /model/instance[1]/data/page-welcome/GRP_ELIG/NO_SEV_ILLNESS ="1" and /model/instance[1]/data/page-welcome/GRP_ELIG/FU_POSSIBLE ="1" and /model/instance[1]/data/page-welcome/GRP_ELIG/SAMPLE_COL_POSSIBLE ="1" and /model/instance[1]/data/page-welcome/GRP_ELIG/PROVIDE_INFORM_CONSENT ="1" and /model/instance[1]/data/page-welcome/GRP_ELIG/FEVER_RESP ="1", "Eligible", if( /model/instance[1]/data/page-welcome/GRP_ELIG/AGE_IC ="0" or /model/instance[1]/data/page-welcome/GRP_ELIG/INC_TEMP ="0" or /model/instance[1]/data/page-welcome/GRP_ELIG/NO_SEV_ILLNESS ="0" or /model/instance[1]/data/page-welcome/GRP_ELIG/FU_POSSIBLE ="0" or /model/instance[1]/data/page-welcome/GRP_ELIG/SAMPLE_COL_POSSIBLE ="0" or /model/instance[1]/data/page-welcome/GRP_ELIG/PROVIDE_INFORM_CONSENT ="0" or /model/instance[1]/data/page-welcome/GRP_ELIG/FEVER_RESP ="0", "Not-Eligible", "nothing"))', 'nothing'); | ||
}); | ||
it(`sequence of "and" clauses and nested if() with sequence of "or" clauses (1)`, () => { | ||
assertStringValue(doc, null, 'if( /data/a ="1" and /data/b ="1", "Eligible", if( /data/a ="0" or /data/b ="0", "Not-Eligible", "nothing"))', 'nothing'); | ||
}); | ||
it(`sequence of "and" clauses and nested if() with sequence of "or" clauses (2)`, () => { | ||
assertStringValue(doc, null, 'if( /data/a ="1" and /data/c ="1", "Eligible", if( /data/a ="0" or /data/b ="0", "Not-Eligible", "nothing"))', 'nothing'); | ||
}); | ||
it(`sequence of "and" clauses and nested if() with sequence of "or" clauses (3)`, () => { | ||
assertStringValue(doc, null, 'if( /data/c ="1" and /data/b ="1", "Eligible", if( /data/a ="0" or /data/b ="0", "Not-Eligible", "nothing"))', 'nothing'); | ||
}); | ||
it(`sequence of "and" clauses and nested if() with sequence of "or" clauses (4)`, () => { | ||
assertStringValue(doc, null, 'if( /data/a ="1" and /data/b ="1", "Eligible", if( /data/a ="0" or /data/d ="0", "Not-Eligible", "nothing"))', 'Not-Eligible'); | ||
}); | ||
it(`sequence of "and" clauses and nested if() with sequence of "or" clauses (5)`, () => { | ||
assertStringValue(doc, null, 'if( /data/a ="1" and /data/b ="1", "Eligible", if( /data/d ="0" or /data/b ="0", "Not-Eligible", "nothing"))', 'Not-Eligible'); | ||
}); | ||
}); | ||
}); |
@@ -179,2 +179,8 @@ // TODO this can be moved to test/unit | ||
false, | ||
'1 or 0 and /explode': | ||
true, | ||
'1 and 0 or 0 or 1 and 1 or 0 or 0 or 0 and /explode or /explode or /explode and /explode': | ||
true, | ||
'1 and 0 or 0 or 1 and 1 or 0 or 0 or 0 and ([/explode]) or /explode or /explode and /explode': | ||
true, | ||
'0 and concat(/explode)': | ||
@@ -244,3 +250,3 @@ false, | ||
_.map(examples, function(expected, expr) { | ||
it(expr + ' should be evaluated', function() { | ||
it(`${expr} should evaluate to ${expected}`, function() { | ||
switch(typeof expected) { | ||
@@ -247,0 +253,0 @@ case 'boolean': return assert.equal(extendedXPathEvaluator.evaluate(expr).booleanValue, expected); |
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
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
360006
8697