From faff03520343e62de109e018beaef1687757e36c Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 9 Aug 2025 13:56:02 -0400 Subject: [PATCH] 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. --- src/js/jsonpath.js | 27 +++++++++++++------- src/js/resources/json-edit.js | 46 ++++++++++++++++++++--------------- src/js/traffic.js | 10 +++++--- tools/jsonpath-tool.html | 8 +++--- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/js/jsonpath.js b/src/js/jsonpath.js index 15caa054b..020aab0bf 100644 --- a/src/js/jsonpath.js +++ b/src/js/jsonpath.js @@ -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 ]; diff --git a/src/js/resources/json-edit.js b/src/js/resources/json-edit.js index f8ea191dd..27fbedadd 100644 --- a/src/js/resources/json-edit.js +++ b/src/js/resources/json-edit.js @@ -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); diff --git a/src/js/traffic.js b/src/js/traffic.js index a06b7be89..049d88097 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -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; diff --git a/tools/jsonpath-tool.html b/tools/jsonpath-tool.html index 5643813dc..f783db46b 100644 --- a/tools/jsonpath-tool.html +++ b/tools/jsonpath-tool.html @@ -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,