1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-23 10:07:19 +00:00

JSON Filter Operators (Revised Attempt) (#6936)

* First commit

Cherry-picked from #6666

* Adjust release version number

Just in case we decide to make a release before we merge #6666
This commit is contained in:
Jeremy Ruston 2022-09-02 18:15:45 +01:00 committed by GitHub
parent 7652aa5fed
commit 35b9faaa89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 631 additions and 13 deletions

View File

@ -0,0 +1,35 @@
/*\
title: $:/core/modules/filters/format/json.js
type: application/javascript
module-type: formatfilteroperator
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter function
*/
exports.json = function(source,operand,options) {
var results = [],
spaces = null;
if(operand) {
spaces = /^\d+$/.test(operand) ? parseInt(operand,10) : operand;
}
source(function(tiddler,title) {
var data = $tw.utils.parseJSONSafe(title);
try {
data = JSON.parse(title);
} catch(e) {
data = undefined;
}
if(data !== undefined) {
results.push(JSON.stringify(data,null,spaces));
}
});
return results;
};
})();

View File

@ -0,0 +1,153 @@
/*\
title: $:/core/modules/filters/json-ops.js
type: application/javascript
module-type: filteroperator
Filter operators for JSON operations
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports["jsonget"] = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var data = $tw.utils.parseJSONSafe(title,title);
if(data) {
var item = getDataItemValueAsString(data,operator.operands);
if(item !== undefined) {
results.push(item);
}
}
});
return results;
};
exports["jsonindexes"] = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var data = $tw.utils.parseJSONSafe(title,title);
if(data) {
var item = getDataItemKeysAsStrings(data,operator.operands);
if(item !== undefined) {
results.push.apply(results,item);
}
}
});
return results;
};
exports["jsontype"] = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var data = $tw.utils.parseJSONSafe(title,title);
if(data) {
var item = getDataItemType(data,operator.operands);
if(item !== undefined) {
results.push(item);
}
}
});
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
*/
function getDataItemValueAsString(data,indexes) {
// Get the item
var item = getDataItem(data,indexes);
// Return the item as a string
return convertDataItemValueToString(item);
}
/*
Given a JSON data structure and an array of index strings, return an array of the string representation of the keys of the item at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
function getDataItemKeysAsStrings(data,indexes) {
// Get the item
var item = getDataItem(data,indexes);
// Return the item keys as a string
return convertDataItemKeysToStrings(item);
}
/*
Return an array of the string representation of the values of a data item, or "undefined" if the item is undefined
*/
function convertDataItemValueToString(item) {
// Return the item as a string
if(item === undefined) {
return item;
}
if(typeof item === "object") {
return JSON.stringify(item);
}
return item.toString();
}
/*
Return an array of the string representation of the keys of a data item, or "undefined" if the item is undefined
*/
function convertDataItemKeysToStrings(item) {
// Return the item as a string
if(item === undefined) {
return item;
} else if(typeof item === "object") {
if(item === null) {
return [];
}
var results = [];
if($tw.utils.isArray(item)) {
for(var i=0; i<item.length; i++) {
results.push(i.toString());
}
return results;
} else {
$tw.utils.each(Object.keys(item).sort(),function(key) {
results.push(key);
});
return results;
}
}
return [];
}
function getDataItemType(data,indexes) {
// Get the item
var item = getDataItem(data,indexes);
// Return the item type
if(item === undefined) {
return item;
} else if(item === null) {
return "null";
} else if($tw.utils.isArray(item)) {
return "array";
} else if(typeof item === "object") {
return "object";
} else {
return typeof item;
}
}
/*
Given a JSON data structure and an array of index strings, return the value at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
function getDataItem(data,indexes) {
if(indexes.length === 0 || (indexes.length === 1 && indexes[0] === "")) {
return data;
}
// Get the item
var item = data;
for(var i=0; i<indexes.length; i++) {
if(item !== undefined) {
item = item[indexes[i]];
}
}
return item;
}
})();

View File

@ -0,0 +1,95 @@
/*\
title: test-json-filters.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the JSON filters and the format:json operator
\*/
(function(){
/* jslint node: true, browser: true */
/* eslint-env node, browser, jasmine */
/* eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/
/* global $tw, require */
"use strict";
describe("json filter tests", function() {
var wiki = new $tw.Wiki();
var tiddlers = [{
title: "First",
text: '{"a":"one","b":"","c":1.618,"d": {"e": "four","f": ["five","six",true,false,null]}}',
type: "application/json"
},{
title: "Second",
text: '["une","deux","trois"]',
type: "application/json"
},{
title: "Third",
text: "This is not JSON",
type: "text/vnd.tiddlywiki"
}];
wiki.addTiddlers(tiddlers);
it("should support the getindex operator", function() {
expect(wiki.filterTiddlers("[{First}getindex[b]]")).toEqual([]);
});
it("should support the jsonget operator", function() {
expect(wiki.filterTiddlers("[{Third}jsonget[]]")).toEqual(["This is not JSON"]);
expect(wiki.filterTiddlers("[{First}jsonget[]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']);
expect(wiki.filterTiddlers("[{First}jsonget[a]]")).toEqual(["one"]);
expect(wiki.filterTiddlers("[{First}jsonget[b]]")).toEqual([""]);
expect(wiki.filterTiddlers("[{First}jsonget[missing-property]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonget[d]]")).toEqual(['{"e":"four","f":["five","six",true,false,null]}']);
expect(wiki.filterTiddlers("[{First}jsonget[d]jsonget[f]]")).toEqual(['["five","six",true,false,null]']);
expect(wiki.filterTiddlers("[{First}jsonget[d],[e]]")).toEqual(["four"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f]]")).toEqual(['["five","six",true,false,null]']);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[0]]")).toEqual(["five"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[1]]")).toEqual(["six"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[2]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[3]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[4]]")).toEqual(["null"]);
});
it("should support the jsonindexes operator", function() {
expect(wiki.filterTiddlers("[{Second}jsonindexes[]]")).toEqual(["0","1","2"]);
expect(wiki.filterTiddlers("[{First}jsonindexes[]]")).toEqual(["a","b","c","d"]);
expect(wiki.filterTiddlers("[{First}jsonindexes[a]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[b]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d]]")).toEqual(["e","f"]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[e]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f]]")).toEqual(["0","1","2","3","4"]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[0]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[1]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[2]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[3]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[4]]")).toEqual([]);
});
it("should support the jsontype operator", function() {
expect(wiki.filterTiddlers("[{Third}jsontype[]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[]]")).toEqual(["object"]);
expect(wiki.filterTiddlers("[{First}jsontype[a]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[b]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[c]]")).toEqual(["number"]);
expect(wiki.filterTiddlers("[{First}jsontype[d]]")).toEqual(["object"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[e]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f]]")).toEqual(["array"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[0]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[1]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[2]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[3]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[4]]")).toEqual(["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}"]);
expect(wiki.filterTiddlers("[{First}format:json[ ]]")).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}"]);
});
});
})();

