JSONPath: Add ability to select root node for appending/modifying

As discussed with filter list maintainers.

Examples of usage:

  $ => result is `null`
  $+={"modifyOrCreate": "..."}

These expressions were not working with previous version.
This commit is contained in:
Raymond Hill 2025-08-09 13:56:02 -04:00
parent 5629bf8a23
commit faff035203
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
4 changed files with 55 additions and 36 deletions

View file

@ -124,12 +124,12 @@ export class JSONPath {
return paths;
}
apply(root) {
if ( this.valid === false ) { return 0; }
if ( this.valid === false ) { return; }
const { rval } = this.#compiled;
this.#root = root;
this.#root = { '$': root };
const paths = this.#evaluate(this.#compiled.steps, []);
const n = paths.length;
let i = n;
let i = paths.length
if ( i === 0 ) { this.#root = null; return; }
while ( i-- ) {
const { obj, key } = this.#resolvePath(paths[i]);
if ( rval !== undefined ) {
@ -140,8 +140,9 @@ export class JSONPath {
delete obj[key];
}
}
const result = this.#root['$'] ?? null;
this.#root = null;
return n;
return result;
}
dump() {
return JSON.stringify(this.#compiled);
@ -166,8 +167,15 @@ export class JSONPath {
if ( query.length === 0 ) { return; }
const steps = [];
let c = query.charCodeAt(i);
steps.push({ mv: c === 0x24 /* $ */ ? this.#ROOT : this.#CURRENT });
if ( c === 0x24 /* $ */ || c === 0x40 /* @ */ ) { i += 1; }
if ( c === 0x24 /* $ */ ) {
steps.push({ mv: this.#ROOT });
i += 1;
} else if ( c === 0x40 /* @ */ ) {
steps.push({ mv: this.#CURRENT });
i += 1;
} else {
steps.push({ mv: i === 0 ? this.#ROOT : this.#CURRENT });
}
let mv = this.#UNDEFINED;
for (;;) {
if ( i === query.length ) { break; }
@ -229,7 +237,8 @@ export class JSONPath {
i = r.i + 1;
mv = this.#UNDEFINED;
}
if ( steps.length <= 1 ) { return; }
if ( steps.length === 0 ) { return; }
if ( mv !== this.#UNDEFINED ) { return; }
return { steps, i };
}
#evaluate(steps, pathin) {
@ -238,7 +247,7 @@ export class JSONPath {
for ( const step of steps ) {
switch ( step.mv ) {
case this.#ROOT:
resultset = [ [] ];
resultset = [ [ '$' ] ];
break;
case this.#CURRENT:
resultset = [ pathin ];

View file

@ -51,12 +51,13 @@ function editOutboundObjectFn(
}
proxyApplyFn(propChain, function(context) {
const obj = context.reflect();
if ( jsonp.apply(obj) === 0 ) { return obj; }
const objAfter = jsonp.apply(obj);
if ( objAfter === undefined ) { return obj; }
safe.uboLog(logPrefix, 'Edited');
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(obj, null, 2)}`);
safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
}
return obj;
return objAfter;
});
}
registerScriptlet(editOutboundObjectFn, {
@ -206,12 +207,13 @@ function editInboundObjectFn(
} catch {
}
if ( typeof clone !== 'object' || clone === null ) { return; }
if ( jsonp.apply(clone) === 0 ) { return; }
const objAfter = jsonp.apply(clone);
if ( objAfter === undefined ) { return; }
safe.uboLog(logPrefix, 'Edited');
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(clone, null, 2)}`);
safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
}
return clone;
return objAfter;
};
proxyApplyFn(propChain, function(context) {
const i = getArgPos(context.args);
@ -343,13 +345,17 @@ function jsonEditXhrResponseFn(trusted, jsonq = '') {
} else if ( typeof innerResponse === 'string' ) {
try { obj = safe.JSON_parse(innerResponse); } catch { }
}
if ( typeof obj !== 'object' || obj === null || jsonp.apply(obj) === 0 ) {
if ( typeof obj !== 'object' || obj === null ) {
return (xhrDetails.response = innerResponse);
}
const objAfter = jsonp.apply(obj);
if ( objAfter === undefined ) {
return (xhrDetails.response = innerResponse);
}
safe.uboLog(logPrefix, 'Edited');
const outerResponse = typeof innerResponse === 'string'
? JSONPath.toJSON(obj, safe.JSON_stringify)
: obj;
? JSONPath.toJSON(objAfter, safe.JSON_stringify)
: objAfter;
return (xhrDetails.response = outerResponse);
}
get responseText() {
@ -467,9 +473,9 @@ function jsonEditXhrRequestFn(trusted, jsonq = '') {
try { data = safe.JSON_parse(body); }
catch { }
if ( data instanceof Object === false ) { return; }
const n = jsonp.apply(data);
if ( n === 0 ) { return; }
body = safe.JSON_stringify(data);
const objAfter = jsonp.apply(data);
if ( objAfter === undefined ) { return; }
body = safe.JSON_stringify(objAfter);
safe.uboLog(logPrefix, 'Edited');
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `After edit:\n${body}`);
@ -583,9 +589,10 @@ function jsonEditFetchResponseFn(trusted, jsonq = '') {
const response = responseBefore.clone();
return response.json().then(obj => {
if ( typeof obj !== 'object' ) { return responseBefore; }
if ( jsonp.apply(obj) === 0 ) { return responseBefore; }
const objAfter = jsonp.apply(obj);
if ( objAfter === undefined ) { return responseBefore; }
safe.uboLog(logPrefix, 'Edited');
const responseAfter = Response.json(obj, {
const responseAfter = Response.json(objAfter, {
status: responseBefore.status,
statusText: responseBefore.statusText,
headers: responseBefore.headers,
@ -694,9 +701,9 @@ function jsonEditFetchRequestFn(trusted, jsonq = '') {
try { data = safe.JSON_parse(body); }
catch { }
if ( data instanceof Object === false ) { return; }
const n = jsonp.apply(data);
if ( n === 0 ) { return; }
return safe.JSON_stringify(data);
const objAfter = jsonp.apply(data);
if ( objAfter === undefined ) { return; }
return safe.JSON_stringify(objAfter);
}
const proxyHandler = context => {
const args = context.callArgs;
@ -813,11 +820,12 @@ function jsonlEditFn(jsonp, text = '') {
linesAfter.push(lineBefore);
continue;
}
if ( jsonp.apply(obj) === 0 ) {
const objAfter = jsonp.apply(obj);
if ( objAfter === undefined ) {
linesAfter.push(lineBefore);
continue;
}
const lineAfter = safe.JSON_stringify(obj);
const lineAfter = safe.JSON_stringify(objAfter);
linesAfter.push(lineAfter);
}
return linesAfter.join(lineSeparator);

View file

@ -651,8 +651,9 @@ function textResponseFilterer(session, directives) {
const json = session.getString();
let obj;
try { obj = JSON.parse(json); } catch { break; }
if ( cache.jsonp.apply(obj) === 0 ) { break; }
session.setString(cache.jsonp.toJSON(obj));
const objAfter = cache.jsonp.apply(obj);
if ( objAfter === undefined ) { break; }
session.setString(cache.jsonp.toJSON(objAfter));
applied.push(directive);
break;
}
@ -666,11 +667,12 @@ function textResponseFilterer(session, directives) {
linesAfter.push(lineBefore);
continue;
}
if ( cache.jsonp.apply(obj) === 0 ) {
const objAfter = cache.jsonp.apply(obj);
if ( objAfter === undefined ) {
linesAfter.push(lineBefore);
continue;
}
linesAfter.push(cache.jsonp.toJSON(obj));
linesAfter.push(cache.jsonp.toJSON(objAfter));
}
session.setString(linesAfter.join('\n'));
break;

View file

@ -150,12 +150,12 @@ section .cm-lineWrapping {
const jsonpath = input.value;
jsonp.compile(jsonpath);
const jsonDataIn = readJSON();
const result = formatResult(jsonp.evaluate(jsonDataIn));
const left = formatResult(jsonp.evaluate(jsonDataIn));
const pathsDiv = document.querySelector('#jsonpath-result');
pathsDiv.textContent = result;
pathsDiv.textContent = left;
const jsonDataOut = readJSON();
jsonp.apply(jsonDataOut);
const bText = JSON.stringify(jsonDataOut, null, 2);
const objAfter = jsonp.apply(jsonDataOut);
const bText = JSON.stringify(objAfter !== undefined ? objAfter : jsonDataOut, null, 2);
cmMergeView.b.dispatch({
changes: {
from: 0, to: cmMergeView.b.state.doc.length,