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 - Z a - z 0 - 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