diff --git a/core/modules/filters/json-ops.js b/core/modules/filters/json-ops.js index 2be9ec754..51e509432 100644 --- a/core/modules/filters/json-ops.js +++ b/core/modules/filters/json-ops.js @@ -68,6 +68,54 @@ exports["jsontype"] = function(source,operator,options) { return results; }; +exports["jsonset"] = function(source,operator,options) { + var suffixes = operator.suffixes || [], + type = suffixes[0] && suffixes[0][0], + indexes = operator.operands.slice(0,-1), + value = operator.operands[operator.operands.length - 1], + results = []; + if(operator.operands.length === 1 && operator.operands[0] === "") { + value = undefined; // Prevents the value from being assigned + } + switch(type) { + case "string": + // Use value unchanged + break; + case "boolean": + value = (value === "true" ? true : (value === "false" ? false : undefined)); + break; + case "number": + value = $tw.utils.parseNumber(value); + break; + case "array": + indexes = operator.operands; + value = []; + break; + case "object": + indexes = operator.operands; + value = {}; + break; + case "null": + indexes = operator.operands; + value = null; + break; + case "json": + value = $tw.utils.parseJSONSafe(value,function() {return undefined;}); + break; + default: + // Use value unchanged + break; + } + source(function(tiddler,title) { + var data = $tw.utils.parseJSONSafe(title,title); + if(data) { + data = setDataItem(data,indexes,value); + 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 */ @@ -186,5 +234,34 @@ function getDataItem(data,indexes) { return item; } +/* +Given a JSON data structure, an array of index strings and a value, return the data structure with the value added at the end of the index chain. If any of the index strings are invalid then the JSON data structure is returned unmodified. If the root item is targetted then a different data object will be returned +*/ +function setDataItem(data,indexes,value) { + // Ignore attempts to assign undefined + if(value === undefined) { + return data; + } + // Check for the root item + if(indexes.length === 0 || (indexes.length === 1 && indexes[0] === "")) { + return value; + } + // Traverse the JSON data structure using the index chain + var current = data; + for(var i = 0; i < indexes.length - 1; i++) { + var index = indexes[i]; + if($tw.utils.hop(current,index)) { + current = current[index]; + } else { + // Return the original JSON data structure if any of the index strings are invalid + return data; + } + } + // Add the value to the end of the index chain + var lastIndex = indexes[indexes.length - 1]; + current[lastIndex] = value; + return data; +} + })(); \ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-json-filters.js b/editions/test/tiddlers/tests/test-json-filters.js index 68a82e774..b2f2c8e82 100644 --- a/editions/test/tiddlers/tests/test-json-filters.js +++ b/editions/test/tiddlers/tests/test-json-filters.js @@ -103,6 +103,24 @@ describe("json filter tests", function() { expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[4]]")).toEqual(["null"]); }); + it("should support the jsonset operator", function() { + expect(wiki.filterTiddlers("[{First}jsonset[]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsonset[],[Antelope]]")).toEqual(['"Antelope"']); + expect(wiki.filterTiddlers("[{First}jsonset:number[],[not a number]]")).toEqual(['0']); + expect(wiki.filterTiddlers("[{First}jsonset[id],[Antelope]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":"Antelope"}']); + expect(wiki.filterTiddlers("[{First}jsonset:notatype[id],[Antelope]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":"Antelope"}']); + expect(wiki.filterTiddlers("[{First}jsonset:boolean[id],[false]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":false}']); + expect(wiki.filterTiddlers("[{First}jsonset:boolean[id],[Antelope]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsonset:number[id],[42]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":42}']); + expect(wiki.filterTiddlers("[{First}jsonset:null[id]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":null}']); + expect(wiki.filterTiddlers("[{First}jsonset:array[d],[f],[5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,[]]}}']); + expect(wiki.filterTiddlers("[{First}jsonset:object[d],[f],[5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,{}]}}']); + expect(wiki.filterTiddlers("[{First}jsonset[missing],[id],[Antelope]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); + expect(wiki.filterTiddlers("[{First}jsonset:json[\"Antelope\"]]")).toEqual(['"Antelope"']); + expect(wiki.filterTiddlers("[{First}jsonset:json[id],[{\"a\":313}]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":{"a":313}}']); + 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 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/jsonset.tid b/editions/tw5.com/tiddlers/filters/jsonset.tid new file mode 100644 index 000000000..9f70f6eb4 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/jsonset.tid @@ -0,0 +1,96 @@ +created: 20230915121010948 +modified: 20230915121010948 +tags: [[Filter Operators]] [[JSON Operators]] +title: jsonset Operator +caption: jsonset +op-purpose: set the value of a property in JSON strings +op-input: a selection of JSON strings +op-parameter: one or more indexes of the property to retrieve and sometimes a value to assign +op-output: the JSON strings with the specified property assigned + +<<.from-version "5.3.2">> See [[JSON in TiddlyWiki]] for background. + +The <<.op jsonset>> operator is used to set a property value in JSON strings. See also the following related operators: + +* <<.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 following examples assume that this JSON data is contained in a variable called `jsondata`. + +The <<.op jsonset>> operator uses multiple operands to specify the indexes of the property to set. When used to assign strings the final operand is interpreted as the value to assign. For example: + +``` +[jsonset[d],[Jaguar]] --> {"a": "one","b": "","c": "three","d": "Jaguar"} +[jsonset[d],[f],[Panther]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": "Panther","g": {"x": "max","y": "may","z": "maize"}}"} +``` + +Indexes can be dynamically composed from variables and transclusions: + +``` +[jsonset,{!!field},[0],{CurrentResult}] +``` + +The data type of the value to be assigned to the property can be specified with an optional suffix: + +|!Suffix |!Description | +|''string'' |The string is specified as the final operand | +|''boolean'' |The boolean value is true if the final operand is the string "true" and false if the final operand is the string "false". Any other value for the final string results prevents the property from being assigned | +|''number'' |The numeric value is taken from the final operand. Invalid numbers are interpreted as zero | +|''json'' |The JSON string value is taken from the final operand. Invalid JSON prevents the property from being assigned | +|''object'' |An empty object is assigned to the property. The final operand is not used as a value | +|''array'' |An empty array is assigned to the property. The final operand is not used as a value | +|''null'' |The special value null is assigned to the property. The final operand is not used as a value | + +For example: + +``` +Input string: +{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} + +[jsonset[]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} +[jsonset[],[Antelope]] --> "Antelope" +[jsonset:number[],[not a number]] --> 0 +[jsonset[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":"Antelope"} +[jsonset:notatype[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":"Antelope"} +[jsonset:boolean[id],[false]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":false} +[jsonset:boolean[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} +[jsonset:number[id],[42]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":42} +[jsonset:null[id]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":null} +[jsonset:array[d],[f],[5]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,[]]}} +[jsonset:object[d],[f],[5]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,{}]}} +[jsonset[missing],[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} +``` + +A subtlety is that the special case of a single operand sets the value of that operand as the new JSON string, entirely replacing the input object. If that operand is blank, the operation is ignored and no assignment takes place. Thus: + +``` +[jsonset[Panther]] --> "Panther" +[jsonset[]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five", "six", true, false, null],"g": {"x": "max","y": "may","z": "maize"}}"} +```