View File

@ -1,5 +1,5 @@
created: 20201020102735123
modified: 20210524044020645
modified: 20220611104737314
tags: [[Operator Examples]] [[format Operator]]
title: format Operator (Examples)
type: text/vnd.tiddlywiki
@ -18,9 +18,12 @@ Modified date shown as a relative date:
A tiddler title with spaces formatted as a title list:
<<.operator-example 4 """[[Hello There]format:titlelist[]]""">>
All tiddler titles tagged with <<tag TableOfContents>> formatted as a title list :
All tiddler titles tagged with <<tag TableOfContents>> formatted as a title list:
<<.operator-example 5 """[tag[TableOfContents]format:titlelist[]]""">>
A JSON string formatted as JSON note how the JSON string is normalised to remove the duplicated properties:
<<.operator-example 6 """[[{"one":"first","one":"another","two":"second"}]format:json[]]""">>
<<.tip "To create a string to save a [[title list|Title List]] into a list field, use `format:titlelist[]` with the [[join operator|join Operator]]">>
<<.operator-example 6 """[tag[TableOfContents]format:titlelist[]join[ ]]""">>
For example, to save titles tagged `TableOfContents` to the titles field of the tiddler [[format titlelist test]]:

View File

@ -1,6 +1,6 @@
caption: format
created: 20201020100834443
modified: 20220523075550449
modified: 20220611104737314
op-input: a [[selection of titles|Title Selection]]
op-output: input strings formatted according to the specified suffix <<.place B>>
op-parameter: optional format string for the formats
@ -17,9 +17,10 @@ type: text/vnd.tiddlywiki
The suffix <<.place B>> is one of the following supported string formats:
|!Format |!Description |
|^`date` |The input string is interpreted as a UTC date and displayed according to the DateFormat specified in the optional operator parameter. (Defaults to "YYYY MM DD 0hh:0mm") |
|^`relativedate` |The input string is interpreted as a UTC date and displayed as the interval from the present instant. Any operator parameters are ignored. |
|^`titlelist` |<<.from-version "5.2.0">>The input string wrapped in double square brackets if it contains a space. Appropriate for use in a [[title list|Title List]]. |
|^`date` |The input string is interpreted as a UTC date and displayed according to the DateFormat specified in the optional operator operand. (Defaults to "YYYY MM DD 0hh:0mm") |
|^`json` |<<.from-version "5.2.4">> The input string is interpreted as JSON and displayed with standard formatting. The optional operator operand specifies the number of spaces to use for indenting, or a string to use for indenting. Nothing is returned if the input string is not valid JSON |
|^`relativedate` |The input string is interpreted as a UTC date and displayed as the interval from the present instant. Any operator parameters are ignored |
|^`titlelist` |<<.from-version "5.2.0">> The input string wrapped in double square brackets if it contains a space. Appropriate for use in a [[title list|Title List]]. |
<<.warning """The [[Title List]] format cannot reliably represent items that contain certain specific character sequences such as `]] `. Thus it should not be used where there is a possibility of such sequences occurring.""">>

