JSONPath: Add ability to substitute a pattern within a string value

As discussed with filter list maintainers.

Examples of JSON path expression to replace a pattern within a string
value:

  ..book.*.author=repl({"pattern": "Melville", "replacement": "Toto"})
  ..book.*.author=repl({"regex": "e", "replacement": "o"})
  ..book.*.author=repl({"regex": "e", "flags": "g", "replacement": "o"})

If the target value is not a string, no modification will occur.
This commit is contained in:
Raymond Hill 2025-08-09 11:05:27 -04:00
parent cf70f2abbc
commit 38ca6d41ff
No known key found for this signature in database
GPG key ID: 25E1490B761470C2

View file

@ -99,12 +99,19 @@ export class JSONPath {
const r = this.#compile(query, 0);
if ( r === undefined ) { return; }
if ( r.i !== query.length ) {
if ( query.startsWith('+=', r.i) ) {
let val;
if ( query.startsWith('=', r.i) ) {
if ( /^=repl\(.+\)$/.test(query.slice(r.i)) ) {
r.modify = 'repl';
val = query.slice(r.i+6, -1);
} else {
val = query.slice(r.i+1);
}
} else if ( query.startsWith('+=', r.i) ) {
r.modify = '+';
r.i += 1;
val = query.slice(r.i+2);
}
if ( query.startsWith('=', r.i) === false ) { return; }
try { r.rval = JSON.parse(query.slice(r.i+1)); }
try { r.rval = JSON.parse(val); }
catch { return; }
}
this.#compiled = r;
@ -118,7 +125,7 @@ export class JSONPath {
}
apply(root) {
if ( this.valid === false ) { return 0; }
const { modify, rval } = this.#compiled;
const { rval } = this.#compiled;
this.#root = root;
const paths = this.#evaluate(this.#compiled.steps, []);
const n = paths.length;
@ -126,11 +133,7 @@ export class JSONPath {
while ( i-- ) {
const { obj, key } = this.#resolvePath(paths[i]);
if ( rval !== undefined ) {
if ( modify === '+' ) {
this.#modifyVal(obj, key, rval);
} else {
obj[key] = rval;
}
this.#modifyVal(obj, key);
} else if ( Array.isArray(obj) && typeof key === 'number' ) {
obj.splice(key, 1);
} else {
@ -458,13 +461,40 @@ export class JSONPath {
}
if ( outcome ) { return k; }
}
#modifyVal(obj, key, rval) {
const lval = obj[key];
if ( rval instanceof Object === false ) { return; }
if ( lval instanceof Object === false ) { return; }
if ( Array.isArray(lval) ) { return; }
for ( const [ k, v ] of Object.entries(rval) ) {
lval[k] = v;
#modifyVal(obj, key) {
const { modify, rval } = this.#compiled;
switch ( modify ) {
case undefined:
obj[key] = rval;
break;
case '+': {
if ( rval instanceof Object === false ) { return; }
const lval = obj[key];
if ( lval instanceof Object === false ) { return; }
if ( Array.isArray(lval) ) { return; }
for ( const [ k, v ] of Object.entries(rval) ) {
lval[k] = v;
}
break;
}
case 'repl': {
const lval = obj[key];
if ( typeof lval !== 'string' ) { return; }
if ( this.#compiled.re === undefined ) {
this.#compiled.re = null;
try {
this.#compiled.re = rval.regex !== undefined
? new RegExp(rval.regex, rval.flags)
: new RegExp(rval.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
} catch {
}
}
if ( this.#compiled.re === null ) { return; }
obj[key] = lval.replace(this.#compiled.re, rval.replacement);
break;
}
default:
break;
}
}
}