From 2f8d53d3f7d05bef071769d05d981cfc6b3ba1f6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 19 Jan 2026 11:30:50 +0000 Subject: [PATCH 01/16] Allow default filter run prefix to be specified for filter evaluation --- core/modules/filters.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index 7c8cb21ba71..57009afad45 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -230,8 +230,8 @@ exports.getFilterRunPrefixes = function() { return this.filterRunPrefixes; } -exports.filterTiddlers = function(filterString,widget,source) { - var fn = this.compileFilter(filterString); +exports.filterTiddlers = function(filterString,widget,source,options) { + var fn = this.compileFilter(filterString,options); try { const fnResult = fn.call(this,source,widget); return fnResult; @@ -244,8 +244,11 @@ exports.filterTiddlers = function(filterString,widget,source) { Compile a filter into a function with the signature fn(source,widget) where: source: an iterator function for the source tiddlers, called source(iterator), where iterator is called as iterator(tiddler,title) widget: an optional widget node for retrieving the current tiddler etc. +options: optional hashmap of options + options.defaultFilterRunPrefix: the default filter run prefix to use when none is specified */ -exports.compileFilter = function(filterString) { +exports.compileFilter = function(filterString,options) { + var defaultFilterRunPrefix = options?.defaultFilterRunPrefix || "or"; if(!this.filterCache) { this.filterCache = Object.create(null); this.filterCacheCount = 0; @@ -354,7 +357,7 @@ exports.compileFilter = function(filterString) { var options = {wiki: self, suffixes: operation.suffixes || []}; switch(operation.prefix || "") { case "": // No prefix means that the operation is unioned into the result - return filterRunPrefixes["or"](operationSubFunction, options); + return filterRunPrefixes[defaultFilterRunPrefix](operationSubFunction, options); case "=": // The results of the operation are pushed into the result without deduplication return filterRunPrefixes["all"](operationSubFunction, options); case "-": // The results of this operation are removed from the main result From 9b56734451ff38b453eecb1458a9a57eb7229b3e Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 19 Jan 2026 11:31:09 +0000 Subject: [PATCH 02/16] Extend subfilter operator with suffix for specifying the default filter run prefix --- core/modules/filters/subfilter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/modules/filters/subfilter.js b/core/modules/filters/subfilter.js index 5e35f7cd178..c7853798e87 100644 --- a/core/modules/filters/subfilter.js +++ b/core/modules/filters/subfilter.js @@ -13,7 +13,9 @@ Filter operator returning its operand evaluated as a filter Export our filter function */ exports.subfilter = function(source,operator,options) { - var list = options.wiki.filterTiddlers(operator.operand,options.widget,source); + var suffixes = operator.suffixes || [], + defaultFilterRunPrefix = (suffixes[0] || [])[0] || "or"; + var list = options.wiki.filterTiddlers(operator.operand,options.widget,source,{defaultFilterRunPrefix}); if(operator.prefix === "!") { var results = []; source(function(tiddler,title) { From 2a1c607450a97b5dbb62e62bb0ef25b4f8258738 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 19 Jan 2026 21:23:43 +0000 Subject: [PATCH 03/16] Update filter operator to support specifying a default filter run prefix --- core/modules/filters/filter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/modules/filters/filter.js b/core/modules/filters/filter.js index 466d43f8b31..36e48c65860 100644 --- a/core/modules/filters/filter.js +++ b/core/modules/filters/filter.js @@ -13,7 +13,9 @@ Filter operator returning those input titles that pass a subfilter Export our filter function */ exports.filter = function(source,operator,options) { - var filterFn = options.wiki.compileFilter(operator.operand), + var suffixes = operator.suffixes || [], + defaultFilterRunPrefix = (suffixes[0] || [])[0] || "or", + filterFn = options.wiki.compileFilter(operator.operand,{defaultFilterRunPrefix}), results = [], target = operator.prefix !== "!"; source(function(tiddler,title) { From f3e9beb1e138009e142a9fc42a523017a85627c8 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 09:58:10 +0000 Subject: [PATCH 04/16] Comment update --- core/modules/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index 57009afad45..ec48b2a779f 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -241,7 +241,7 @@ exports.filterTiddlers = function(filterString,widget,source,options) { }; /* -Compile a filter into a function with the signature fn(source,widget) where: +Compile a filter into a function with the signature fn(source,widget,options) where: source: an iterator function for the source tiddlers, called source(iterator), where iterator is called as iterator(tiddler,title) widget: an optional widget node for retrieving the current tiddler etc. options: optional hashmap of options From beb6839a35a939c33616c2bac8c1747bcd3c796e Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 12:01:12 +0000 Subject: [PATCH 05/16] Update filter caching to take into account the default filter run prefix --- core/modules/filters.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index ec48b2a779f..29a6683e028 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -249,12 +249,13 @@ options: optional hashmap of options */ exports.compileFilter = function(filterString,options) { var defaultFilterRunPrefix = options?.defaultFilterRunPrefix || "or"; + var cacheKey = filterString + '|' + defaultFilterRunPrefix; if(!this.filterCache) { this.filterCache = Object.create(null); this.filterCacheCount = 0; } - if(this.filterCache[filterString] !== undefined) { - return this.filterCache[filterString]; + if(this.filterCache[cacheKey] !== undefined) { + return this.filterCache[cacheKey]; } var filterParseTree; try { @@ -415,7 +416,7 @@ exports.compileFilter = function(filterString,options) { this.filterCache = Object.create(null); this.filterCacheCount = 0; } - this.filterCache[filterString] = fnMeasured; + this.filterCache[cacheKey] = fnMeasured; this.filterCacheCount++; return fnMeasured; }; From b6859c5a2d22252d8143de847d271522fd98c936 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 12:10:47 +0000 Subject: [PATCH 06/16] Add some tests --- .../tiddlers/tests/data/filters/Filter.tid | 22 +++++++++++++++++++ .../tiddlers/tests/data/filters/Subfilter.tid | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 editions/test/tiddlers/tests/data/filters/Filter.tid create mode 100644 editions/test/tiddlers/tests/data/filters/Subfilter.tid diff --git a/editions/test/tiddlers/tests/data/filters/Filter.tid b/editions/test/tiddlers/tests/data/filters/Filter.tid new file mode 100644 index 00000000000..3c0230eed7e --- /dev/null +++ b/editions/test/tiddlers/tests/data/filters/Filter.tid @@ -0,0 +1,22 @@ +title: Filters/Filter +description: Test filter operator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +\procedure test-filter() 1 1 +[join[X]] +[!match] + +(<$text text={{{ [filter] +[join[ ]] }}}/>) + +(<$text text={{{ [filter:all] +[join[ ]] }}}/>) + ++ +title: 1X1 + ++ +title: ExpectedResult + +

($:/core 1X1 ExpectedResult Output)

($:/core ExpectedResult Output)

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/filters/Subfilter.tid b/editions/test/tiddlers/tests/data/filters/Subfilter.tid new file mode 100644 index 00000000000..238dfe88017 --- /dev/null +++ b/editions/test/tiddlers/tests/data/filters/Subfilter.tid @@ -0,0 +1,20 @@ +title: Filters/Subfilter +description: Test subfilter operator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +\procedure test-data() 1 2 1 3 3 4 + +(<$text text={{{ [subfilter] +[join[ ]] }}}/>) + +(<$text text={{{ [subfilter:all] +[join[ ]] }}}/>) + + ++ +title: ExpectedResult + +

(2 1 3 4)

(1 2 1 3 3 4)

\ No newline at end of file From 8276f66edf8bbb4da9a518691ebf5005ef1ad0c5 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 14:49:07 +0000 Subject: [PATCH 07/16] Add filter run pragma for setting default filter run prefix --- core/modules/filters.js | 98 ++++++++++++------- core/modules/filters/filter.js | 2 +- core/modules/filters/subfilter.js | 2 +- .../filters/DefaultFilterRunPrefixPragma.tid | 19 ++++ 4 files changed, 82 insertions(+), 39 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid diff --git a/core/modules/filters.js b/core/modules/filters.js index 29a6683e028..ef9e4a64d62 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -146,14 +146,16 @@ exports.parseFilter = function(filterString) { match; var whitespaceRegExp = /(\s+)/mg, // Groups: - // 1 - entire filter run prefix - // 2 - filter run prefix itself - // 3 - filter run prefix suffixes - // 4 - opening square bracket following filter run prefix - // 5 - double quoted string following filter run prefix - // 6 - single quoted string following filter run prefix - // 7 - anything except for whitespace and square brackets - operandRegExp = /((?:\+|\-|~|(?:=>?)|\:(\w+)(?:\:([\w\:, ]*))?)?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+))/mg; + // 1 - pragma + // 2 - pragma suffix + // 3 - entire filter run prefix + // 4 - filter run prefix name + // 5 - filter run prefix suffixes + // 6 - opening square bracket following filter run prefix + // 7 - double quoted string following filter run prefix + // 8 - single quoted string following filter run prefix + // 9 - anything except for whitespace and square brackets + operandRegExp = /(?:::(\w+)(?:\:(\w+))?(?=\s|$)|((?:\+|\-|~|(?:=>?)|:(\w+)(?:\:([\w\:, ]*))?)?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+)))/mg; while(p < filterString.length) { // Skip any whitespace whitespaceRegExp.lastIndex = p; @@ -170,18 +172,23 @@ exports.parseFilter = function(filterString) { }; match = operandRegExp.exec(filterString); if(match && match.index === p) { - // If there is a filter run prefix if(match[1]) { - operation.prefix = match[1]; + // If there is a filter run prefix + operation.pragma = match[1]; + operation.suffix = match[2]; + p = p + operation.pragma.length; + } else if(match[3]) { + // If there is a filter run prefix + operation.prefix = match[3]; p = p + operation.prefix.length; // Name for named prefixes - if(match[2]) { - operation.namedPrefix = match[2]; + if(match[4]) { + operation.namedPrefix = match[4]; } // Suffixes for filter run prefix - if(match[3]) { + if(match[5]) { operation.suffixes = []; - $tw.utils.each(match[3].split(":"),function(subsuffix) { + $tw.utils.each(match[5].split(":"),function(subsuffix) { operation.suffixes.push([]); $tw.utils.each(subsuffix.split(","),function(entry) { entry = $tw.utils.trim(entry); @@ -193,7 +200,7 @@ exports.parseFilter = function(filterString) { } } // Opening square bracket - if(match[4]) { + if(match[6]) { p = parseFilterOperation(operation.operators,filterString,p); } else { p = match.index + match[0].length; @@ -203,9 +210,9 @@ exports.parseFilter = function(filterString) { p = parseFilterOperation(operation.operators,filterString,p); } // Quoted strings and unquoted title - if(match[5] || match[6] || match[7]) { // Double quoted string, single quoted string or unquoted title + if(match[7] || match[8] || match[9]) { // Double quoted string, single quoted string or unquoted title operation.operators.push( - {operator: "title", operands: [{text: match[5] || match[6] || match[7]}]} + {operator: "title", operands: [{text: match[7] || match[8] || match[9]}]} ); } results.push(operation); @@ -334,7 +341,8 @@ exports.compileFilter = function(filterString,options) { regexp: operator.regexp },{ wiki: self, - widget: widget + widget: widget, + defaultFilterRunPrefix: defaultFilterRunPrefix }); if($tw.utils.isArray(results)) { accumulator = self.makeTiddlerIterator(results); @@ -355,29 +363,45 @@ exports.compileFilter = function(filterString,options) { var filterRunPrefixes = self.getFilterRunPrefixes(); // Wrap the operator functions in a wrapper function that depends on the prefix operationFunctions.push((function() { - var options = {wiki: self, suffixes: operation.suffixes || []}; - switch(operation.prefix || "") { - case "": // No prefix means that the operation is unioned into the result - return filterRunPrefixes[defaultFilterRunPrefix](operationSubFunction, options); - case "=": // The results of the operation are pushed into the result without deduplication - return filterRunPrefixes["all"](operationSubFunction, options); - case "-": // The results of this operation are removed from the main result - return filterRunPrefixes["except"](operationSubFunction, options); - case "+": // This operation is applied to the main results so far - return filterRunPrefixes["and"](operationSubFunction, options); - case "~": // This operation is unioned into the result only if the main result so far is empty - return filterRunPrefixes["else"](operationSubFunction, options); - case "=>": // This operation is applied to the main results so far, and the results are assigned to a variable - return filterRunPrefixes["let"](operationSubFunction, options); - default: - if(operation.namedPrefix && filterRunPrefixes[operation.namedPrefix]) { - return filterRunPrefixes[operation.namedPrefix](operationSubFunction, options); - } else { + if(operation.pragma) { + switch(operation.pragma) { + case "defaultprefix": + defaultFilterRunPrefix = operation.suffix || "or"; + break; + default: return function(results,source,widget) { results.clear(); results.push($tw.language.getString("Error/FilterRunPrefix")); }; - } + } + return function(results,source,widget) { + // Dummy response + }; + } else { + var options = {wiki: self, suffixes: operation.suffixes || []}; + switch(operation.prefix || "") { + case "": // Use the default filter run prefix if none is specified + return filterRunPrefixes[defaultFilterRunPrefix](operationSubFunction, options); + case "=": // The results of the operation are pushed into the result without deduplication + return filterRunPrefixes["all"](operationSubFunction, options); + case "-": // The results of this operation are removed from the main result + return filterRunPrefixes["except"](operationSubFunction, options); + case "+": // This operation is applied to the main results so far + return filterRunPrefixes["and"](operationSubFunction, options); + case "~": // This operation is unioned into the result only if the main result so far is empty + return filterRunPrefixes["else"](operationSubFunction, options); + case "=>": // This operation is applied to the main results so far, and the results are assigned to a variable + return filterRunPrefixes["let"](operationSubFunction, options); + default: + if(operation.namedPrefix && filterRunPrefixes[operation.namedPrefix]) { + return filterRunPrefixes[operation.namedPrefix](operationSubFunction, options); + } else { + return function(results,source,widget) { + results.clear(); + results.push($tw.language.getString("Error/FilterRunPrefix")); + }; + } + } } })()); }); diff --git a/core/modules/filters/filter.js b/core/modules/filters/filter.js index 36e48c65860..9c72ba52da4 100644 --- a/core/modules/filters/filter.js +++ b/core/modules/filters/filter.js @@ -14,7 +14,7 @@ Export our filter function */ exports.filter = function(source,operator,options) { var suffixes = operator.suffixes || [], - defaultFilterRunPrefix = (suffixes[0] || [])[0] || "or", + defaultFilterRunPrefix = (suffixes[0] || [options.defaultFilterRunPrefix] || [])[0] || "or", filterFn = options.wiki.compileFilter(operator.operand,{defaultFilterRunPrefix}), results = [], target = operator.prefix !== "!"; diff --git a/core/modules/filters/subfilter.js b/core/modules/filters/subfilter.js index c7853798e87..4f428ef7df3 100644 --- a/core/modules/filters/subfilter.js +++ b/core/modules/filters/subfilter.js @@ -14,7 +14,7 @@ Export our filter function */ exports.subfilter = function(source,operator,options) { var suffixes = operator.suffixes || [], - defaultFilterRunPrefix = (suffixes[0] || [])[0] || "or"; + defaultFilterRunPrefix = (suffixes[0] || [options.defaultFilterRunPrefix] || [])[0] || "or"; var list = options.wiki.filterTiddlers(operator.operand,options.widget,source,{defaultFilterRunPrefix}); if(operator.prefix === "!") { var results = []; diff --git a/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid b/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid new file mode 100644 index 00000000000..8fb64bdece9 --- /dev/null +++ b/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid @@ -0,0 +1,19 @@ +title: Filters/DefaultFilterRunPrefixPragma +description: Test Default Filter Run Prefix Pragma +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +\procedure mysubfilter() 1 1 +[join[Y]] + +(<$text text={{{ ::defaultprefix:all 1 1 [subfilter] +[join[X]] }}}/>) + +(<$text text={{{ 1 1 ::defaultprefix:all 1 1 +[join[X]] }}}/>) + ++ +title: ExpectedResult + +

(1X1X1Y1)

(1X1X1)

\ No newline at end of file From f2fbcf60a2ec7826569b6e0d41d2774687372f95 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 14:52:47 +0000 Subject: [PATCH 08/16] Kill linter error --- core/modules/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index ef9e4a64d62..7dc9f9f33dc 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -255,7 +255,7 @@ options: optional hashmap of options options.defaultFilterRunPrefix: the default filter run prefix to use when none is specified */ exports.compileFilter = function(filterString,options) { - var defaultFilterRunPrefix = options?.defaultFilterRunPrefix || "or"; + var defaultFilterRunPrefix = (options || {}).defaultFilterRunPrefix || "or"; var cacheKey = filterString + '|' + defaultFilterRunPrefix; if(!this.filterCache) { this.filterCache = Object.create(null); From 0ffb4d988b43a99d3b04b03136ff02f21bee4e7b Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 15:43:04 +0000 Subject: [PATCH 09/16] Fix comment Co-authored-by: Saq Imtiaz --- core/modules/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index 7dc9f9f33dc..f5e2af87524 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -173,7 +173,7 @@ exports.parseFilter = function(filterString) { match = operandRegExp.exec(filterString); if(match && match.index === p) { if(match[1]) { - // If there is a filter run prefix + // If there is a filter pragma operation.pragma = match[1]; operation.suffix = match[2]; p = p + operation.pragma.length; From 5f03d1e6ac586eb66eff2f855c543ee156438a93 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 15:49:44 +0000 Subject: [PATCH 10/16] Default to the filter run prefix "all" for certain widgets See https://github.com/TiddlyWiki/TiddlyWiki5/pull/9595#issuecomment-3773451043 --- core/modules/widgets/action-sendmessage.js | 4 ++-- core/modules/widgets/action-setmultiplefields.js | 6 +++--- core/modules/widgets/genesis.js | 8 ++++---- core/modules/widgets/setmultiplevariables.js | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/modules/widgets/action-sendmessage.js b/core/modules/widgets/action-sendmessage.js index 7b9e6453f1e..895a8b82e12 100644 --- a/core/modules/widgets/action-sendmessage.js +++ b/core/modules/widgets/action-sendmessage.js @@ -62,8 +62,8 @@ SendMessageWidget.prototype.invokeAction = function(triggeringWidget,event) { var paramObject = Object.create(null); // Add names/values pairs if present if(this.actionNames && this.actionValues) { - var names = this.wiki.filterTiddlers(this.actionNames,this), - values = this.wiki.filterTiddlers(this.actionValues,this); + var names = this.wiki.filterTiddlers(this.actionNames,this,{defaultFilterRunPrefix: "all"}), + values = this.wiki.filterTiddlers(this.actionValues,this,{defaultFilterRunPrefix: "all"}); $tw.utils.each(names,function(name,index) { paramObject[name] = values[index] || ""; }); diff --git a/core/modules/widgets/action-setmultiplefields.js b/core/modules/widgets/action-setmultiplefields.js index 1e2326394df..56e5bc1bc52 100644 --- a/core/modules/widgets/action-setmultiplefields.js +++ b/core/modules/widgets/action-setmultiplefields.js @@ -56,10 +56,10 @@ Invoke the action associated with this widget */ SetMultipleFieldsWidget.prototype.invokeAction = function(triggeringWidget,event) { var tiddler = this.wiki.getTiddler(this.actionTiddler), - names, values = this.wiki.filterTiddlers(this.actionValues,this); + names, values = this.wiki.filterTiddlers(this.actionValues,this,{defaultFilterRunPrefix: "all"}); if(this.actionFields) { var additions = {}; - names = this.wiki.filterTiddlers(this.actionFields,this); + names = this.wiki.filterTiddlers(this.actionFields,this,{defaultFilterRunPrefix: "all"}); $tw.utils.each(names,function(fieldname,index) { additions[fieldname] = values[index] || ""; }); @@ -68,7 +68,7 @@ SetMultipleFieldsWidget.prototype.invokeAction = function(triggeringWidget,event this.wiki.addTiddler(new $tw.Tiddler(creationFields,tiddler,{title: this.actionTiddler},modificationFields,additions)); } else if(this.actionIndexes) { var data = this.wiki.getTiddlerData(this.actionTiddler,Object.create(null)); - names = this.wiki.filterTiddlers(this.actionIndexes,this); + names = this.wiki.filterTiddlers(this.actionIndexes,this,{defaultFilterRunPrefix: "all"}); $tw.utils.each(names,function(name,index) { data[name] = values[index] || ""; }); diff --git a/core/modules/widgets/genesis.js b/core/modules/widgets/genesis.js index 3084ad28569..62875a5a362 100644 --- a/core/modules/widgets/genesis.js +++ b/core/modules/widgets/genesis.js @@ -72,8 +72,8 @@ GenesisWidget.prototype.execute = function() { this.attributeNames = []; this.attributeValues = []; if(this.genesisNames && this.genesisValues) { - this.attributeNames = this.wiki.filterTiddlers(self.genesisNames,this); - this.attributeValues = this.wiki.filterTiddlers(self.genesisValues,this); + this.attributeNames = this.wiki.filterTiddlers(self.genesisNames,this,{defaultFilterRunPrefix: "all"}); + this.attributeValues = this.wiki.filterTiddlers(self.genesisValues,this,{defaultFilterRunPrefix: "all"}); $tw.utils.each(this.attributeNames,function(varname,index) { $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],varname,self.attributeValues[index] || ""); }); @@ -103,8 +103,8 @@ GenesisWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(), filterNames = this.getAttribute("$names",""), filterValues = this.getAttribute("$values",""), - attributeNames = this.wiki.filterTiddlers(filterNames,this), - attributeValues = this.wiki.filterTiddlers(filterValues,this); + attributeNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}), + attributeValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); if($tw.utils.count(changedAttributes) > 0 || !$tw.utils.isArrayEqual(this.attributeNames,attributeNames) || !$tw.utils.isArrayEqual(this.attributeValues,attributeValues)) { this.refreshSelf(); return true; diff --git a/core/modules/widgets/setmultiplevariables.js b/core/modules/widgets/setmultiplevariables.js index c2c3f65de0c..5669dab809e 100644 --- a/core/modules/widgets/setmultiplevariables.js +++ b/core/modules/widgets/setmultiplevariables.js @@ -49,8 +49,8 @@ SetMultipleVariablesWidget.prototype.setVariables = function() { this.variableNames = []; this.variableValues = []; if(filterNames && filterValues) { - this.variableNames = this.wiki.filterTiddlers(filterNames,this); - this.variableValues = this.wiki.filterTiddlers(filterValues,this); + this.variableNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}); + this.variableValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); $tw.utils.each(this.variableNames,function(varname,index) { self.setVariable(varname,self.variableValues[index]); }); @@ -63,8 +63,8 @@ Refresh the widget by ensuring our attributes are up to date SetMultipleVariablesWidget.prototype.refresh = function(changedTiddlers) { var filterNames = this.getAttribute("$names",""), filterValues = this.getAttribute("$values",""), - variableNames = this.wiki.filterTiddlers(filterNames,this), - variableValues = this.wiki.filterTiddlers(filterValues,this); + variableNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}), + variableValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); if(!$tw.utils.isArrayEqual(this.variableNames,variableNames) || !$tw.utils.isArrayEqual(this.variableValues,variableValues)) { this.refreshSelf(); return true; From adf9853ce456bf6844ae91babd6359f7558df50b Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 20 Jan 2026 15:54:10 +0000 Subject: [PATCH 11/16] More joy of linting --- core/modules/filters.js | 2 +- core/modules/widgets/setmultiplevariables.js | 62 ++++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index f5e2af87524..0a2e808a382 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -256,7 +256,7 @@ options: optional hashmap of options */ exports.compileFilter = function(filterString,options) { var defaultFilterRunPrefix = (options || {}).defaultFilterRunPrefix || "or"; - var cacheKey = filterString + '|' + defaultFilterRunPrefix; + var cacheKey = filterString + "|" + defaultFilterRunPrefix; if(!this.filterCache) { this.filterCache = Object.create(null); this.filterCacheCount = 0; diff --git a/core/modules/widgets/setmultiplevariables.js b/core/modules/widgets/setmultiplevariables.js index 5669dab809e..3cb1c3c1c2f 100644 --- a/core/modules/widgets/setmultiplevariables.js +++ b/core/modules/widgets/setmultiplevariables.js @@ -12,7 +12,7 @@ Widget to set multiple variables at once from a list of names and a list of valu var Widget = require("$:/core/modules/widgets/widget.js").widget; var SetMultipleVariablesWidget = function(parseTreeNode,options) { - this.initialise(parseTreeNode,options); + this.initialise(parseTreeNode,options); }; /* @@ -24,52 +24,52 @@ SetMultipleVariablesWidget.prototype = new Widget(); Render this widget into the DOM */ SetMultipleVariablesWidget.prototype.render = function(parent,nextSibling) { - this.parentDomNode = parent; - this.computeAttributes(); - this.execute(); - this.renderChildren(parent,nextSibling); + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + this.renderChildren(parent,nextSibling); }; /* Compute the internal state of the widget */ SetMultipleVariablesWidget.prototype.execute = function() { - // Setup our variables - this.setVariables(); - // Construct the child widgets - this.makeChildWidgets(); + // Setup our variables + this.setVariables(); + // Construct the child widgets + this.makeChildWidgets(); }; SetMultipleVariablesWidget.prototype.setVariables = function() { - // Set the variables - var self = this, - filterNames = this.getAttribute("$names",""), - filterValues = this.getAttribute("$values",""); - this.variableNames = []; - this.variableValues = []; - if(filterNames && filterValues) { - this.variableNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}); - this.variableValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); - $tw.utils.each(this.variableNames,function(varname,index) { - self.setVariable(varname,self.variableValues[index]); - }); - } + // Set the variables + var self = this, + filterNames = this.getAttribute("$names",""), + filterValues = this.getAttribute("$values",""); + this.variableNames = []; + this.variableValues = []; + if(filterNames && filterValues) { + this.variableNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}); + this.variableValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); + $tw.utils.each(this.variableNames,function(varname,index) { + self.setVariable(varname,self.variableValues[index]); + }); + } }; /* Refresh the widget by ensuring our attributes are up to date */ SetMultipleVariablesWidget.prototype.refresh = function(changedTiddlers) { - var filterNames = this.getAttribute("$names",""), - filterValues = this.getAttribute("$values",""), - variableNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}), - variableValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); - if(!$tw.utils.isArrayEqual(this.variableNames,variableNames) || !$tw.utils.isArrayEqual(this.variableValues,variableValues)) { - this.refreshSelf(); - return true; - } - return this.refreshChildren(changedTiddlers); + var filterNames = this.getAttribute("$names",""), + filterValues = this.getAttribute("$values",""), + variableNames = this.wiki.filterTiddlers(filterNames,this,{defaultFilterRunPrefix: "all"}), + variableValues = this.wiki.filterTiddlers(filterValues,this,{defaultFilterRunPrefix: "all"}); + if(!$tw.utils.isArrayEqual(this.variableNames,variableNames) || !$tw.utils.isArrayEqual(this.variableValues,variableValues)) { + this.refreshSelf(); + return true; + } + return this.refreshChildren(changedTiddlers); }; exports["setmultiplevariables"] = SetMultipleVariablesWidget; From c4d0b618279c8d02f5b939e6f4bd7bdacdd34773 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 21 Jan 2026 21:45:45 +0000 Subject: [PATCH 12/16] Fix parsing bug --- core/modules/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/filters.js b/core/modules/filters.js index 0a2e808a382..43770768b6e 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -176,7 +176,7 @@ exports.parseFilter = function(filterString) { // If there is a filter pragma operation.pragma = match[1]; operation.suffix = match[2]; - p = p + operation.pragma.length; + p = match.index + match[0].length; } else if(match[3]) { // If there is a filter run prefix operation.prefix = match[3]; From 3f81e3651e0109d0ac9853df43c0bfbc9ded3db6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 21 Jan 2026 21:49:16 +0000 Subject: [PATCH 13/16] Add test for invalid pragma --- core/language/en-GB/Misc.multids | 1 + core/modules/filters.js | 2 +- .../tests/data/filters/DefaultFilterRunPrefixPragma.tid | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/language/en-GB/Misc.multids b/core/language/en-GB/Misc.multids index f792d0c8d0b..de619181d78 100644 --- a/core/language/en-GB/Misc.multids +++ b/core/language/en-GB/Misc.multids @@ -30,6 +30,7 @@ Error/DeserializeOperator/MissingOperand: Filter Error: Missing operand for 'des Error/DeserializeOperator/UnknownDeserializer: Filter Error: Unknown deserializer provided as operand for the 'deserialize' operator Error/Filter: Filter error Error/FilterSyntax: Syntax error in filter expression +Error/FilterPragma: Filter Error: Unknown pragma filter Error/FilterRunPrefix: Filter Error: Unknown prefix for filter run Error/IsFilterOperator: Filter Error: Unknown parameter for the 'is' filter operator Error/FormatFilterOperator: Filter Error: Unknown suffix for the 'format' filter operator diff --git a/core/modules/filters.js b/core/modules/filters.js index 43770768b6e..d3504d2ba77 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -371,7 +371,7 @@ exports.compileFilter = function(filterString,options) { default: return function(results,source,widget) { results.clear(); - results.push($tw.language.getString("Error/FilterRunPrefix")); + results.push($tw.language.getString("Error/FilterPragma")); }; } return function(results,source,widget) { diff --git a/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid b/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid index 8fb64bdece9..dd2f095272a 100644 --- a/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid +++ b/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid @@ -13,7 +13,9 @@ title: Output (<$text text={{{ 1 1 ::defaultprefix:all 1 1 +[join[X]] }}}/>) +(<$text text={{{ ::nonexistent X }}}/>) + + title: ExpectedResult -

(1X1X1Y1)

(1X1X1)

\ No newline at end of file +

(1X1X1Y1)

(1X1X1)

(Filter Error: Unknown pragma for filter)

\ No newline at end of file From 3b84a7042c03c9114607d0311eb476c28aec2aa2 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Thu, 22 Jan 2026 12:11:45 +0000 Subject: [PATCH 14/16] Improve English --- core/language/en-GB/Misc.multids | 2 +- .../tests/data/filters/DefaultFilterRunPrefixPragma.tid | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/language/en-GB/Misc.multids b/core/language/en-GB/Misc.multids index de619181d78..bb4aaf9932e 100644 --- a/core/language/en-GB/Misc.multids +++ b/core/language/en-GB/Misc.multids @@ -30,7 +30,7 @@ Error/DeserializeOperator/MissingOperand: Filter Error: Missing operand for 'des Error/DeserializeOperator/UnknownDeserializer: Filter Error: Unknown deserializer provided as operand for the 'deserialize' operator Error/Filter: Filter error Error/FilterSyntax: Syntax error in filter expression -Error/FilterPragma: Filter Error: Unknown pragma filter +Error/FilterPragma: Filter Error: Unknown filter pragma Error/FilterRunPrefix: Filter Error: Unknown prefix for filter run Error/IsFilterOperator: Filter Error: Unknown parameter for the 'is' filter operator Error/FormatFilterOperator: Filter Error: Unknown suffix for the 'format' filter operator diff --git a/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid b/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid index dd2f095272a..c6f3cf37ee9 100644 --- a/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid +++ b/editions/test/tiddlers/tests/data/filters/DefaultFilterRunPrefixPragma.tid @@ -18,4 +18,4 @@ title: Output + title: ExpectedResult -

(1X1X1Y1)

(1X1X1)

(Filter Error: Unknown pragma for filter)

\ No newline at end of file +

(1X1X1Y1)

(1X1X1)

(Filter Error: Unknown filter pragma)

\ No newline at end of file From 73cd48cadad701ffd0a2a0fea6db78c38dd88fcb Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Thu, 22 Jan 2026 21:01:39 +0000 Subject: [PATCH 15/16] Release note --- .../tiddlers/releasenotes/5.4.0/#9595.tid | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.4.0/#9595.tid diff --git a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9595.tid b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9595.tid new file mode 100644 index 00000000000..bcff55860c4 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9595.tid @@ -0,0 +1,34 @@ +title: $:/changenotes/5.4.0/#9595 +description: Easier avoidance of deduplication in filters +release: 5.4.0 +tags: $:/tags/ChangeNote +change-type: enhancement +change-category: filters +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9595 +github-contributors: Jermolene + +Resolves a long-standing issue with filters where there has been no easy way to avoid deduplication of results during filter evaluation. There are two distinct new capabilities. + +First, there is a new filter pragma `::defaultprefix` that sets the default filter run prefix for the remainder of the filter expression. Filter pragmas are a new concept akin to wikitext pragmas, allowing special instructions to be embedded in filter expressions. + +The `::defaultprefix` pragma takes as its first parameter the desired default filter run prefix, followed by any number of filter operations to which it applies. The default prefix `all` can be used to disable deduplication for the subsequent operations (`or` is the default prefix that performs deduplication). + +For example: + +``` +::defaultprefix:all 1 2 2 1 4 +[join[ ]] +``` + +Returns `1 2 2 1 4`. + +Contrast to the result without the pragma which returns `2 1 4`: + +``` +1 2 2 1 4 +[join[ ]] +``` + +Second, there is a new parameter to the `subfilter` and `filter` operators to specify the default filter run prefix to use when the filter is evaluated. This overrides any default set with the `::defaultprefix` pragma. + +``` +[subfilter:all] +``` From f41ed1c88c523c572c3b7135adc56757fe549387 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 23 Jan 2026 09:03:35 +0000 Subject: [PATCH 16/16] Add todos for the remaining docs --- .../tw5.com/tiddlers/concepts/Dominant Append.tid | 2 ++ editions/tw5.com/tiddlers/filters/filter.tid | 2 ++ .../tw5.com/tiddlers/filters/subfilter Operator.tid | 2 ++ .../tiddlers/filters/syntax/Filter Expression.tid | 2 ++ .../tiddlers/filters/syntax/Filter Pragma.tid | 11 +++++++++++ .../tiddlers/filters/syntax/Filter Run Prefix.tid | 2 ++ .../filters/syntax/defaultprefix Pragma.tid | 13 +++++++++++++ 7 files changed, 34 insertions(+) create mode 100644 editions/tw5.com/tiddlers/filters/syntax/Filter Pragma.tid create mode 100644 editions/tw5.com/tiddlers/filters/syntax/defaultprefix Pragma.tid diff --git a/editions/tw5.com/tiddlers/concepts/Dominant Append.tid b/editions/tw5.com/tiddlers/concepts/Dominant Append.tid index 70c9815eeb7..156eb2abccd 100644 --- a/editions/tw5.com/tiddlers/concepts/Dominant Append.tid +++ b/editions/tw5.com/tiddlers/concepts/Dominant Append.tid @@ -4,6 +4,8 @@ tags: Filters title: Dominant Append type: text/vnd.tiddlywiki +TODO:docs-default-prefix-for-subfilter: Description and link to new escape hatches + [[Filters]] manipulate [[sets of titles|Title Selection]] in which no title may appear more than once. Furthermore, they often need to append one such set to another. This is done in such a way that, if a title would be duplicated, the earlier copy of that title is discarded. The titles being appended are dominant. diff --git a/editions/tw5.com/tiddlers/filters/filter.tid b/editions/tw5.com/tiddlers/filters/filter.tid index 1ec0857f877..4327691eabd 100644 --- a/editions/tw5.com/tiddlers/filters/filter.tid +++ b/editions/tw5.com/tiddlers/filters/filter.tid @@ -12,6 +12,8 @@ tags: [[Filter Operators]] [[Negatable Operators]] title: filter Operator type: text/vnd.tiddlywiki +TODO:docs-default-prefix-for-subfilter: Description of new parameter + <<.from-version "5.1.23">> The <<.op filter>> operator runs a subfilter for each input title, and returns those input titles for which the subfilter returns a non-empty result (in other words the result is not an empty list). The results of the subfilter are thrown away. Simple filter operations can be concatenated together directly (eg `[tag[HelloThere]search[po]]`) but this doesn't work when the filtering operations require intermediate results to be computed. The <<.op filter>> operator can be used to filter on an intermediate result which is discarded. To take the same example but to also filter by those tiddlers whose text field is longer than 1000 characters: diff --git a/editions/tw5.com/tiddlers/filters/subfilter Operator.tid b/editions/tw5.com/tiddlers/filters/subfilter Operator.tid index 096560ae2a3..076604f55dc 100644 --- a/editions/tw5.com/tiddlers/filters/subfilter Operator.tid +++ b/editions/tw5.com/tiddlers/filters/subfilter Operator.tid @@ -12,6 +12,8 @@ tags: [[Filter Operators]] [[Field Operators]] [[Selection Constructors]] [[Nega title: subfilter Operator type: text/vnd.tiddlywiki +TODO:docs-default-prefix-for-subfilter: Description of new parameter + <<.from-version "5.1.18">> Note that the <<.op subfilter>> operator was introduced in version 5.1.18 and is not available in earlier versions. <<.tip " Literal filter parameters cannot contain square brackets but you can work around the issue by using a variable:">> diff --git a/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid b/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid index 739be62bf2b..18c0f443525 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Filter Expression.tid @@ -4,6 +4,8 @@ tags: [[Filter Syntax]] title: Filter Expression type: text/vnd.tiddlywiki +TODO:docs-default-prefix-for-subfilter: Update railroad diagram + A <<.def "filter expression">> is the outermost level of the [[filter syntax|Filter Syntax]]. It consists of [[filter runs|Filter Run]] with optional [[filter run prefixes|Filter Run Prefix]]. Multiple filter runs are separated by [[whitespace|Filter Whitespace]]. <$railroad text=""" diff --git a/editions/tw5.com/tiddlers/filters/syntax/Filter Pragma.tid b/editions/tw5.com/tiddlers/filters/syntax/Filter Pragma.tid new file mode 100644 index 00000000000..b65d291dd31 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/syntax/Filter Pragma.tid @@ -0,0 +1,11 @@ +created: 20260122212128206 +modified: 20260122212128206 +tags: [[Filter Expression]] +title: Filter Pragma +type: text/vnd.tiddlywiki + +TODO:docs-default-prefix-for-subfilter: High level pragma docs & railroad diagram + +A <<.def "filter pragma">> is a special instruction embedded within a filter expression that affects the behavior of subsequent filter operations. Filter pragmas are similar to wikitext pragmas and are used to control aspects of filter evaluation. + +The `::defaultprefix` pragma sets the default filter run prefix for the remainder of the filter expression. This allows users to control deduplication behavior without having to specify prefixes for each individual operation. diff --git a/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix.tid b/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix.tid index 5eb487b6da6..2f063ec9681 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix.tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix.tid @@ -4,6 +4,8 @@ tags: [[Filter Expression]] title: Filter Run Prefix type: text/vnd.tiddlywiki +TODO:docs-default-prefix-for-subfilter: Link to how the default filter run prefix can be specified + There are 2 types of filter run prefixes that are interchangeable; [[named prefixes|Named Filter Run Prefix]] and [[shortcut prefixes|Shortcut Filter Run Prefix]]. <$railroad text=""" diff --git a/editions/tw5.com/tiddlers/filters/syntax/defaultprefix Pragma.tid b/editions/tw5.com/tiddlers/filters/syntax/defaultprefix Pragma.tid new file mode 100644 index 00000000000..0eeb8b0f5f2 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/syntax/defaultprefix Pragma.tid @@ -0,0 +1,13 @@ +created: 20260122212128206 +modified: 20260122212128206 +tags: [[Filter Pragma]] +title: defaultprefix Filter Pragma +type: text/vnd.tiddlywiki + +TODO:docs-default-prefix-for-subfilter: Purpose and docs of new pragma + +A <<.def "defaultprefix filter pragma">> is used to set the default [[Filter Run Prefix]] for the remainder of the filter expression. This allows users to control deduplication behavior without having to specify prefixes for each individual operation. + +Any of the [[named prefixes|Named Filter Run Prefix]] can be specified as the default prefix, but `all` is the most commonly used value to disable deduplication. + +