View File

@ -0,0 +1,86 @@
created: 20220611104737314
modified: 20220611104737314
tags: [[Filter Operators]] [[JSON Operators]]
title: jsonget Operator
caption: jsonget
op-purpose: retrieve the value of a property from JSON strings
op-input: a selection of JSON strings
op-parameter: one or more indexes of the property to retrieve
op-output: the values of each of the retrieved properties
<<.from-version "5.2.4">> See [[JSON in TiddlyWiki]] for background.
The <<.op jsonget>> operator is used to retrieve values from JSON data. See also the following related operators:
* <<.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
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 jsonget>> operator uses multiple operands to specify the indexes of the property to retrieve:
```
[<jsondata>jsonget[a]] --> "one"
[<jsondata>jsonget[d],[e]] --> "four"
[<jsondata>jsonget[d],[f],[0]] --> "five"
```
Indexes can be dynamically composed from variables and transclusions:
```
[<jsondata>jsonget<variable>,{!!field},[0]]
```
Boolean values and null are returned as normal strings. The <<.olink jsontype>> operator can be used to retrieve a string identifying the original type. Thus:
```
[<jsondata>jsontype[a]] --> "string"
[<jsondata>jsontype[d]] --> "object"
[<jsondata>jsontype[d],[f]] --> "array"
[<jsondata>jsontype[d],[f],[2]] --> "boolean"
```
Using the <<.op jsonget>> operator to retrieve an object or an array returns a JSON string of the values. For example:
```
[<jsondata>jsonget[d],[f]] --> `["five","six",true,false,null]`
[<jsondata>jsonget[d],[g]] --> `{"x": "max","y": "may","z": "maize"}`
```
The <<.olink jsonindexes>> operator retrieves the corresponding indexes:
```
[<jsondata>jsonindexes[d],[f]] --> "0", "1", "2", "3", "4"
[<jsondata>jsonindexes[d],[g]] --> "x", "y", "z"
```
A subtlety is that the special case of a single blank operand is used to identify the root object. Thus:
```
[<jsondata>jsonindexes[]] --> "a", "b", "c", "d"
```

