From 2b0739f06e6c1e85d70b02fcbe46bfc41ab7d869 Mon Sep 17 00:00:00 2001 From: aka James4u Date: Wed, 12 Nov 2025 07:33:28 -0800 Subject: [PATCH] Adds jsondelete operator to fix #9371 (#9390) * Added jsondelter to fix 9371 * Replacced deprecated utils.isArray with Array.isArray, Refactored to remove duplication between setDataItem() and getDataItem() * added changenotes for #9371 * Update #9371.tid Fix key word: links-> github-links * changed change-category * updated github-links * Apply suggestion from @saqimtiaz --------- Co-authored-by: Saq Imtiaz --- core/modules/filters/json-ops.js | 84 +++++++++++++++---- .../test/tiddlers/tests/test-json-filters.js | 27 ++++++ .../tiddlers/filters/examples/jsondelete.tid | 59 +++++++++++++ .../tw5.com/tiddlers/filters/jsondelete.tid | 54 ++++++++++++ .../tiddlers/releasenotes/5.4.0/#9371.tid | 11 +++ 5 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 editions/tw5.com/tiddlers/filters/examples/jsondelete.tid create mode 100644 editions/tw5.com/tiddlers/filters/jsondelete.tid create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.4.0/#9371.tid diff --git a/core/modules/filters/json-ops.js b/core/modules/filters/json-ops.js index 8f16ad4175..cbffecc247 100644 --- a/core/modules/filters/json-ops.js +++ b/core/modules/filters/json-ops.js @@ -113,6 +113,22 @@ exports["jsonset"] = function(source,operator,options) { return results; }; +exports["jsondelete"] = function(source,operator,options) { + var indexes = operator.operands, + results = []; + source(function(tiddler,title) { + var data = $tw.utils.parseJSONSafe(title,title); + // If parsing failed (data equals original title and is a string), return unchanged + if(data === title && typeof data === "string") { + results.push(title); + } else if(data) { + data = deleteDataItem(data,indexes); + results.push(JSON.stringify(data)); + } + }); + return results; +}; + /* Given a JSON data structure and an array of index strings, return an array of the string representation of the values at the end of the index chain, or "undefined" if any of the index strings are invalid */ @@ -144,7 +160,7 @@ function convertDataItemValueToStrings(item) { return ["null"] } else if(typeof item === "object") { var results = [],i,t; - if($tw.utils.isArray(item)) { + if(Array.isArray(item)) { // Return all the items in arrays recursively for(i=0; i= 0 && lastIndex < current.length) { + current.splice(lastIndex,1); + } + } else if(typeof current === "object" && current !== null) { + delete current[lastIndex]; + } + return data; +} diff --git a/editions/test/tiddlers/tests/test-json-filters.js b/editions/test/tiddlers/tests/test-json-filters.js index 566f77fe29..77d770ea32 100644 --- a/editions/test/tiddlers/tests/test-json-filters.js +++ b/editions/test/tiddlers/tests/test-json-filters.js @@ -143,6 +143,33 @@ describe("json filter tests", function() { expect(wiki.filterTiddlers("[{First}jsonset:json[notjson]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); }); + it("should support the jsondelete operator", function() { + // Delete top-level object property + expect(wiki.filterTiddlers("[{First}jsondelete[a]]")).toEqual(['{"b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsondelete[b]]")).toEqual(['{"a":"one","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsondelete[c]]")).toEqual(['{"a":"one","b":"","d":{"e":"four","f":["five","six",true,false,null]}}']); + // Delete nested object property + expect(wiki.filterTiddlers("[{First}jsondelete[d],[e]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"f":["five","six",true,false,null]}}']); + // Delete array element + expect(wiki.filterTiddlers("[{First}jsondelete[d],[f],[0]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsondelete[d],[f],[1]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five",true,false,null]}}']); + // Delete using negative array index + expect(wiki.filterTiddlers("[{First}jsondelete[d],[f],[-1]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false]}}']); + expect(wiki.filterTiddlers("[{First}jsondelete[d],[f],[-2]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,null]}}']); + expect(wiki.filterTiddlers("[{First}jsondelete[d],[f],[-5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["six",true,false,null]}}']); + // Delete from array + expect(wiki.filterTiddlers("[{Second}jsondelete[0]]")).toEqual(['["deux","trois",["quatre","cinq"]]']); + expect(wiki.filterTiddlers("[{Second}jsondelete[1]]")).toEqual(['["une","trois",["quatre","cinq"]]']); + expect(wiki.filterTiddlers("[{Second}jsondelete[-1]]")).toEqual(['["une","deux","trois"]']); + // Attempting to delete non-existent property should return unchanged + expect(wiki.filterTiddlers("[{First}jsondelete[missing-property]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsondelete[d],[missing]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + // Attempting to delete root should return unchanged + expect(wiki.filterTiddlers("[{First}jsondelete[]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + // Non-JSON input should return unchanged + expect(wiki.filterTiddlers("[{Third}jsondelete[a]]")).toEqual(["This is not JSON"]); + }); + it("should support the format:json operator", function() { expect(wiki.filterTiddlers("[{First}format:json[]]")).toEqual(["{\"a\":\"one\",\"b\":\"\",\"c\":1.618,\"d\":{\"e\":\"four\",\"f\":[\"five\",\"six\",true,false,null]}}"]); expect(wiki.filterTiddlers("[{First}format:json[4]]")).toEqual(["{\n \"a\": \"one\",\n \"b\": \"\",\n \"c\": 1.618,\n \"d\": {\n \"e\": \"four\",\n \"f\": [\n \"five\",\n \"six\",\n true,\n false,\n null\n ]\n }\n}"]); diff --git a/editions/tw5.com/tiddlers/filters/examples/jsondelete.tid b/editions/tw5.com/tiddlers/filters/examples/jsondelete.tid new file mode 100644 index 0000000000..58653a61a5 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/jsondelete.tid @@ -0,0 +1,59 @@ +created: 20250115120000000 +modified: 20250115120000000 +tags: [[Operator Examples]] [[jsondelete Operator]] +title: jsondelete Operator (Examples) + +<$let object-a="""{ + "a": "one", + "b": "", + "c": "three", + "d": { + "e": "four", + "f": [ + "five", + "six", + true, + false, + null + ], + "g": { + "x": "max", + "y": "may", + "z": "maize" + } + } +} +""" +object-b="""{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}""" +array-a="""["une","deux","trois",["quatre","cinq"]]"""> + +The examples below assume the following JSON object is contained in the variable `object-a`: + +
<>
+ +<<.operator-example 1 "[jsondelete[a]]" "Delete a top-level object property">> +<<.operator-example 2 "[jsondelete[d],[e]]" "Delete a nested object property">> +<<.operator-example 3 "[jsondelete[d],[f],[0]]" "Delete the first element from an array">> +<<.operator-example 4 "[jsondelete[d],[f],[-1]]" "Delete the last element from an array using negative index">> +<<.operator-example 5 "[jsondelete[d],[f],[-2]]" "Delete the second-to-last element from an array using negative index">> +<<.operator-example 6 "[jsondelete[d],[g],[x]]" "Delete a deeply nested object property">> +<<.operator-example 7 "[jsondelete[]]" "If no parameters are specified, the JSON object is returned unchanged">> +<<.operator-example 8 "[jsondelete[missing]]" "If the property does not exist, the JSON object is returned unchanged">> + +The examples below assume the following JSON object is contained in the variable `object-b`: + +
<>
+ +<<.operator-example 9 "[jsondelete[b]]" "Delete an empty string property">> +<<.operator-example 10 "[jsondelete[d],[f],[1]]" "Delete a middle element from an array">> + +The examples below assume the following JSON array is contained in the variable `array-a`: + +
<>
+ +<<.operator-example 11 "[jsondelete[0]]" "Delete the first element from a top-level array">> +<<.operator-example 12 "[jsondelete[-1]]" "Delete the last element from a top-level array using negative index">> +<<.operator-example 13 "[jsondelete[3],[0]]" "Delete an element from a nested array">> + +<<.operator-example 14 "[] [] :and[jsondelete[a]]" "If the input consists of multiple JSON objects with matching properties, the property is deleted from all of them">> + diff --git a/editions/tw5.com/tiddlers/filters/jsondelete.tid b/editions/tw5.com/tiddlers/filters/jsondelete.tid new file mode 100644 index 0000000000..29a9271596 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/jsondelete.tid @@ -0,0 +1,54 @@ +caption: jsondelete +created: 20250115120000000 +modified: 20250115120000000 +op-input: a selection of JSON objects +op-output: the JSON objects with the specified property deleted +op-parameter: one or more indexes of the property to delete +op-purpose: delete a property from JSON objects +tags: [[Filter Operators]] [[JSON Operators]] +title: jsondelete Operator + +<<.from-version "5.4.0">> The <<.op jsondelete>> operator is used to delete a property from JSON strings. See [[JSON in TiddlyWiki]] for background. See also the following related operators: + +* <<.olink jsonset>> to set values within JSON objects +* <<.olink jsonget>> to retrieve the values of a property in JSON data +* <<.olink jsontype>> to retrieve the type of a JSON value +* <<.olink jsonindexes>> to retrieve the names of the fields of a JSON object, or the indexes of a JSON array +* <<.olink jsonextract>> to retrieve a JSON value as a string of JSON + +Properties within a JSON object are identified by a sequence of indexes. In the following example, the value at `[a]` is `one`, and the value at `[d][f][0]` is `five`. + +``` +{ + "a": "one", + "b": "", + "c": "three", + "d": { + "e": "four", + "f": [ + "five", + "six", + true, + false, + null + ], + "g": { + "x": "max", + "y": "may", + "z": "maize" + } + } +} +``` + +The <<.op jsondelete>> operator uses multiple parameters to specify the indexes of the property to delete. For object properties, the property is removed using JavaScript's `delete` operator. For array elements, the element is removed using `splice`, which shifts remaining elements. + +Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on. + +Indexes can be dynamically composed from variables and transclusions, e.g. `[jsondelete,{!!field},[0]]`. + +If the specified property does not exist, the JSON object is returned unchanged. If you attempt to delete the root object itself (by providing no indexes or a blank index), the JSON object is returned unchanged. + +If the input consists of multiple JSON objects, the property is deleted from all of them. + +<<.operator-examples "jsondelete">> diff --git a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9371.tid b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9371.tid new file mode 100644 index 0000000000..a9b4d49a98 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9371.tid @@ -0,0 +1,11 @@ +title: $:/changenotes/5.4.0/#9371 +description: Added jsondelete operator for deleting properties from JSON objects +release: 5.4.0 +tags: $:/tags/ChangeNote +change-type: feature +change-category: filters +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9390 +github-contributors: SmartDever02 + +Added the <<.op jsondelete>> operator for deleting properties from JSON strings. The operator uses the same code path as <<.op jsonset>> to locate the correct part of the object, ensuring consistency between setting and deleting operations. It supports deleting both object properties and array elements, with support for negative array indexes counted from the end. +