2424 * Implement the parsing of uBO-flavored JSON path queries.
2525 *
2626 * Reference to original JSON path syntax:
27- * https://goessner.net/articles/JsonPath/index.html
27+ * https://www.rfc-editor.org/info/rfc9535/
2828 *
2929 * uBO-flavored JSON path implementation differs as follow:
30+ * - Array slice operator not supported
31+ * - Selectors can be a `/`-delimited regex, i.e. `$./pattern/`
32+ * - Onle a single filter selector is allowed in filter selector syntax
3033 *
31- * - Both $ and @ are implicit. Though you can use them, you do not have to.
32- * Use $ only when the implicit context is not that of root. Example:
33- * - Official: $..book[?(@.isbn)]
34- * - uBO-flavored: ..book[?(.isbn)]
35- *
36- * - uBO-flavor syntax does not (yet) support:
37- * - Array slice operator
38- *
39- * - Regarding filter expressions, uBO-flavored JSON path supports a limited
34+ * - uBO-flavored JSON path supports a limited set of filter selectors:
4035 * set of expressions since unlike the official implementation, uBO can't use
4136 * JS eval() to evaluate arbitrary JS expressions. The operand MUST be valid
4237 * JSON. The currently supported expressions are:
5045 * - $=: stringified value ends with
5146 * - *=: stringified value includes
5247 * - =/.../: true if the regular expression matches the stringified value
48+ *
49+ * - uBO-flavored JSON path supports assignement operator:
50+ * - =[any well-formed JSON value]
5351 * - =repl(...): [to be documented] i.e. {"regex":"...","flags":"i","replacement":"..."}
5452 * - =call(...): [to be documented] i.e. ["${obj}","setAttribute","${key}"]
5553 * *
@@ -87,6 +85,10 @@ export class JSONPath {
8785 return ( stringifier || JSON . stringify ) ( obj , ...args )
8886 . replace ( / \/ / g, '\\/' ) ;
8987 }
88+ static keys = Object . keys ;
89+ static entries = Object . entries ;
90+ static hasOwn = Object . hasOwn ;
91+ static Regex = RegExp ;
9092 get value ( ) {
9193 return this . #compiled && this . #compiled. rval ;
9294 }
@@ -163,7 +165,7 @@ export class JSONPath {
163165 #CHILDREN = 3 ;
164166 #DESCENDANTS = 4 ;
165167 #reUnquotedIdentifier = / ^ [ A - Z a - z _ ] [ \w ] * | ^ \* / ;
166- #reExpr = / ^ ( [ ! = ^ $ * ] = | [ < > ] = ? ) ( .+ ?) \] / ;
168+ #reExpr = / ^ \s * ( [ ! = ^ $ * ] = | [ < > ] = ? ) \s * ( .+ ?) \] / ;
167169 #reIndice = / ^ - ? \d + / ;
168170 #reRval = / ^ = ( [ a - z ] + ) \( ( .+ ) \) $ / ;
169171 #root;
@@ -205,20 +207,21 @@ export class JSONPath {
205207 if ( mv === this . #UNDEFINED ) {
206208 const step = steps . at ( - 1 ) ;
207209 if ( step === undefined ) { return ; }
208- i = this . #compileExpr( query , step , i ) ;
210+ const j = this . #compileExpr( query , step , i ) ;
211+ if ( j ) { i = j ; }
209212 break ;
210213 }
211- const s = this . #consumeUnquotedIdentifier( query , i ) ;
212- if ( s === undefined ) { return ; }
213- steps . push ( { mv, k : s } ) ;
214- i += s . length ;
214+ const r = this . #consumeUnquotedIdentifier( query , i ) ;
215+ if ( r === undefined ) { return ; }
216+ steps . push ( { mv, k : r . s } ) ;
217+ i = r . i ;
215218 mv = this . #UNDEFINED;
216219 continue ;
217220 }
218221 // Bracket accessor syntax
219222 if ( query . startsWith ( '[?' , i ) ) {
220- const not = query . charCodeAt ( i + 2 ) === 0x21 /* ! */ ;
221- const j = i + 2 + ( not ? 1 : 0 ) ;
223+ const not = query . charCodeAt ( i + 2 ) === 0x21 /* ! */ ? 1 : 0 ;
224+ const j = i + 2 + not ;
222225 const r = this . #compile( query , j ) ;
223226 if ( r === undefined ) { return ; }
224227 if ( query . startsWith ( ']' , r . i ) === false ) { return ; }
@@ -255,12 +258,19 @@ export class JSONPath {
255258 resultset = [ [ '$' ] ] ;
256259 break ;
257260 case this . #CURRENT:
261+ if ( step . op ) {
262+ const { obj, key } = this . #resolvePath( pathin ) ;
263+ const outcome = this . #evaluateExpr( step , obj , key ) ;
264+ if ( outcome !== true ) { break ; }
265+ }
258266 resultset = [ pathin ] ;
259267 break ;
260268 case this . #CHILDREN:
261- case this . #DESCENDANTS:
269+ case this . #DESCENDANTS: {
270+ if ( resultset . length === 0 ) { break ; }
262271 resultset = this . #getMatches( resultset , step ) ;
263272 break ;
273+ }
264274 default :
265275 break ;
266276 }
@@ -271,50 +281,66 @@ export class JSONPath {
271281 const listout = [ ] ;
272282 for ( const pathin of listin ) {
273283 const { value : owner } = this . #resolvePath( pathin ) ;
274- if ( step . k === '*' ) {
275- this . #getMatchesFromAll( pathin , step , owner , listout ) ;
276- } else if ( step . k !== undefined ) {
277- this . #getMatchesFromKeys( pathin , step , owner , listout ) ;
278- } else if ( step . steps ) {
284+ if ( step . steps ) {
279285 this . #getMatchesFromExpr( pathin , step , owner , listout ) ;
286+ continue ;
287+ }
288+ const iter = this . #expandKey( owner , step . k ) ;
289+ if ( iter ) {
290+ for ( const k of iter ) {
291+ const outcome = this . #evaluateExpr( step , owner , k ) ;
292+ if ( outcome !== true ) { continue ; }
293+ listout . push ( [ ...pathin , k ] ) ;
294+ }
295+ }
296+ if ( step . mv !== this . #DESCENDANTS ) { continue ; }
297+ for ( const { obj, key, path } of this . #getDescendants( owner , true ) ) {
298+ const iter = this . #expandKey( obj [ key ] , step . k ) ;
299+ if ( iter === undefined ) { continue ; }
300+ for ( const k of iter ) {
301+ const outcome = this . #evaluateExpr( step , obj [ key ] , k ) ;
302+ if ( outcome !== true ) { continue ; }
303+ listout . push ( [ ...pathin , ...path , k ] ) ;
304+ }
280305 }
281306 }
282307 return listout ;
283308 }
284- #getMatchesFromAll( pathin , step , owner , out ) {
285- const recursive = step . mv === this . #DESCENDANTS;
286- for ( const { path } of this . #getDescendants( owner , recursive ) ) {
287- out . push ( [ ...pathin , ...path ] ) ;
309+ #expandKey( owner , k ) {
310+ if ( typeof owner !== 'object' ) { return ; }
311+ if ( Array . isArray ( k ) ) {
312+ const out = [ ] ;
313+ for ( const a of k ) {
314+ const iter = this . #expandKey( owner , a ) ;
315+ if ( iter === undefined ) { continue ; }
316+ out . push ( ...iter ) ;
317+ }
318+ return out ;
288319 }
289- }
290- #getMatchesFromKeys( pathin , step , owner , out ) {
291- const kk = Array . isArray ( step . k ) ? step . k : [ step . k ] ;
292- for ( const k of kk ) {
293- const normalized = this . #evaluateExpr( step , owner , k ) ;
294- if ( normalized === undefined ) { continue ; }
295- out . push ( [ ...pathin , normalized ] ) ;
320+ if ( typeof k === 'number' ) {
321+ if ( Array . isArray ( owner ) === false ) { return ; }
322+ return [ k >= 0 ? k : owner . length + k ] ;
296323 }
297- if ( step . mv !== this . #DESCENDANTS ) { return ; }
298- for ( const { obj, key, path } of this . #getDescendants( owner , true ) ) {
299- for ( const k of kk ) {
300- const normalized = this . #evaluateExpr( step , obj [ key ] , k ) ;
301- if ( normalized === undefined ) { continue ; }
302- out . push ( [ ...pathin , ...path , normalized ] ) ;
324+ if ( k === '*' ) {
325+ if ( Array . isArray ( owner ) ) { return owner . keys ( ) ; }
326+ return JSONPath . keys ( owner ) ;
327+ }
328+ if ( k instanceof JSONPath . Regex ) {
329+ const out = [ ] ;
330+ for ( const key of JSONPath . keys ( owner ) ) {
331+ if ( k . test ( key ) === false ) { continue ; }
332+ out . push ( key ) ;
303333 }
334+ return out ;
304335 }
336+ return [ k ] ;
305337 }
306338 #getMatchesFromExpr( pathin , step , owner , out ) {
307339 const recursive = step . mv === this . #DESCENDANTS;
308- if ( Array . isArray ( owner ) === false ) {
309- const r = this . #evaluate( step . steps , pathin ) ;
310- if ( r . length !== 0 ) { out . push ( pathin ) ; }
311- if ( recursive !== true ) { return ; }
312- }
313- for ( const { obj, key, path } of this . #getDescendants( owner , recursive ) ) {
314- if ( Array . isArray ( obj [ key ] ) ) { continue ; }
340+ for ( const { path } of this . #getDescendants( owner , recursive ) ) {
315341 const q = [ ...pathin , ...path ] ;
316342 const r = this . #evaluate( step . steps , q ) ;
317- if ( r . length === 0 ) { continue ; }
343+ if ( Boolean ( r ? .length ) === false ) { continue ; }
318344 out . push ( q ) ;
319345 }
320346 }
@@ -345,7 +371,7 @@ export class JSONPath {
345371 if ( Array . isArray ( v ) ) {
346372 this . stack . push ( { obj : v , keys : v . keys ( ) } ) ;
347373 } else if ( typeof v === 'object' && v !== null ) {
348- this . stack . push ( { obj : v , keys : Object . keys ( v ) . values ( ) } ) ;
374+ this . stack . push ( { obj : v , keys : JSONPath . keys ( v ) . values ( ) } ) ;
349375 }
350376 }
351377 return this ;
@@ -359,7 +385,7 @@ export class JSONPath {
359385 if ( Array . isArray ( v ) ) {
360386 iterator . stack . push ( { obj : v , keys : v . keys ( ) } ) ;
361387 } else if ( typeof v === 'object' && v !== null ) {
362- iterator . stack . push ( { obj : v , keys : Object . keys ( v ) . values ( ) } ) ;
388+ iterator . stack . push ( { obj : v , keys : JSONPath . keys ( v ) . values ( ) } ) ;
363389 }
364390 return iterator ;
365391 }
@@ -368,7 +394,7 @@ export class JSONPath {
368394 for ( ; ; ) {
369395 const c0 = query . charCodeAt ( i ) ;
370396 if ( c0 === 0x5D /* ] */ ) { break ; }
371- if ( c0 === 0x2C /* , */ ) {
397+ if ( c0 === 0x2C /* , */ || c0 === 0x20 /* SPACE */ ) {
372398 i += 1 ;
373399 continue ;
374400 }
@@ -387,17 +413,24 @@ export class JSONPath {
387413 i += match [ 0 ] . length ;
388414 continue ;
389415 }
390- const s = this . #consumeUnquotedIdentifier( query , i ) ;
391- if ( s === undefined ) { return ; }
392- keys . push ( s ) ;
393- i += s . length ;
416+ const r = this . #consumeUnquotedIdentifier( query , i ) ;
417+ if ( r === undefined ) { return ; }
418+ keys . push ( r . s ) ;
419+ i = r . i ;
394420 }
395421 return { s : keys . length === 1 ? keys [ 0 ] : keys , i } ;
396422 }
397423 #consumeUnquotedIdentifier( query , i ) {
424+ if ( query . charCodeAt ( i ) === 0x2F /* / */ ) {
425+ const r = this . #untilChar( query , 0x2F , i + 1 ) ;
426+ if ( r === undefined ) { return ; }
427+ let re ;
428+ try { re = new JSONPath . Regex ( r . s ) ; } catch { return ; }
429+ return { s : re , i : r . i } ;
430+ }
398431 const match = this . #reUnquotedIdentifier. exec ( query . slice ( i ) ) ;
399432 if ( match === null ) { return ; }
400- return match [ 0 ] ;
433+ return { s : match [ 0 ] , i : i + match [ 0 ] . length } ;
401434 }
402435 #untilChar( query , targetCharCode , i ) {
403436 const len = query . length ;
@@ -429,22 +462,27 @@ export class JSONPath {
429462 if ( r === undefined ) { return i ; }
430463 const match = / ^ [ i ] / . exec ( query . slice ( r . i ) ) ;
431464 try {
432- step . rval = new RegExp ( r . s , match && match [ 0 ] || undefined ) ;
433- } catch {
434- return i ;
435- }
465+ step . rval = new JSONPath . Regex ( r . s , match && match [ 0 ] || undefined ) ;
466+ } catch { return ; }
436467 step . op = 're' ;
437468 if ( match ) { r . i += match [ 0 ] . length ; }
438469 return r . i ;
439470 }
440471 const match = this . #reExpr. exec ( query . slice ( i ) ) ;
441- if ( match === null ) { return i ; }
442- try {
443- step . rval = JSON . parse ( match [ 2 ] ) ;
444- step . op = match [ 1 ] ;
445- } catch {
472+ if ( match === null ) { return ; }
473+ const op = match [ 1 ] , rval = match [ 2 ] ;
474+ if ( rval . charCodeAt ( 0 ) === 0x27 /* ' */ ) {
475+ const r = this . #untilChar( rval , 0x27 , 1 ) ;
476+ if ( r === undefined ) { return ; }
477+ step . rval = r . s ;
478+ step . op = op ;
479+ } else {
480+ try {
481+ step . rval = JSON . parse ( rval ) ;
482+ step . op = op ;
483+ } catch { return ; }
446484 }
447- return i + match [ 1 ] . length + match [ 2 ] . length ;
485+ return i + match [ 0 ] . length - 1 ;
448486 }
449487 #resolvePath( path ) {
450488 if ( path . length === 0 ) { return { value : this . #root } ; }
@@ -455,34 +493,26 @@ export class JSONPath {
455493 }
456494 return { obj, key, value : obj [ key ] } ;
457495 }
458- #evaluateExpr( step , owner , key ) {
496+ #evaluateExpr( step , owner , k ) {
459497 if ( owner === undefined || owner === null ) { return ; }
460- let k ;
461- if ( typeof key === 'number' ) {
462- if ( Array . isArray ( owner ) === false ) { return ; }
463- k = key >= 0 ? key : owner . length + key ;
464- } else {
465- k = key ;
466- }
467- const hasOwn = owner [ k ] !== undefined || Object . hasOwn ( owner , k ) ;
498+ const hasOwn = owner [ k ] !== undefined || JSONPath . hasOwn ( owner , k ) ;
468499 if ( step . op !== undefined && hasOwn === false ) { return ; }
469500 const target = step . not !== true ;
470501 const v = owner [ k ] ;
471- let outcome = false ;
472502 switch ( step . op ) {
473- case '==' : outcome = ( v === step . rval ) === target ; break ;
474- case '!=' : outcome = ( v !== step . rval ) === target ; break ;
475- case '<' : outcome = ( v < step . rval ) === target ; break ;
476- case '<=' : outcome = ( v <= step . rval ) === target ; break ;
477- case '>' : outcome = ( v > step . rval ) === target ; break ;
478- case '>=' : outcome = ( v >= step . rval ) === target ; break ;
479- case '^=' : outcome = `${ v } ` . startsWith ( step . rval ) === target ; break ;
480- case '$=' : outcome = `${ v } ` . endsWith ( step . rval ) === target ; break ;
481- case '*=' : outcome = `${ v } ` . includes ( step . rval ) === target ; break ;
482- case 're' : outcome = step . rval . test ( `${ v } ` ) ; break ;
483- default : outcome = hasOwn === target ; break ;
503+ case '==' : return ( v === step . rval ) === target ;
504+ case '!=' : return ( v !== step . rval ) === target ;
505+ case '<' : return ( v < step . rval ) === target ;
506+ case '<=' : return ( v <= step . rval ) === target ;
507+ case '>' : return ( v > step . rval ) === target ;
508+ case '>=' : return ( v >= step . rval ) === target ;
509+ case '^=' : return `${ v } ` . startsWith ( step . rval ) === target ;
510+ case '$=' : return `${ v } ` . endsWith ( step . rval ) === target ;
511+ case '*=' : return `${ v } ` . includes ( step . rval ) === target ;
512+ case 're' : return step . rval . test ( `${ v } ` ) ;
513+ default : break ;
484514 }
485- if ( outcome ) { return k ; }
515+ return hasOwn === target ;
486516 }
487517 #modifyVal( obj , key ) {
488518 let { modify, rval } = this . #compiled;
@@ -498,7 +528,7 @@ export class JSONPath {
498528 const lval = obj [ key ] ;
499529 if ( lval instanceof Object === false ) { return ; }
500530 if ( Array . isArray ( lval ) ) { return ; }
501- for ( const [ k , v ] of Object . entries ( rval ) ) {
531+ for ( const [ k , v ] of JSONPath . entries ( rval ) ) {
502532 lval [ k ] = v ;
503533 }
504534 break ;
@@ -522,10 +552,9 @@ export class JSONPath {
522552 this . #compiled. re = null ;
523553 try {
524554 this . #compiled. re = rval . regex !== undefined
525- ? new RegExp ( rval . regex , rval . flags )
526- : new RegExp ( rval . pattern . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ) ;
527- } catch {
528- }
555+ ? new JSONPath . Regex ( rval . regex , rval . flags )
556+ : new JSONPath . Regex ( rval . pattern . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ) ;
557+ } catch { }
529558 }
530559 if ( this . #compiled. re === null ) { return ; }
531560 obj [ key ] = lval . replace ( this . #compiled. re , rval . replacement ) ;
0 commit comments