View File

@ -0,0 +1,64 @@
created: 20220611104737314
modified: 20220611104737314
tags: [[Filter Operators]] [[JSON Operators]]
title: jsonindexes Operator
caption: jsonindexes
op-purpose: retrieve the value of a property from JSON strings
op-input: a selection of JSON strings
op-parameter: one or more indexes of the property to retrieve
op-output: the values of each of the retrieved properties
<<.from-version "5.2.4">> See [[JSON in TiddlyWiki]] for background.
The <<.op jsonindexes>> operator is used to retrieve the property names of JSON objects or the index names of JSON arrays. 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
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 jsonindexes>> operator uses multiple operands to specify the indexes of the property to retrieve:
```
[<jsondata>jsonindexes[d],[f]] --> "0", "1", "2", "3", "4"
[<jsondata>jsonindexes[d],[g]] --> "x", "y", "z"
```
Indexes can be dynamically composed from variables and transclusions:
```
[<jsondata>jsonindexes<variable>,{!!field}]
```
Retrieving the indexes of JSON properties that are not objects or arrays will return nothing.
A subtlety is that the special case of a single blank operand is used to identify the root object. Thus:
```
[<jsondata>jsonindexes[]] --> "a", "b", "c", "d"
```

View File

@ -0,0 +1,73 @@
created: 20220611104737314
modified: 20220611104737314
tags: [[Filter Operators]] [[JSON Operators]]
title: jsontype Operator
caption: jsontype
op-purpose: retrieve the type of a property from JSON strings
op-input: a selection of JSON strings
op-parameter: one or more indexes of the property whose type is to be retrieved
op-output: the types of each of the retrieved properties
<<.from-version "5.2.4">> See [[JSON in TiddlyWiki]] for background.
The <<.op jsontype>> operator is used to retrieve the type of a property in JSON data. See also the following related operators:
* <<.olink jsonget>> to retrieve the values of a property in JSON data
* <<.olink jsonindexes>> to retrieve the names of the fields of a JSON object, or the indexes of a JSON array
JSON supports the following data types:
* ''string'' - a Unicode string
* ''number'' - a floating point number
* ''boolean'' - Boolean value (true or false)
* ''array'' - an array of values
* ''object'' - an object of name/value pairs
* ''null'' - a special type representing a missing value
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 jsontype>> operator uses multiple operands to specify the indexes of the property whose type is to be retrieved:
```
[<jsondata>jsontype[a]] --> "string"
[<jsondata>jsontype[d]] --> "object"
[<jsondata>jsontype[d],[f]] --> "array"
[<jsondata>jsontype[d],[f],[2]] --> "boolean"
```
Indexes can be dynamically composed from variables and transclusions:
```
[<jsondata>jsontype<variable>,{!!field},[0]]
```
A subtlety is that the special case of a single blank operand is used to identify the root object. Thus:
```
[<jsondata>jsontype[]] --> "object"
```

View File

