1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-10-31 23:26:18 +00:00

Add join attribute to list widget (#7694)

* Add join attribute to list widget

* Use new join attribute in HTML saving templates

This simplifies the logic involved in saving tiddlers in JSON format
into TW html files, and should also slightly speed up the saving process
depending on how often that list widget gets refreshed.

* Unit tests for list widget's new join attribute

* Add `<$list-join>` widget

Allows specifying complicated join text more easily than an attribute
This commit is contained in:
Robin Munn 2023-11-25 16:35:05 +07:00 committed by GitHub
parent 22f9df5850
commit 64812f5c06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 237 additions and 25 deletions

View File

@ -50,8 +50,8 @@ ListWidget.prototype.render = function(parent,nextSibling) {
$tw.modules.applyMethods("storyview",this.storyViews); $tw.modules.applyMethods("storyview",this.storyViews);
} }
this.parentDomNode = parent; this.parentDomNode = parent;
this.computeAttributes(); var changedAttributes = this.computeAttributes();
this.execute(); this.execute(changedAttributes);
this.renderChildren(parent,nextSibling); this.renderChildren(parent,nextSibling);
// Construct the storyview // Construct the storyview
var StoryView = this.storyViews[this.storyViewName]; var StoryView = this.storyViews[this.storyViewName];
@ -71,7 +71,7 @@ ListWidget.prototype.render = function(parent,nextSibling) {
/* /*
Compute the internal state of the widget Compute the internal state of the widget
*/ */
ListWidget.prototype.execute = function() { ListWidget.prototype.execute = function(changedAttributes) {
var self = this; var self = this;
// Get our attributes // Get our attributes
this.template = this.getAttribute("template"); this.template = this.getAttribute("template");
@ -80,6 +80,10 @@ ListWidget.prototype.execute = function() {
this.counterName = this.getAttribute("counter"); this.counterName = this.getAttribute("counter");
this.storyViewName = this.getAttribute("storyview"); this.storyViewName = this.getAttribute("storyview");
this.historyTitle = this.getAttribute("history"); this.historyTitle = this.getAttribute("history");
// Create join template only if needed
if(this.join === undefined || (changedAttributes && changedAttributes.join)) {
this.join = this.makeJoinTemplate();
}
// Compose the list elements // Compose the list elements
this.list = this.getTiddlerList(); this.list = this.getTiddlerList();
var members = [], var members = [],
@ -102,6 +106,7 @@ ListWidget.prototype.findExplicitTemplates = function() {
var self = this; var self = this;
this.explicitListTemplate = null; this.explicitListTemplate = null;
this.explicitEmptyTemplate = null; this.explicitEmptyTemplate = null;
this.explicitJoinTemplate = null;
this.hasTemplateInBody = false; this.hasTemplateInBody = false;
var searchChildren = function(childNodes) { var searchChildren = function(childNodes) {
$tw.utils.each(childNodes,function(node) { $tw.utils.each(childNodes,function(node) {
@ -109,6 +114,8 @@ ListWidget.prototype.findExplicitTemplates = function() {
self.explicitListTemplate = node.children; self.explicitListTemplate = node.children;
} else if(node.type === "list-empty") { } else if(node.type === "list-empty") {
self.explicitEmptyTemplate = node.children; self.explicitEmptyTemplate = node.children;
} else if(node.type === "list-join") {
self.explicitJoinTemplate = node.children;
} else if(node.type === "element" && node.tag === "p") { } else if(node.type === "element" && node.tag === "p") {
searchChildren(node.children); searchChildren(node.children);
} else { } else {
@ -152,6 +159,24 @@ ListWidget.prototype.getEmptyMessage = function() {
} }
}; };
/*
Compose the template for a join between list items
*/
ListWidget.prototype.makeJoinTemplate = function() {
var parser,
join = this.getAttribute("join","");
if(join) {
parser = this.wiki.parseText("text/vnd.tiddlywiki",join,{parseAsInline:true})
if(parser) {
return parser.tree;
} else {
return [];
}
} else {
return this.explicitJoinTemplate; // May be null, and that's fine
}
};
/* /*
Compose the template for a list item Compose the template for a list item
*/ */
@ -160,6 +185,7 @@ ListWidget.prototype.makeItemTemplate = function(title,index) {
var tiddler = this.wiki.getTiddler(title), var tiddler = this.wiki.getTiddler(title),
isDraft = tiddler && tiddler.hasField("draft.of"), isDraft = tiddler && tiddler.hasField("draft.of"),
template = this.template, template = this.template,
join = this.join,
templateTree; templateTree;
if(isDraft && this.editTemplate) { if(isDraft && this.editTemplate) {
template = this.editTemplate; template = this.editTemplate;
@ -185,12 +211,12 @@ ListWidget.prototype.makeItemTemplate = function(title,index) {
} }
} }
// Return the list item // Return the list item
var parseTreeNode = {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree}; var parseTreeNode = {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree, join: join};
parseTreeNode.isLast = index === this.list.length - 1;
if(this.counterName) { if(this.counterName) {
parseTreeNode.counter = (index + 1).toString(); parseTreeNode.counter = (index + 1).toString();
parseTreeNode.counterName = this.counterName; parseTreeNode.counterName = this.counterName;
parseTreeNode.isFirst = index === 0; parseTreeNode.isFirst = index === 0;
parseTreeNode.isLast = index === this.list.length - 1;
} }
return parseTreeNode; return parseTreeNode;
}; };
@ -206,7 +232,7 @@ ListWidget.prototype.refresh = function(changedTiddlers) {
this.storyview.refreshStart(changedTiddlers,changedAttributes); this.storyview.refreshStart(changedTiddlers,changedAttributes);
} }
// Completely refresh if any of our attributes have changed // Completely refresh if any of our attributes have changed
if(changedAttributes.filter || changedAttributes.variable || changedAttributes.counter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) { if(changedAttributes.filter || changedAttributes.variable || changedAttributes.counter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.join || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) {
this.refreshSelf(); this.refreshSelf();
result = true; result = true;
} else { } else {
@ -310,10 +336,29 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) {
} }
} else { } else {
// Cycle through the list, inserting and removing list items as needed // Cycle through the list, inserting and removing list items as needed
var mustRecreateLastItem = false;
if(this.join && this.join.length) {
if(this.children.length !== this.list.length) {
mustRecreateLastItem = true;
} else if(prevList[prevList.length-1] !== this.list[this.list.length-1]) {
mustRecreateLastItem = true;
}
}
var isLast = false, wasLast = false;
for(t=0; t<this.list.length; t++) { for(t=0; t<this.list.length; t++) {
isLast = t === this.list.length-1;
var index = this.findListItem(t,this.list[t]); var index = this.findListItem(t,this.list[t]);
wasLast = index === this.children.length-1;
if(wasLast && (index !== t || this.children.length !== this.list.length)) {
mustRecreateLastItem = !!(this.join && this.join.length);
}
if(index === undefined) { if(index === undefined) {
// The list item must be inserted // The list item must be inserted
if(isLast && mustRecreateLastItem && t>0) {
// First re-create previosly-last item that will no longer be last
this.removeListItem(t-1);
this.insertListItem(t-1,this.list[t-1]);
}
this.insertListItem(t,this.list[t]); this.insertListItem(t,this.list[t]);
hasRefreshed = true; hasRefreshed = true;
} else { } else {
@ -322,9 +367,15 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) {
this.removeListItem(n); this.removeListItem(n);
hasRefreshed = true; hasRefreshed = true;
} }
// Refresh the item we're reusing // Refresh the item we're reusing, or recreate if necessary
var refreshed = this.children[t].refresh(changedTiddlers); if(mustRecreateLastItem && (isLast || wasLast)) {
hasRefreshed = hasRefreshed || refreshed; this.removeListItem(t);
this.insertListItem(t,this.list[t]);
hasRefreshed = true;
} else {
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
}
} }
} }
} }
@ -414,8 +465,17 @@ ListItemWidget.prototype.execute = function() {
this.setVariable(this.parseTreeNode.counterName + "-first",this.parseTreeNode.isFirst ? "yes" : "no"); this.setVariable(this.parseTreeNode.counterName + "-first",this.parseTreeNode.isFirst ? "yes" : "no");
this.setVariable(this.parseTreeNode.counterName + "-last",this.parseTreeNode.isLast ? "yes" : "no"); this.setVariable(this.parseTreeNode.counterName + "-last",this.parseTreeNode.isLast ? "yes" : "no");
} }
// Add join if needed
var children = this.parseTreeNode.children,
join = this.parseTreeNode.join;
if(join && join.length && !this.parseTreeNode.isLast) {
children = children.slice(0);
$tw.utils.each(join,function(joinNode) {
children.push(joinNode);
})
}
// Construct the child widgets // Construct the child widgets
this.makeChildWidgets(); this.makeChildWidgets(children);
}; };
/* /*
@ -450,4 +510,14 @@ ListEmptyWidget.prototype.refresh = function() { return false; }
exports["list-empty"] = ListEmptyWidget; exports["list-empty"] = ListEmptyWidget;
var ListJoinWidget = function(parseTreeNode,options) {
// Main initialisation inherited from widget.js
this.initialise(parseTreeNode,options);
};
ListJoinWidget.prototype = new Widget();
ListJoinWidget.prototype.render = function() {}
ListJoinWidget.prototype.refresh = function() { return false; }
exports["list-join"] = ListJoinWidget;
})(); })();

View File

@ -1,4 +1,3 @@
title: $:/core/templates/html-json-skinny-tiddler title: $:/core/templates/html-json-skinny-tiddler
<$list filter="[<numTiddlers>compare:number:gteq[1]] ~[<counter>!match[1]]">`,`<$text text=<<newline>>/></$list>
<$jsontiddler tiddler=<<currentTiddler>> exclude="text" escapeUnsafeScriptChars="yes"/> <$jsontiddler tiddler=<<currentTiddler>> exclude="text" escapeUnsafeScriptChars="yes"/>

View File

@ -1,3 +1,3 @@
title: $:/core/templates/html-json-tiddler title: $:/core/templates/html-json-tiddler
<$list filter="[<counter>!match[1]]">`,`<$text text=<<newline>>/></$list><$jsontiddler tiddler=<<currentTiddler>> escapeUnsafeScriptChars="yes"/> <$jsontiddler tiddler=<<currentTiddler>> escapeUnsafeScriptChars="yes"/>

View File

@ -6,14 +6,14 @@ title: $:/core/templates/store.area.template.html
<$list filter="[[storeAreaFormat]is[variable]getvariable[]else[json]match[json]]"> <$list filter="[[storeAreaFormat]is[variable]getvariable[]else[json]match[json]]">
<!-- New-style JSON store area, with an old-style store area for compatibility with v5.1.x tooling --> <!-- New-style JSON store area, with an old-style store area for compatibility with v5.1.x tooling -->
`<script class="tiddlywiki-tiddler-store" type="application/json">[` `<script class="tiddlywiki-tiddler-store" type="application/json">[`
<$vars newline={{{ [charcode[10]] }}}> <$let newline={{{ [charcode[10]] }}} join=`,$(newline)$`>
<$text text=<<newline>>/> <$text text=<<newline>>/>
<$list filter=<<saveTiddlerFilter>> counter="counter" template="$:/core/templates/html-json-tiddler"/> <$list filter=<<saveTiddlerFilter>> join=<<join>> template="$:/core/templates/html-json-tiddler"/>
<$vars numTiddlers={{{ [subfilter<saveTiddlerFilter>count[]] }}}> <$vars numTiddlers={{{ [subfilter<saveTiddlerFilter>count[]] }}}>
<$list filter={{{ [<skinnySaveTiddlerFilter>] }}} counter="counter" template="$:/core/templates/html-json-skinny-tiddler"/> <$list filter={{{ [<skinnySaveTiddlerFilter>] }}} join=<<join>> template="$:/core/templates/html-json-skinny-tiddler"/>
</$vars> </$vars>
<$text text=<<newline>>/> <$text text=<<newline>>/>
</$vars> </$let>
`]</script>` `]</script>`
`<div id="storeArea" style="display:none;">` `<div id="storeArea" style="display:none;">`
`</div>` `</div>`

View File

@ -4,11 +4,8 @@ tags: $:/tags/ViewTemplate
\whitespace trim \whitespace trim
<$reveal type="nomatch" stateTitle=<<folded-state>> text="hide" tag="div" retain="yes" animate="yes"> <$reveal type="nomatch" stateTitle=<<folded-state>> text="hide" tag="div" retain="yes" animate="yes">
<div class="tc-subtitle"> <div class="tc-subtitle">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewTemplate/Subtitle]!has[draft.of]]" variable="subtitleTiddler" counter="indexSubtitleTiddler"> <$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewTemplate/Subtitle]!has[draft.of]]" variable="subtitleTiddler">
<$list filter="[<indexSubtitleTiddler-first>match[no]]" variable="ignore"> <$transclude tiddler=<<subtitleTiddler>> mode="inline"/><$list-join>&nbsp;</$list-join>
&nbsp;
</$list>
<$transclude tiddler=<<subtitleTiddler>> mode="inline"/>
</$list> </$list>
</div> </div>
</$reveal> </$reveal>

View File

@ -41,6 +41,33 @@ description: Under development
Note that the <<.attr "emptyMessage">> and <<.attr "template">> attributes take precedence if they are present. Note that the <<.attr "emptyMessage">> and <<.attr "template">> attributes take precedence if they are present.
!! Joiners for the ListWidget
<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7694">> a <<.attr "join">> attribute to the <<.wid "ListWidget">> widget to insert a short piece of text between list items. This is both easier to use and faster than using the <<.attr "counter">> attribute for the same purpose. So if your list looked like this:
```
<$list filter=<<filter>> counter="counter" variable="item">
<$text text=<<item>>/><$list filter="[<counter-last>match[no]]" variable="ignore"><$text text=", "/></$list>
</$list>
```
You can replace it with:
```
<$list filter=<<filter>> variable="item" join=", ">
<$text text=<<item>>/>
</$list>
```
If the joiner text that you need is long and awkward to write in an attribute, you can use the new `<$list-join>` widget. Like `<$list-template>` and `<$list-empty>`, it must be an immediate child of the <<.wid "ListWidget">>:
```
<$list filter=<<filter>> variable="item">
<$text text=<<item>>/>
<$list-join>, and <em>also</em> let's not forget </$list-join>
</$list>
```
!! jsonset operator !! jsonset operator
<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7742">> [[jsonset Operator]] for setting values within JSON objects <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7742">> [[jsonset Operator]] for setting values within JSON objects

View File

@ -0,0 +1,30 @@
title: ListWidget/WithJoinTemplate
description: List widget with join template and $list-empty
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
+
title: Output
\whitespace trim
\procedure test(filter)
<$list filter=<<filter>>>
Item:<<currentTiddler>>
<$list-empty>
None!
</$list-empty>
<$list-join>,</$list-join>
</$list>
\end
<<test "1 2 3">>
<<test "">>
+
title: ExpectedResult
<p>Item:1,Item:2,Item:3</p><p>None!</p>

View File

@ -0,0 +1,32 @@
title: ListWidget/WithJoinTemplateInBlockMode
description: List widget with join template and $list-empty in block mode
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
+
title: Output
\whitespace trim
\procedure test(filter)
<$list filter=<<filter>>>
Item:<<currentTiddler>>
<$list-empty>
None!
</$list-empty>
<$list-join><br></$list-join>
</$list>
\end
<<test "1 2 3">>
<<test "">>
+
title: ExpectedResult
comment: I wish there was a good way to get rid of these extraneous paragraph elements
<p>Item:1</p><p></p><p></p><br><p>Item:2</p><p></p><p></p><br><p>Item:3</p><p></p><p></p>None!

View File

@ -527,6 +527,45 @@ describe("Widget module", function() {
expect(wrapper.children[0].children[15].sequenceNumber).toBe(53); expect(wrapper.children[0].children[15].sequenceNumber).toBe(53);
}); });
var testListJoin = function(oldList, newList) {
return function() {
var wiki = new $tw.Wiki();
// Add some tiddlers
wiki.addTiddler({title: "Numbers", text: "", list: oldList});
var text = "<$list filter='[list[Numbers]]' variable='item' join=', '><<item>></$list>";
var widgetNode = createWidgetNode(parseText(text,wiki),wiki);
// Render the widget node to the DOM
var wrapper = renderWidgetNode(widgetNode);
// Test the rendering
expect(wrapper.innerHTML).toBe("<p>" + oldList.split(' ').join(', ') + "</p>");
// Change the list and ensure new rendering is still right
wiki.addTiddler({title: "Numbers", text: "", list: newList});
refreshWidgetNode(widgetNode,wrapper,["Numbers"]);
expect(wrapper.innerHTML).toBe("<p>" + newList.split(' ').join(', ') + "</p>");
}
}
it("the list widget with join should update correctly when empty list gets one item", testListJoin("", "1"));
it("the list widget with join should update correctly when empty list gets two items", testListJoin("", "1 2"));
it("the list widget with join should update correctly when single-item list is appended to", testListJoin("1", "1 2"));
it("the list widget with join should update correctly when single-item list is prepended to", testListJoin("1", "2 1"));
it("the list widget with join should update correctly when list is appended", testListJoin("1 2 3 4", "1 2 3 4 5"));
it("the list widget with join should update correctly when last item is removed", testListJoin("1 2 3 4", "1 2 3"));
it("the list widget with join should update correctly when first item is inserted", testListJoin("1 2 3 4", "0 1 2 3 4"));
it("the list widget with join should update correctly when first item is removed", testListJoin("1 2 3 4", "2 3 4"));
it("the list widget with join should update correctly when first two items are swapped", testListJoin("1 2 3 4", "2 1 3 4"));
it("the list widget with join should update correctly when last two items are swapped", testListJoin("1 2 3 4", "1 2 4 3"));
it("the list widget with join should update correctly when last item is moved to the front", testListJoin("1 2 3 4", "4 1 2 3"));
it("the list widget with join should update correctly when last item is moved to the middle", testListJoin("1 2 3 4", "1 4 2 3"));
it("the list widget with join should update correctly when first item is moved to the back", testListJoin("1 2 3 4", "2 3 4 1"));
it("the list widget with join should update correctly when middle item is moved to the back", testListJoin("1 2 3 4", "1 3 4 2"));
it("the list widget with join should update correctly when the last item disappears at the same time as other edits 1", testListJoin("1 3 4", "1 2 3"));
it("the list widget with join should update correctly when the last item disappears at the same time as other edits 2", testListJoin("1 3 4", "1 3 2"));
it("the list widget with join should update correctly when the last item disappears at the same time as other edits 3", testListJoin("1 3 4", "2 1 3"));
it("the list widget with join should update correctly when the last item disappears at the same time as other edits 4", testListJoin("1 3 4", "2 3 1"));
it("the list widget with join should update correctly when the last item disappears at the same time as other edits 5", testListJoin("1 3 4", "3 1 2"));
it("the list widget with join should update correctly when the last item disappears at the same time as other edits 6", testListJoin("1 3 4", "3 2 1"));
var testCounterLast = function(oldList, newList) { var testCounterLast = function(oldList, newList) {
return function() { return function() {
var wiki = new $tw.Wiki(); var wiki = new $tw.Wiki();

View File

@ -84,6 +84,7 @@ The action of the list widget depends on the results of the filter combined with
|limit |<<.from-version "5.3.2">> Optional numeric limit for the number of results that are returned. Negative values will return the results from the end of the list | |limit |<<.from-version "5.3.2">> Optional numeric limit for the number of results that are returned. Negative values will return the results from the end of the list |
|template |The title of a template tiddler for transcluding each tiddler in the list. When no template is specified, the body of the ListWidget serves as the item template. With no body, a simple link to the tiddler is returned. | |template |The title of a template tiddler for transcluding each tiddler in the list. When no template is specified, the body of the ListWidget serves as the item template. With no body, a simple link to the tiddler is returned. |
|editTemplate |An alternative template to use for [[DraftTiddlers|DraftMechanism]] in edit mode | |editTemplate |An alternative template to use for [[DraftTiddlers|DraftMechanism]] in edit mode |
|join |<<.from-version "5.3.2">> Text to include between each list item |
|variable |The name for a [[variable|Variables]] in which the title of each listed tiddler is stored. Defaults to ''currentTiddler'' | |variable |The name for a [[variable|Variables]] in which the title of each listed tiddler is stored. Defaults to ''currentTiddler'' |
|counter |<<.from-version "5.2.0">> Optional name for a [[variable|Variables]] in which the 1-based numeric index of each listed tiddler is stored (see below) | |counter |<<.from-version "5.2.0">> Optional name for a [[variable|Variables]] in which the 1-based numeric index of each listed tiddler is stored (see below) |
|emptyMessage |Message to be displayed when the list is empty | |emptyMessage |Message to be displayed when the list is empty |
@ -120,10 +121,29 @@ Displays as:
</$list> </$list>
<<< <<<
Note that using the `counter` attribute can reduce performance when working with list items that dynamically reorder or update themselves. The best advice is only to use it when it is really necessary: to obtain a numeric index, or to detect the first or last entries in the list. Note that using the `counter` attribute can reduce performance when working with list items that dynamically reorder or update themselves. The best advice is only to use it when it is really necessary: to obtain a numeric index, or to detect the first or last entries in the list. Note that if you are only using it to insert something (like a comma) between list items, the `join` attribute performs much better and you should use it instead of `counter`.
Setting `counter="transclusion"` is a handy way to make child elements for each list element be identified as unique. A common use case are multiple [[tag macros|tag Macro]] for the same tag generated by a list widget. Refer to [[tag macro examples|tag Macro (Examples)]] for more details. Setting `counter="transclusion"` is a handy way to make child elements for each list element be identified as unique. A common use case are multiple [[tag macros|tag Macro]] for the same tag generated by a list widget. Refer to [[tag macro examples|tag Macro (Examples)]] for more details.
!! `join` attribute
<<.from-version "5.3.2">> The optional `join` attribute allow you to insert some [[WikiText]] between each list item without needing to use the `counter` attribute, which can become quite slow if the list is updated frequently.
<<.from-version "5.3.2">> If the widget `<$list-join>` is found as an immediate child of the <<.wid "ListWidget">> widget then the content of that widget is used as the "join" template, included between two list items. Note that the <<.attr "join">> attribute takes precedence if it is present.
For example:
```
<$list filter="[tag[About]sort[title]]" join=", " variable="item"><<item>></$list>
```
Displays as:
<<<
<$list filter="[tag[About]sort[title]]" join=", " variable="item"><<item>></$list>
<<<
!! Edit mode !! Edit mode
The `<$list>` widget can optionally render draft tiddlers through a different template to handle editing, see DraftMechanism. The `<$list>` widget can optionally render draft tiddlers through a different template to handle editing, see DraftMechanism.

View File

@ -1,4 +1,3 @@
title: $:/core/templates/html-json-skinny-tiddler title: $:/core/templates/html-json-skinny-tiddler
<$list filter="[<numTiddlers>compare:number:gteq[1]] ~[<counter>!match[1]]">`,`<$text text=<<newline>>/></$list>
<$jsontiddler tiddler=<<currentTiddler>> exclude="text" escapeUnsafeScriptChars="yes" $revision=<<changecount>> $bag="default" $_is_skinny=""/> <$jsontiddler tiddler=<<currentTiddler>> exclude="text" escapeUnsafeScriptChars="yes" $revision=<<changecount>> $bag="default" $_is_skinny=""/>

View File

@ -1,4 +1,3 @@
title: $:/core/templates/html-json-tiddler title: $:/core/templates/html-json-tiddler
<$list filter="[<counter>!match[1]]">`,`<$text text=<<newline>>/></$list> <$jsontiddler tiddler=<<currentTiddler>> escapeUnsafeScriptChars="yes" $revision=<<changecount>> $bag="default"/>
<$jsontiddler tiddler=<<currentTiddler>> escapeUnsafeScriptChars="yes" $revision=<<changecount>> $bag="default">/>