Skip to content

Commit 38ca6d4

Browse files
committed
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.
1 parent cf70f2a commit 38ca6d4

File tree

1 file changed

+47
-17
lines changed

1 file changed

+47
-17
lines changed

src/js/jsonpath.js

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,19 @@ export class JSONPath {
9999
const r = this.#compile(query, 0);
100100
if ( r === undefined ) { return; }
101101
if ( r.i !== query.length ) {
102-
if ( query.startsWith('+=', r.i) ) {
102+
let val;
103+
if ( query.startsWith('=', r.i) ) {
104+
if ( /^=repl\(.+\)$/.test(query.slice(r.i)) ) {
105+
r.modify = 'repl';
106+
val = query.slice(r.i+6, -1);
107+
} else {
108+
val = query.slice(r.i+1);
109+
}
110+
} else if ( query.startsWith('+=', r.i) ) {
103111
r.modify = '+';
104-
r.i += 1;
112+
val = query.slice(r.i+2);
105113
}
106-
if ( query.startsWith('=', r.i) === false ) { return; }
107-
try { r.rval = JSON.parse(query.slice(r.i+1)); }
114+
try { r.rval = JSON.parse(val); }
108115
catch { return; }
109116
}
110117
this.#compiled = r;
@@ -118,19 +125,15 @@ export class JSONPath {
118125
}
119126
apply(root) {
120127
if ( this.valid === false ) { return 0; }
121-
const { modify, rval } = this.#compiled;
128+
const { rval } = this.#compiled;
122129
this.#root = root;
123130
const paths = this.#evaluate(this.#compiled.steps, []);
124131
const n = paths.length;
125132
let i = n;
126133
while ( i-- ) {
127134
const { obj, key } = this.#resolvePath(paths[i]);
128135
if ( rval !== undefined ) {
129-
if ( modify === '+' ) {
130-
this.#modifyVal(obj, key, rval);
131-
} else {
132-
obj[key] = rval;
133-
}
136+
this.#modifyVal(obj, key);
134137
} else if ( Array.isArray(obj) && typeof key === 'number' ) {
135138
obj.splice(key, 1);
136139
} else {
@@ -458,13 +461,40 @@ export class JSONPath {
458461
}
459462
if ( outcome ) { return k; }
460463
}
461-
#modifyVal(obj, key, rval) {
462-
const lval = obj[key];
463-
if ( rval instanceof Object === false ) { return; }
464-
if ( lval instanceof Object === false ) { return; }
465-
if ( Array.isArray(lval) ) { return; }
466-
for ( const [ k, v ] of Object.entries(rval) ) {
467-
lval[k] = v;
464+
#modifyVal(obj, key) {
465+
const { modify, rval } = this.#compiled;
466+
switch ( modify ) {
467+
case undefined:
468+
obj[key] = rval;
469+
break;
470+
case '+': {
471+
if ( rval instanceof Object === false ) { return; }
472+
const lval = obj[key];
473+
if ( lval instanceof Object === false ) { return; }
474+
if ( Array.isArray(lval) ) { return; }
475+
for ( const [ k, v ] of Object.entries(rval) ) {
476+
lval[k] = v;
477+
}
478+
break;
479+
}
480+
case 'repl': {
481+
const lval = obj[key];
482+
if ( typeof lval !== 'string' ) { return; }
483+
if ( this.#compiled.re === undefined ) {
484+
this.#compiled.re = null;
485+
try {
486+
this.#compiled.re = rval.regex !== undefined
487+
? new RegExp(rval.regex, rval.flags)
488+
: new RegExp(rval.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
489+
} catch {
490+
}
491+
}
492+
if ( this.#compiled.re === null ) { return; }
493+
obj[key] = lval.replace(this.#compiled.re, rval.replacement);
494+
break;
495+
}
496+
default:
497+
break;
468498
}
469499
}
470500
}

0 commit comments

Comments
 (0)