@ -3,13 +3,121 @@ tags: $:/tags/EditPreview
list-after: $:/core/ui/EditTemplate/body/preview/output
caption: parse tree
\define preview(mode)
<$wikify name="preview-text" text={{!!text}} type={{!!type}} mode="$mode$" output="parsetree">
<pre>
<code>
<$text text=<<preview-text>>/>
</code>
</pre>
\whitespace trim
\procedure preview-node-properties(node)
<$let excludeProperties="text type tag children attributes orderedAttributes">
<$list filter="[<node>jsonindexes[]] -[subfilter<excludeProperties>] +[limit[1]]" variable="ignore">
<table>
<tbody>
<$list filter="[<node>jsonindexes[]] -[subfilter<excludeProperties>] +[sort[]]" variable="index">
<tr>
<td>
<$text text=<<index>>/>
</td>
<td>
<$text text={{{ [<node>jsonget<index>] }}}/>
</td>
</tr>
</$list>
</tbody>
</table>
</$list>
</$let>
\end
\procedure preview-node-attribute-string(attribute)
<$text text={{{ [<attribute>jsonget[value]] }}}/>
\end
\procedure preview-node-attribute-indirect(attribute)
{{<$text text={{{ [<attribute>jsonget[textReference]] }}}/>}}
\end
\procedure preview-node-attribute-macro(attribute)
&lt;&lt;
<$text text={{{ [<attribute>jsonget[value],[name]] }}}/>
<$list filter="[<attribute>jsonindexes[value],[params]]" variable="index">
&nbsp;
<$list filter="[<attribute>jsonget[value],[params],<index>,[name]]" variable="ignore">
<$text text={{{ [<attribute>jsonget[value],[params],<index>,[name]] }}}/>
:
</$list>
<$text text={{{ [<attribute>jsonget[value],[params],<index>,[value]] }}}/>
</$list>
>>
\end
\procedure preview-node-attributes(node)
<$list filter="[<node>jsonindexes[attributes]limit[1]]" variable="ignore">
<table>
<tbody>
<$list filter="[<node>jsonindexes[attributes]sort[]]" variable="index">
<tr>
<td>
<$text text=<<index>>/>
</td>
<td>
<$let type={{{ [<node>jsonget[attributes],<index>,[type]] }}}>
<$transclude $variable={{{ [<type>match[string]then[preview-node-attribute-string]] :else[<type>match[indirect]then[preview-node-attribute-indirect]] :else[<type>match[macro]then[preview-node-attribute-macro]] }}} attribute={{{ [<node>jsonget[attributes],<index>] }}}/>
</$let>
</td>
</tr>
</$list>
</tbody>
</table>
</$list>
\end
\procedure preview-node-children(node)
<div style="padding:4px 4px 0 4px;">
<$transclude $variable="preview-node-properties" node=<<node>>/>
<$transclude $variable="preview-node-attributes" node=<<node>>/>
<$transclude $variable="preview-node-list" nodeList={{{ [<node>jsonget[children]] }}}/>
</div>
\end
\procedure preview-node-title-widget(node)
<div style="border:2px solid red;margin:4px;">
<div style="background:red;color:white;padding:4px;">
<$<$text text={{{ [<node>jsonget[type]] }}}/>>
</div>
<$transclude $variable="preview-node-children" node=<<node>>/>
</div>
\end
\procedure preview-node-title-element(node)
<div style="border:2px solid purple;margin:4px;">
<div style="background:purple;color:white;padding:4px;">
&lt;<$text text={{{ [<node>jsonget[tag]] }}}/>>
</div>
<$transclude $variable="preview-node-children" node=<<node>>/>
</div>
\end
\procedure preview-node-title-text(node)
<div style="border:2px solid green;margin:4px;">
<div style="background:green;color:white;padding:4px;">
<span style="color:#ff0;font-weight:bold;">"</span><span style="white-space:pre-wrap;"><$text text={{{ [<node>jsonget[text]] }}}/></span><span style="color:#ff0;font-weight:bold;">"</span>
</div>
</div>
\end
\procedure preview-node(node)
<$let type={{{ [<node>jsonget[type]] }}}>
<$transclude $variable={{{ [<type>match[element]then[preview-node-title-element]] :else[<type>match[text]then[preview-node-title-text]] :else[[preview-node-title-widget]] }}} node=<<node>>/>
</$let>
\end
\procedure preview-node-list(nodeList)
<$list filter="[<nodeList>jsonindexes[]]" variable="index">
<$transclude $variable="preview-node" node={{{ [<nodeList>jsonget<index>] }}}/>
</$list>
\end
\procedure preview(mode)
<$wikify name="preview-json" text={{!!text}} type={{!!type}} mode=<<mode>> output="parsetree">
<$transclude $variable="preview-node-list" nodeList=<<preview-json>>/>
</$wikify>
\end