Skip to content

Commit 0162c24

Browse files
authored
brunerd JSON Path v0.9.17
1 parent aba60a5 commit 0162c24

1 file changed

Lines changed: 188 additions & 12 deletions

File tree

jsonpath.js

Lines changed: 188 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// JSONPath 0.9.16 - XPath for JSON
1+
// JSONPath 0.9.17 - XPath for JSON
22
// Copyright (c) 2021 Joel Bruner (https://github.com/brunerd)
33
// Copyright (c) 2020 "jpaquit" (https://github.com/jpaquit)
44
// Copyright (c) 2007 Stefan Goessner (goessner.net)
@@ -22,8 +22,181 @@ function jsonPath(obj, expr, arg) {
2222
escapeUnicode: arg && arg.escapeUnicode || false,
2323
result: [],
2424
normalize: function(expr) {
25+
//fix non-comparison @ paths in filters like: "?(@.a || @['b'])" rewriting as ?(@.a!==undefined || @.b!==undefined)
26+
//fix negation ! by adding surrounding parens "?(@.a && !@['b'])" rewrites as ?(@.a!==undefined && !(@.b!==undefined))
27+
function fixFilterString(str) {
28+
//turn into an array
29+
str = str.split('');
30+
31+
//a few of the modes we can be in
32+
var mode = {
33+
inDoubleQuote: false,
34+
inSingleQuote: false,
35+
inEscape: false,
36+
inRegexp: false,
37+
//either @ or $
38+
inPath: false,
39+
inKeyName: false,
40+
inBracket: false,
41+
};
42+
43+
//last character of significance for a path name
44+
var lastPathChar
45+
var comparatorOp=false
46+
var negationCount=0
47+
var parenStack=[]
48+
49+
//process string
50+
for (var i=0; i < str.length; i++) {
51+
52+
//if not a keyname break out before we go
53+
if (mode.inKeyName && !/[$_A-Za-z0-9.]/.test(str[i])){
54+
mode.inKeyName=false
55+
}
56+
57+
//ignore anything in quotes ' "
58+
if (mode.inDoubleQuote || mode.inSingleQuote) {
59+
if (mode.inEscape) { mode.inEscape = false }
60+
else if (str[i] === '"' && mode.inDoubleQuote) { mode.inDoubleQuote = false; }
61+
else if (str[i] === "'" && mode.inSingleQuote){ mode.inSingleQuote = false; }
62+
else if (str[i] === '\\' ) { mode.inEscape = true }
63+
}
64+
//ignore whatever is inside a /.../ style regex (JSON Pointer RFC uses I-regex)
65+
else if (mode.inRegexp){
66+
if (mode.inEscape) { mode.inEscape = false }
67+
//forward slash / IS allowed in brackets
68+
else if (str[i] === '[') { mode.inBracket=true }
69+
else if (str[i] === ']') { mode.inBracket=false }
70+
else if (str[i] === '/' && !mode.inBracket) { mode.inRegexp = false }
71+
else if (str[i] === '\\' ) { mode.inEscape = true }
72+
}
73+
//record our last place
74+
else if(mode.inKeyName){
75+
lastPathChar=i
76+
}
77+
//beginning of JSON string in " quote ' quote
78+
else if (str[i] === '"' || str[i] === "'") {
79+
if (str[i] === '"'){ mode.inDoubleQuote = true; }
80+
else { mode.inSingleQuote = true; }
81+
lastPathChar = i
82+
}
83+
//beginning of a absolute or relative path, only dot and index (bracket) selectors allowed
84+
else if (str[i] === '$' || str[i] === '@') {
85+
//set this var
86+
lastPathChar = i
87+
88+
//beginning of dot child key
89+
if (str[i+1] === '.') {
90+
mode.inKeyName=true
91+
lastPathChar=i
92+
}
93+
}
94+
//end of bracket
95+
else if (str[i] === ']') {
96+
lastPathChar=i
97+
mode.inBracket=false
98+
//wow should we test for bracket or . next?
99+
//test the next
100+
if (str[i+1] === "."){
101+
mode.inKeyName=true
102+
i++
103+
}
104+
}
105+
//if we are not in a double quote then this should mean this is a regex
106+
else if (str[i] === '/' ) {
107+
mode.inRegexp = true
108+
}
109+
//if ==, !=, <=, or >=
110+
else if ((str[i] === '=' || str[i] === '!' || str[i] === '<' || str[i] === '>' || str[i] === '=' ) && str[i+1] === '=' ) {
111+
//reset
112+
lastPathChar=undefined
113+
//advance 1 for the equal sign
114+
i=i+1
115+
mode.inPath=false
116+
comparatorOp=true
117+
}
118+
//or just < or >
119+
else if (str[i] === '<' || str[i] === '>') {
120+
//reset
121+
lastPathChar=undefined
122+
mode.inPath=false
123+
comparatorOp=true
124+
}
125+
//or regex =~
126+
else if (str[i] === '=' && str[i+1] === '~' ) {
127+
//reset
128+
lastPathChar=undefined
129+
mode.inPath=false
130+
comparatorOp=true
131+
132+
//advance 1
133+
i=i+1
134+
}
135+
//booleans: &&, ||
136+
else if ((str[i] === '&' && str[i+1] === '&') || (str[i] === '|' && str[i+1] === '|' )) {
137+
//if this wasn't reset then it never had a comparison applied
138+
if (comparatorOp != true && lastPathChar !== undefined) {
139+
//insert !== undefined into array after lastPathChar
140+
str.splice((lastPathChar+1),0,"!==undefined")
141+
i++
142+
}
143+
mode.inPath=false
144+
comparatorOp=false
145+
146+
//close out negation !( with )
147+
if(negationCount){
148+
for (var loop=0; loop<negationCount; negationCount--){
149+
str.splice(i,0,")")
150+
i++
151+
}
152+
}
153+
}
154+
else if(str[i] === '!'){
155+
//insert (
156+
str.splice(i+1,0,"(")
157+
negationCount++
158+
i++
159+
}
160+
//a paren beginning
161+
else if(str[i] === '('){
162+
//store value in stack
163+
parenStack.unshift(negationCount)
164+
//reset count
165+
negationCount=0
166+
}
167+
//a paren beginning
168+
else if(str[i] === ')'){
169+
//close out
170+
if(negationCount){
171+
for (var loop=0; loop<negationCount; negationCount--){
172+
str.splice(i+1,0,")")
173+
i++
174+
}
175+
}
176+
//get any previous values in stack
177+
negationCount=parenStack.shift()
178+
}
25179

26-
//work on strings only, pass through all others (like a pre-objectified path array)
180+
//end of for loop for str -2 as we currently expect parens surrounding all
181+
if(i == (str.length-2)){
182+
if(comparatorOp != true && lastPathChar !== undefined) {
183+
//insert at the end
184+
str.splice((lastPathChar+1),0,"!==undefined")
185+
i++
186+
}
187+
//insert remaining ) at the end
188+
if(negationCount){
189+
//insert (
190+
for (var loop=0; loop<negationCount; negationCount--){
191+
str.splice(i+1,0,")")
192+
i++
193+
}
194+
}
195+
}
196+
}
197+
var finalString = str.join('')
198+
return finalString
199+
} //work on strings only, pass through all others (like a pre-objectified path array)
27200
if (expr.constructor === null || expr.constructor !== String) { return expr }
28201

29202
var pathStack=[]
@@ -118,6 +291,7 @@ function jsonPath(obj, expr, arg) {
118291
pendingData.unshift(Number(L2Match[4]))
119292
}
120293
}
294+
//WOW - this should really not be allowed, only quoted ["-"] or ['-']
121295
//(-) - from JSON Pointer, represents the index AFTER the last one, always non-existent
122296
else if(L2Match[5]){
123297
if (needsDelimiter) { Level2Regex.lastIndex=subLastLastIndex; break; } else { needsDelimiter=true }
@@ -178,8 +352,8 @@ function jsonPath(obj, expr, arg) {
178352
//keep working on revExpr
179353
L3Match = Level3Regex.exec(revExpr)
180354

181-
//(["'])(.*?)\1(?!\\) - quoted string
182-
//escape @ for substitution in P.eval
355+
//" or ' - quoted string (["'])(.*?)\1(?!\\)
356+
//escape @ in strings for substitution in P.eval
183357
if(L3Match[1]) {
184358
filterText+=L3Match[0].replace(/@/g, "@\\")
185359
}
@@ -206,17 +380,19 @@ function jsonPath(obj, expr, arg) {
206380
}
207381
//(.) - any other character
208382
else if(L3Match[9]) {
209-
//if this is a = assignment (not != <= >=) break, =~ regex is matched earlier
383+
//if this is a = assignment (not != <= >=) break, == and =~ regex is matched earlier
210384
if(L3Match[9] === "=" && !/[<>!]/.test(revExpr[Level3Regex.lastIndex])) {
211385
break
212386
}
213387
filterText+=L3Match[9]
214388
}
215389

390+
//currently assuming filter expression is always in parens (pre-IETF draft)
391+
//if they are even break
216392
if (closeParens === openParens){
217393
needsDelimiter=true
218394

219-
//before we break check if the next char is a question mark and if so include that
395+
//check if the next char is a filter expression question mark (e.g. $[?(<expr>)])
220396
if(revExpr[Level3Regex.lastIndex] === '?'){
221397
if(isSlice){
222398
Level3Regex.lastIndex=0
@@ -229,9 +405,8 @@ function jsonPath(obj, expr, arg) {
229405
//set our Level2Regex index to where we were in this
230406
Level2Regex.lastIndex = Level3Regex.lastIndex
231407
//reverse back to normal and store this in the array of items
232-
var filterTextFinal = filterText.split('').reverse().join('')
408+
var filterTextFinal = fixFilterString(filterText.split('').reverse().join(''))
233409
pendingData.unshift({"expression":filterTextFinal})
234-
235410
break;
236411
}
237412
} while (Level3Regex.lastIndex !== 0 && Level3Regex.lastIndex !== revExpr.length)
@@ -378,10 +553,10 @@ function jsonPath(obj, expr, arg) {
378553
P.trace(tx, val, path);
379554
}
380555
}
381-
//?(expr) - a filter expression, this tests an expression and if true will descend into that key or return it's value
382-
else if (/^\?\(.*?\)$/.test(loc.expression)){
556+
//? - a filter expression, this tests an expression and if true will descend into that key or return it's value
557+
else if (/^\?/.test(loc.expression)){
383558
P.walk(loc.expression, x, val, path, function(m,l,x,v,p) {
384-
if (P.eval(l.replace(/^\?\((.*?)\)$/,"$1"), v instanceof Array ? v[m] : v, m) !== undefined) {
559+
if (P.eval(l.replace(/^\?/,""), v instanceof Array ? v[m] : v, m)) {
385560
var tx = x.slice(); tx.unshift(m); P.trace(tx,v,p);
386561
}
387562
});
@@ -457,8 +632,9 @@ function jsonPath(obj, expr, arg) {
457632

458633
var tx = x.slice()
459634

635+
//WOW this should not be needed, parens have no effect leave them if present
460636
//if this is a () expression, strip the outer parens
461-
if ((/^\(.*?\)$/).test(x)) { tx = tx.replace((/^\((.*?)\)$/),"$1") }
637+
//if ((/^\(.*?\)$/).test(x)) { tx = tx.replace((/^\((.*?)\)$/),"$1") }
462638

463639
//remove all all data between "" '' and //, split by semi-colon
464640
//remove all spaces before ( and collapse multiple spaces down to a single space

0 commit comments

Comments
 (0)