From 5d1c1eaf8765c004f82d81d385835a365aa67575 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 22 Nov 2025 12:29:42 +0000 Subject: [PATCH] Introduce multi-valued variables and let filter run prefix (#8972) * Introduce let filter run prefix for assigning filter run result to a variable * Get rid of the special behaviour for all[] Not needed because the input to the filter run is available * Fix tests * Fix tests * Cleanup * Support for saving result lists in a variable Extend let filter run prefix to store list of results, and add varlist operator for accessing variables as a list. We already had partial support for variables returning a list of values in order for functions to work, now we extend it so that any variable can be used to store a list We should extend the set widget so that it returns a result list that can be accessed with the varlist operator * Docs update * Introduce letlist widget for assigning lists to variables Terrible name. Annoyingly, we can't overload the existing let or set widgets. * Docs update * Update DefaultTiddlers to highlight the new docs * Fixed varlist crash with empty parameter * Switch to triple brace syntax for assigning filtered lists * Docs update * Test for multivalued functions * varlist operator: fixed crash accessing non-existent variable See https://github.com/TiddlyWiki/TiddlyWiki5/pull/8972#issuecomment-2712068743 * Dispense with the letlist widget What this PR actually does is rename the letlist widget to "let". The result is the same as using the letlist widget, but it is backwards compatible by virtue of the fact that all existing ways to access variables will only see the single value for the variable. * Refactor the let filter run prefix to assign the input list to the variable named by the filter run These semantics are much simpler, and allow the variable name to be computed. * Missed off 211b13526501b7e61e42a6ffc6695688281258b2 * Docs update * Bug fix * Introduce round brackets for multi-valued filter operands Allowing us to drop the varlist operator * Introduce => as a shortcut syntax for the let filter run prefix Also relax the requirement for a filter run prefix to be followed by an opening square bracket * Fix bug exposed in "Filter Operators" tiddler See https://github.com/TiddlyWiki/TiddlyWiki5/pull/8972#issuecomment-2740003414 * Fix bug with missing variable attributes See https://github.com/TiddlyWiki/TiddlyWiki5/pull/8972#issuecomment-2752792329 * Fix bug with round brackets for 2nd parameter onwards * Allow functions to take multivalued parameters * Simplify title operator * Extend title operator to allow negated form to use multi-valued variables * Remove duplicate test * Update action-log widget to log multi-valued attributes * Docs updates * Fix typos * Happy linter happy life * Fix version numbers of from-version procedures * Another incorrect version number * Add change note * Fix filenames of tests * Typo * Update let.js * Docs updates --- core/modules/filterrunprefixes/let.js | 41 +++++++ core/modules/filters.js | 103 +++++++++++++----- core/modules/filters/function.js | 4 +- core/modules/filters/title.js | 5 +- core/modules/filters/unknown.js | 4 +- core/modules/utils/utils.js | 18 ++- core/modules/widgets/action-log.js | 26 +++-- core/modules/widgets/let.js | 8 +- core/modules/widgets/widget.js | 90 ++++++++++++--- .../tiddlers/system/DefaultTiddlers.tid | 11 +- .../data/let-filter-prefix/ResultList.tid | 12 ++ .../ResultListUnnamedVariable.tid | 12 ++ .../data/let-filter-prefix/ShortcutSyntax.tid | 12 ++ .../tests/data/let-filter-prefix/Simple.tid | 12 ++ .../data/multi-valued-variables/Function.tid | 18 +++ .../MissingVariable.tid | 12 ++ .../multi-valued-variables/NegatedTitle.tid | 18 +++ .../data/multi-valued-variables/Operands.tid | 18 +++ .../multi-valued-variables/Parameters.tid | 21 ++++ .../ParametersShortcut.tid | 21 ++++ .../data/multi-valued-variables/Simple.tid | 17 +++ .../tests/test-widget-getVariableInfo.js | 6 +- .../Interchangeable Filter Run Prefixes.tid | 1 + .../filters/syntax/Let Filter Run Prefix.tid | 28 +++++ .../syntax/Named Filter Run Prefix.tid | 1 + .../syntax/Shortcut Filter Run Prefixes.tid | 5 +- editions/tw5.com/tiddlers/filters/title.tid | 2 + .../tiddlers/releasenotes/5.4.0/#8972.tid | 16 +++ .../tiddlers/system/DefaultTiddlers.tid | 10 +- .../variables/Multi-Valued Variables.tid | 48 ++++++++ .../tw5.com/tiddlers/variables/Variables.tid | 8 +- .../tiddlers/widgets/ActionLogWidget.tid | 1 + .../tw5.com/tiddlers/widgets/LetWidget.tid | 16 ++- 33 files changed, 530 insertions(+), 95 deletions(-) create mode 100644 core/modules/filterrunprefixes/let.js create mode 100644 editions/test/tiddlers/tests/data/let-filter-prefix/ResultList.tid create mode 100644 editions/test/tiddlers/tests/data/let-filter-prefix/ResultListUnnamedVariable.tid create mode 100644 editions/test/tiddlers/tests/data/let-filter-prefix/ShortcutSyntax.tid create mode 100644 editions/test/tiddlers/tests/data/let-filter-prefix/Simple.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/Function.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/MissingVariable.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/NegatedTitle.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/Operands.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/Parameters.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/ParametersShortcut.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/Simple.tid create mode 100644 editions/tw5.com/tiddlers/filters/syntax/Let Filter Run Prefix.tid create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid create mode 100644 editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid diff --git a/core/modules/filterrunprefixes/let.js b/core/modules/filterrunprefixes/let.js new file mode 100644 index 000000000..b4dad57d8 --- /dev/null +++ b/core/modules/filterrunprefixes/let.js @@ -0,0 +1,41 @@ +/*\ +title: $:/core/modules/filterrunprefixes/let.js +type: application/javascript +module-type: filterrunprefix + +Assign a value to a variable + +\*/ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Export our filter prefix function +*/ +exports.let = function(operationSubFunction,options) { + // Return the filter run prefix function + return function(results,source,widget) { + // Save the result list + var resultList = results.toArray(); + // Clear the results + results.clear(); + // Evaluate the subfunction to get the variable name + var subFunctionResults = operationSubFunction(source,widget); + if(subFunctionResults.length === 0) { + return; + } + var name = subFunctionResults[0]; + if(typeof name !== "string" || name.length === 0) { + return; + } + // Assign the result of the subfunction to the variable + var variables = {}; + variables[name] = resultList; + // Return the variables + return { + variables: variables + }; + }; +}; diff --git a/core/modules/filters.js b/core/modules/filters.js index 213268b1a..56bc533cc 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -35,7 +35,7 @@ function parseFilterOperation(operators,filterString,p) { operator.prefix = filterString.charAt(p++); } // Get the operator name - nextBracketPos = filterString.substring(p).search(/[\[\{<\/]/); + nextBracketPos = filterString.substring(p).search(/[\[\{<\/\(]/); if(nextBracketPos === -1) { throw "Missing [ in filter expression"; } @@ -79,6 +79,10 @@ function parseFilterOperation(operators,filterString,p) { operand.variable = true; nextBracketPos = filterString.indexOf(">",p); break; + case "(": // Round brackets + operand.multiValuedVariable = true; + nextBracketPos = filterString.indexOf(")",p); + break; case "/": // regexp brackets var rex = /^((?:[^\\\/]|\\.)*)\/(?:\(([mygi]+)\))?/g, rexMatch = rex.exec(filterString.substring(p)); @@ -112,7 +116,7 @@ function parseFilterOperation(operators,filterString,p) { // Check for multiple operands while(filterString.charAt(p) === ",") { p++; - if(/^[\[\{<\/]/.test(filterString.substring(p))) { + if(/^[\[\{<\/\(]/.test(filterString.substring(p))) { nextBracketPos = p; p++; parseOperand(filterString.charAt(nextBracketPos)); @@ -141,7 +145,15 @@ exports.parseFilter = function(filterString) { p = 0, // Current position in the filter string match; var whitespaceRegExp = /(\s+)/mg, - operandRegExp = /((?:\+|\-|~|=|\:(\w+)(?:\:([\w\:, ]*))?)?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\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; while(p < filterString.length) { // Skip any whitespace whitespaceRegExp.lastIndex = p; @@ -152,38 +164,45 @@ exports.parseFilter = function(filterString) { // Match the start of the operation if(p < filterString.length) { operandRegExp.lastIndex = p; - match = operandRegExp.exec(filterString); - if(!match || match.index !== p) { - throw $tw.language.getString("Error/FilterSyntax"); - } var operation = { prefix: "", operators: [] }; - if(match[1]) { - operation.prefix = match[1]; - p = p + operation.prefix.length; - if(match[2]) { - operation.namedPrefix = match[2]; - } - if(match[3]) { - operation.suffixes = []; - $tw.utils.each(match[3].split(":"),function(subsuffix) { - operation.suffixes.push([]); - $tw.utils.each(subsuffix.split(","),function(entry) { - entry = $tw.utils.trim(entry); - if(entry) { - operation.suffixes[operation.suffixes.length -1].push(entry); - } + match = operandRegExp.exec(filterString); + if(match && match.index === p) { + // If there is a filter run prefix + if(match[1]) { + operation.prefix = match[1]; + p = p + operation.prefix.length; + // Name for named prefixes + if(match[2]) { + operation.namedPrefix = match[2]; + } + // Suffixes for filter run prefix + if(match[3]) { + operation.suffixes = []; + $tw.utils.each(match[3].split(":"),function(subsuffix) { + operation.suffixes.push([]); + $tw.utils.each(subsuffix.split(","),function(entry) { + entry = $tw.utils.trim(entry); + if(entry) { + operation.suffixes[operation.suffixes.length -1].push(entry); + } + }); }); - }); + } + } + // Opening square bracket + if(match[4]) { + p = parseFilterOperation(operation.operators,filterString,p); + } else { + p = match.index + match[0].length; } - } - if(match[4]) { // Opening square bracket - p = parseFilterOperation(operation.operators,filterString,p); } else { - p = match.index + match[0].length; + // No filter run prefix + 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 operation.operators.push( {operator: "title", operands: [{text: match[5] || match[6] || match[7]}]} @@ -251,6 +270,8 @@ exports.compileFilter = function(filterString) { results = []; $tw.utils.each(operation.operators,function(operator) { var operands = [], + multiValueOperands = [], + isMultiValueOperand = [], operatorFunction; if(!operator.operator) { // Use the "title" operator if no operator is specified @@ -266,13 +287,29 @@ exports.compileFilter = function(filterString) { if(operand.indirect) { var currTiddlerTitle = widget && widget.getVariable("currentTiddler"); operand.value = self.getTextReference(operand.text,"",currTiddlerTitle); + operand.multiValue = [operand.value]; } else if(operand.variable) { var varTree = $tw.utils.parseFilterVariable(operand.text); operand.value = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source})[0] || ""; + operand.multiValue = [operand.value]; + } else if(operand.multiValuedVariable) { + var varTree = $tw.utils.parseFilterVariable(operand.text); + var resultList = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source}); + if((resultList.length > 0 && resultList[0] !== undefined) || resultList.length === 0) { + operand.multiValue = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source}) || []; + operand.value = operand.multiValue[0] || ""; + } else { + operand.value = ""; + operand.multiValue = []; + } + operand.isMultiValueOperand = true; } else { operand.value = operand.text; + operand.multiValue = [operand.value]; } operands.push(operand.value); + multiValueOperands.push(operand.multiValue); + isMultiValueOperand.push(!!operand.isMultiValueOperand); }); // Invoke the appropriate filteroperator module @@ -280,6 +317,8 @@ exports.compileFilter = function(filterString) { operator: operator.operator, operand: operands.length > 0 ? operands[0] : undefined, operands: operands, + multiValueOperands: multiValueOperands, + isMultiValueOperand: isMultiValueOperand, prefix: operator.prefix, suffix: operator.suffix, suffixes: operator.suffixes, @@ -319,6 +358,8 @@ exports.compileFilter = function(filterString) { 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); @@ -345,7 +386,13 @@ exports.compileFilter = function(filterString) { self.filterRecursionCount = (self.filterRecursionCount || 0) + 1; if(self.filterRecursionCount < MAX_FILTER_DEPTH) { $tw.utils.each(operationFunctions,function(operationFunction) { - operationFunction(results,source,widget); + var operationResult = operationFunction(results,source,widget); + if(operationResult) { + if(operationResult.variables) { + // If the filter run prefix has returned variables, create a new fake widget with those variables + widget = widget.makeFakeWidgetWithVariables(operationResult.variables); + } + } }); } else { results.push("/**-- Excessive filter recursion --**/"); diff --git a/core/modules/filters/function.js b/core/modules/filters/function.js index b6f2fa636..cf7a6bb0d 100644 --- a/core/modules/filters/function.js +++ b/core/modules/filters/function.js @@ -16,8 +16,8 @@ exports.function = function(source,operator,options) { var functionName = operator.operands[0], params = [], results; - $tw.utils.each(operator.operands.slice(1),function(param) { - params.push({value: param}); + $tw.utils.each(operator.multiValueOperands.slice(1),function(paramList) { + params.push({value: paramList[0] || "",multiValue: paramList}); }); // console.log(`Calling ${functionName} with params ${JSON.stringify(params)}`); var variableInfo = options.widget && options.widget.getVariableInfo && options.widget.getVariableInfo(functionName,{params: params, source: source}); diff --git a/core/modules/filters/title.js b/core/modules/filters/title.js index a1dff909b..228676da4 100644 --- a/core/modules/filters/title.js +++ b/core/modules/filters/title.js @@ -16,12 +16,13 @@ exports.title = function(source,operator,options) { var results = []; if(operator.prefix === "!") { source(function(tiddler,title) { - if(tiddler && tiddler.fields.title !== operator.operand) { + var titleList = operator.multiValueOperands[0] || []; + if(tiddler && titleList.indexOf(tiddler.fields.title) === -1) { results.push(title); } }); } else { - results.push(operator.operand); + Array.prototype.push.apply(results,operator.multiValueOperands[0]); } return results; }; diff --git a/core/modules/filters/unknown.js b/core/modules/filters/unknown.js index 8fe2a6889..33b229092 100644 --- a/core/modules/filters/unknown.js +++ b/core/modules/filters/unknown.js @@ -20,8 +20,8 @@ exports["[unknown]"] = function(source,operator,options) { // Check for a user defined filter operator if(operator.operator.indexOf(".") !== -1) { var params = []; - $tw.utils.each(operator.operands,function(param) { - params.push({value: param}); + $tw.utils.each(operator.multiValueOperands,function(paramList) { + params.push({value: paramList[0] || "",multiValue: paramList}); }); var variableInfo = options.widget && options.widget.getVariableInfo && options.widget.getVariableInfo(operator.operator,{params: params, source: source}); if(variableInfo && variableInfo.srcVariable) { diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index 3e1cfabd5..a9c05975d 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -49,14 +49,26 @@ exports.warning = function(text) { }; /* -Log a table of name: value pairs +Log a table of name: value or name: [values...] pairs */ exports.logTable = function(data) { - if(console.table) { + var hasArrays = false; + $tw.utils.each(data,function(value,name) { + if($tw.utils.isArray(value)) { + hasArrays = true; + } + }); + if(console.table && !hasArrays) { console.table(data); } else { $tw.utils.each(data,function(value,name) { - console.log(name + ": " + value); + if($tw.utils.isArray(value)) { + for(var t=0; t 1 ? variableInfo.resultList : variableInfo.text; } } if(this.filter) { diff --git a/core/modules/widgets/let.js b/core/modules/widgets/let.js index 3cc09ad94..b09c67e91 100644 --- a/core/modules/widgets/let.js +++ b/core/modules/widgets/let.js @@ -46,7 +46,7 @@ LetWidget.prototype.computeAttributes = function() { self = this; this.currentValueFor = Object.create(null); $tw.utils.each($tw.utils.getOrderedAttributesFromParseTreeNode(this.parseTreeNode),function(attribute) { - var value = self.computeAttribute(attribute), + var value = self.computeAttribute(attribute,{asList: true}), name = attribute.name; // Now that it's prepped, we're allowed to look this variable up // when defining later variables @@ -56,7 +56,7 @@ LetWidget.prototype.computeAttributes = function() { }); // Run through again, setting variables and looking for differences $tw.utils.each(this.currentValueFor,function(value,name) { - if (self.attributes[name] !== value) { + if(!$tw.utils.isArrayEqual(self.attributes[name],value)) { self.attributes[name] = value; self.setVariable(name,value); changedAttributes[name] = true; @@ -69,8 +69,10 @@ LetWidget.prototype.getVariableInfo = function(name,options) { // Special handling: If this variable exists in this very $let, we can // use it, but only if it's been staged. if ($tw.utils.hop(this.currentValueFor,name)) { + var value = this.currentValueFor[name]; return { - text: this.currentValueFor[name] + text: value[0] || "", + resultList: value }; } return Widget.prototype.getVariableInfo.call(this,name,options); diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index b89df34b7..67d32e976 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -80,7 +80,7 @@ Widget.prototype.execute = function() { /* Set the value of a context variable name: name of the variable -value: value of the variable +value: value of the variable, can be a string or an array params: array of {name:, default:} for each parameter isMacroDefinition: true if the variable is set via a \define macro pragma (and hence should have variable substitution performed) options includes: @@ -90,8 +90,10 @@ options includes: */ Widget.prototype.setVariable = function(name,value,params,isMacroDefinition,options) { options = options || {}; + var valueIsArray = $tw.utils.isArray(value); this.variables[name] = { - value: value, + value: valueIsArray ? (value[0] || "") : value, + resultList: valueIsArray ? value : [value], params: params, isMacroDefinition: !!isMacroDefinition, isFunctionDefinition: !!options.isFunctionDefinition, @@ -114,7 +116,7 @@ allowSelfAssigned: if true, includes the current widget in the context chain ins Returns an object with the following fields: -params: array of {name:,value:} or {value:} of parameters to be applied +params: array of {name:,value:,multiValue:} of parameters to be applied (name is optional) text: text of variable, with parameters properly substituted resultList: result of variable evaluation as an array srcVariable: reference to the object defining the variable @@ -140,7 +142,9 @@ Widget.prototype.getVariableInfo = function(name,options) { params = self.resolveVariableParameters(variable.params,actualParams); // Substitute any parameters specified in the definition $tw.utils.each(params,function(param) { - value = $tw.utils.replaceString(value,new RegExp("\\$" + $tw.utils.escapeRegExp(param.name) + "\\$","mg"),param.value); + if("name" in param) { + value = $tw.utils.replaceString(value,new RegExp("\\$" + $tw.utils.escapeRegExp(param.name) + "\\$","mg"),param.value); + } }); value = self.substituteVariableReferences(value,options); resultList = [value]; @@ -154,13 +158,20 @@ Widget.prototype.getVariableInfo = function(name,options) { variables[param.name] = param["default"]; } }); - // Parameters are an array of {value:} or {name:, value:} pairs + // Parameters are an array of {name:, value:, multivalue:} pairs (name and multivalue are optional) $tw.utils.each(params,function(param) { - variables[param.name] = param.value; + if(param.multiValue) { + variables[param.name] = param.multiValue; + } else { + variables[param.name] = param.value || ""; + } }); resultList = this.wiki.filterTiddlers(value,this.makeFakeWidgetWithVariables(variables),options.source); value = resultList[0] || ""; } else { + if(variable.resultList) { + resultList = variable.resultList; + } params = variable.params; } return { @@ -192,22 +203,24 @@ Widget.prototype.getVariable = function(name,options) { /* Maps actual parameters onto formal parameters, returning an array of {name:,value:} objects formalParams - Array of {name:,default:} (default value is optional) -actualParams - Array of string values or {name:,value:} (name is optional) +actualParams - Array of string values or {name:,value:,multiValue} (name and multiValue is optional) */ Widget.prototype.resolveVariableParameters = function(formalParams,actualParams) { formalParams = formalParams || []; actualParams = actualParams || []; var nextAnonParameter = 0, // Next candidate anonymous parameter in macro call - paramInfo, paramValue, + paramInfo, paramValue, paramMultiValue, results = []; // Step through each of the parameters in the macro definition for(var p=0; p ++ +title: ExpectedResult + +

$:/core,ExpectedResult,Output

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/let-filter-prefix/ResultListUnnamedVariable.tid b/editions/test/tiddlers/tests/data/let-filter-prefix/ResultListUnnamedVariable.tid new file mode 100644 index 000000000..58493f2f4 --- /dev/null +++ b/editions/test/tiddlers/tests/data/let-filter-prefix/ResultListUnnamedVariable.tid @@ -0,0 +1,12 @@ +title: LetFilterRunPrefix/ResultListUnnamedVariable +description: Using the "let" filter run prefix to store result lists, not just single values +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +<$text text={{{ [all[tiddlers]] :let[all[]tag[nothing]] [(varname)sort[]join[,]] }}}/> ++ +title: ExpectedResult + +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/let-filter-prefix/ShortcutSyntax.tid b/editions/test/tiddlers/tests/data/let-filter-prefix/ShortcutSyntax.tid new file mode 100644 index 000000000..9c5b5997d --- /dev/null +++ b/editions/test/tiddlers/tests/data/let-filter-prefix/ShortcutSyntax.tid @@ -0,0 +1,12 @@ +title: LetFilterRunPrefix/ShortcutSyntax +description: Simple usage of "let" filter run prefix +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +<$text text={{{ [[magpie]] =>varname [] +[join[-]] }}}/> ++ +title: ExpectedResult + +

magpie

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/let-filter-prefix/Simple.tid b/editions/test/tiddlers/tests/data/let-filter-prefix/Simple.tid new file mode 100644 index 000000000..f16ea107f --- /dev/null +++ b/editions/test/tiddlers/tests/data/let-filter-prefix/Simple.tid @@ -0,0 +1,12 @@ +title: LetFilterRunPrefix/Simple +description: Simple usage of "let" filter run prefix +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +<$text text={{{ [[magpie]] :let[[varname]] [] +[join[-]] }}}/> ++ +title: ExpectedResult + +

magpie

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/Function.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/Function.tid new file mode 100644 index 000000000..9d2ba9c06 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/Function.tid @@ -0,0 +1,18 @@ +title: MultiValuedVariables/Function +description: Multi-valued functions +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\function myfunc() [all[tiddlers]sort[]] + +<$let varname=<>> +<$text text={{{ [(varname)] +[join[-]] }}}/> + ++ +title: ExpectedResult + +

+$:/core-ExpectedResult-Output +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/MissingVariable.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/MissingVariable.tid new file mode 100644 index 000000000..8481dcb44 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/MissingVariable.tid @@ -0,0 +1,12 @@ +title: MultiValuedVariables/MissingVariable +description: Using multivalued operands with a missing variable +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +<$text text={{{ [(varname)] }}}/> ++ +title: ExpectedResult + +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/NegatedTitle.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/NegatedTitle.tid new file mode 100644 index 000000000..576efdade --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/NegatedTitle.tid @@ -0,0 +1,18 @@ +title: MultiValuedVariables/NegatedTitle +description: Multi-valued operands +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +<$let + exclude={{{ $:/core Output }}} +> +<$text text={{{ [all[tiddlers]!title(exclude)] +[join[-]] }}}/> + ++ +title: ExpectedResult + +

+ExpectedResult +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/Operands.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/Operands.tid new file mode 100644 index 000000000..902dc5699 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/Operands.tid @@ -0,0 +1,18 @@ +title: MultiValuedVariables/Operands +description: Multi-valued operands +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\function myfunc() [all[tiddlers]sort[]] + +<$let varname=<>> +<$text text={{{ [(varname)] +[join[-]] }}}/> + ++ +title: ExpectedResult + +

+$:/core-ExpectedResult-Output +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/Parameters.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/Parameters.tid new file mode 100644 index 000000000..c3ee0fa81 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/Parameters.tid @@ -0,0 +1,21 @@ +title: MultiValuedVariables/Parameters +description: Multi-valued function parameters +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\function myfunc(input) [(input)sort[]] + +<$let + input={{{ [all[tiddlers]] }}} + output={{{ [function[myfunc],(input)] }}} +> +<$text text={{{ [(output)] +[join[-]] }}}/> + ++ +title: ExpectedResult + +

+$:/core-ExpectedResult-Output +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/ParametersShortcut.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/ParametersShortcut.tid new file mode 100644 index 000000000..9dbb5a3f2 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/ParametersShortcut.tid @@ -0,0 +1,21 @@ +title: MultiValuedVariables/ParametersShortcut +description: Multi-valued function parameters using shortcut syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\function my.func(input) [(input)sort[]] + +<$let + input={{{ [all[tiddlers]] }}} + output={{{ [my.func(input)] }}} +> +<$text text={{{ [(output)] +[join[-]] }}}/> + ++ +title: ExpectedResult + +

+$:/core-ExpectedResult-Output +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/Simple.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/Simple.tid new file mode 100644 index 000000000..f52e1e6a1 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/Simple.tid @@ -0,0 +1,17 @@ +title: MultiValuedVariables/Simple +description: Simple usage of multivalued assignments with the "let" widget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +<$let varname={{{ [all[tiddlers]sort[]] }}} + varname2=<>> +<$text text={{{ [(varname2)] +[join[-]] }}}/> + ++ +title: ExpectedResult + +

+$:/core-ExpectedResult-Output +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-widget-getVariableInfo.js b/editions/test/tiddlers/tests/test-widget-getVariableInfo.js index ae0f4ce20..23baf436d 100644 --- a/editions/test/tiddlers/tests/test-widget-getVariableInfo.js +++ b/editions/test/tiddlers/tests/test-widget-getVariableInfo.js @@ -60,10 +60,10 @@ describe("Widget module", function() { childNode = childNode.children[0]; } - expect(childNode.getVariableInfo("macro",{allowSelfAssigned:true}).params).toEqual([{name:"a",value:"aa"}]); + expect(childNode.getVariableInfo("macro",{allowSelfAssigned:true}).params).toEqual([{name:"a",value:"aa",multiValue:["aa"]}]); // function params - expect(childNode.getVariableInfo("fn", {allowSelfAssigned:true}).params).toEqual([{name:"f",value:"ff"}]); + expect(childNode.getVariableInfo("fn", {allowSelfAssigned:true}).params).toEqual([{name:"f",value:"ff",multiValue:["ff"]}]); // functions have a text and a value expect(childNode.getVariableInfo("x", {allowSelfAssigned:true}).text).toBe("fff"); expect(childNode.getVariableInfo("x", {allowSelfAssigned:true}).srcVariable.value).toBe("[]"); @@ -73,7 +73,7 @@ describe("Widget module", function() { expect(childNode.getVariableInfo("$my.widget", {allowSelfAssigned:true}).params).toEqual([{name:"w",default:"ww"}]); // no params expected - expect(childNode.getVariableInfo("abc", {allowSelfAssigned:true})).toEqual({text:"def"}); + expect(childNode.getVariableInfo("abc", {allowSelfAssigned:true})).toEqual({text:"def", resultList: [ "def" ]}); // debugger; Find code in browser diff --git a/editions/tw5.com/tiddlers/filters/syntax/Interchangeable Filter Run Prefixes.tid b/editions/tw5.com/tiddlers/filters/syntax/Interchangeable Filter Run Prefixes.tid index 0313f543a..8eb9c7330 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Interchangeable Filter Run Prefixes.tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Interchangeable Filter Run Prefixes.tid @@ -14,6 +14,7 @@ In technical / logical terms: |`-[run]` |`:except[run]` |difference of sets |... AND NOT run | |`~[run]` |`:else[run]` |else |... ELSE run | |`=[run]` |`:all[run]` |union of sets without de-duplication |... OR run | +|`=>[run]` |`:let[run]` |<<.from-version "5.4.0">> assign results to a variable |... LET run | The input of a run is normally a list of all the non-[[shadow|ShadowTiddlers]] tiddler titles in the wiki (in no particular order).
But the `+` prefix can change this: diff --git a/editions/tw5.com/tiddlers/filters/syntax/Let Filter Run Prefix.tid b/editions/tw5.com/tiddlers/filters/syntax/Let Filter Run Prefix.tid new file mode 100644 index 000000000..464011add --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/syntax/Let Filter Run Prefix.tid @@ -0,0 +1,28 @@ +created: 20250307212252946 +from-version: 5.4.0 +modified: 20250307212252946 +rp-input: all titles from previous filter runs +rp-output: an empty title list is always returned from the "let" filter run prefix +rp-purpose: assign the title list resulting from previous filter runs to a multi-valued variable +tags: [[Named Filter Run Prefix]] +title: Let Filter Run Prefix +type: text/vnd.tiddlywiki + +<$railroad text=""" +\start none +\end none +( ":let" ) +[[run|"Filter Run"]] +"""/> + +The `:let` filter run prefix assigns the title list resulting from previous filter runs to a [[multi-valued variable|Multi-Valued Variable]]. The variable is named with the first result returned by the filter run. + +The variable is made available to the remaining [[filter runs|Filter Run]] in the [[filter expression|Filter Expression]]. Only the first item in the result list is returned when the variable is accessed in the usual way (or an empty string if the result list is empty). Using round brackets instead of angle brackets around a variable name as an operand retrieves the complete list of items in the result list. + +This prefix has an optional [[shortcut syntax|Shortcut Filter Run Prefix]] symbol `=>run`. For example: + +``` +=[] =[] =>myvar +``` + +The `:let` filter run prefix always clears the current result list. \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/syntax/Named Filter Run Prefix.tid b/editions/tw5.com/tiddlers/filters/syntax/Named Filter Run Prefix.tid index 31534479e..0706e4cff 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Named Filter Run Prefix.tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Named Filter Run Prefix.tid @@ -23,6 +23,7 @@ A named filter run prefix can precede any [[run|Filter Run]] of a [[filter expre [[<":or"> |"Or Filter Run Prefix"]] | [[<":reduce"> |"Reduce Filter Run Prefix"]] | [[<":sort"> /"v5.2.0"/ |"Sort Filter Run Prefix"]] | +[[<":let"> /"v5.4.0"/ |"Let Filter Run Prefix"]] | [[<":then"> /"v5.3.0"/ |"Then Filter Run Prefix"]]) [[run|"Filter Run"]] """/> diff --git a/editions/tw5.com/tiddlers/filters/syntax/Shortcut Filter Run Prefixes.tid b/editions/tw5.com/tiddlers/filters/syntax/Shortcut Filter Run Prefixes.tid index fa8bc26e1..bf4326c41 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Shortcut Filter Run Prefixes.tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Shortcut Filter Run Prefixes.tid @@ -9,7 +9,7 @@ Shortcut prefixes are commonly used by advanced users because they are fast to t <$railroad text=""" \start none \end none -(-|:"+"|"-"|"~"|"=") +(-|:"+"|"-"|"~"|"="|"=>") [[run|"Filter Run"]] """/> @@ -23,7 +23,8 @@ If a run has: * the prefix `~`, if the filter output so far is an empty list then the output titles of the run are [[dominantly appended|Dominant Append]] to the filter's output. If the filter output so far is not an empty list then the run is ignored. <<.from-version "5.1.18">> -* the prefix `=`, output titles are appended to the filter's output without de-duplication. <<.from-version "5.1.20">> +* the prefix `=`, output titles are appended to the filter's output without de-duplication. <<.from-version "5.1.20">> +* the prefix `=>`, the input is assigned to the variable named with the output title. <<.from-version "5.4.0">> {{Interchangeable Filter Run Prefixes}} \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/title.tid b/editions/tw5.com/tiddlers/filters/title.tid index 3bb59d73f..e6837ef0e 100644 --- a/editions/tw5.com/tiddlers/filters/title.tid +++ b/editions/tw5.com/tiddlers/filters/title.tid @@ -16,4 +16,6 @@ op-neg-output: the input, but with tiddler <<.place T>> filtered out if it exist <<.op title>> is a [[constructor|Selection Constructors]] (except in the form `!title`), but <<.olink2 "field:title" field>> is a [[modifier|Selection Constructors]]. +<<.from-version "5.4.0">> If the operand is quoted with round brackets then the <<.op title>> operator returns the complete list of titles assigned to the multi-valued variable. When negated, the title operator with multi-valued operands returns all the titles that are not present in the operand list. + <<.operator-examples "title">> diff --git a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid new file mode 100644 index 000000000..45203e0eb --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid @@ -0,0 +1,16 @@ +title: $:/changenotes/5.4.0/#8972 +description: Multi-valued variables and let filter run prefix +release: 5.4.0 +tags: $:/tags/ChangeNote +change-type: enhancement +change-category: hackability +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/8972 +github-contributors: Jermolene + +This PR introduces a new filter run prefix `:let` that assigns the result of the filter run to a variable that is made available for the remaining filter runs of the filter expression. It solves the problem that previously it was impossible to compute values for filter operator parameters; parameters could only be a literal string, text reference or variable reference. + +This PR also introduces multi-valued variables, the ability to store a list of results in a variable, not just a single string. They can be assigned with the new `:let` filter run prefix, or the existing `<$let>` widget. The full list of values can be retrieved using round brackets instead of the usual angle brackets. In all other contexts only the first item in the list is used as the variable value. + +* [[Multi-Valued Variables]] +* [[Let Filter Run Prefix]] +* [[LetWidget]] diff --git a/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid b/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid index 47d011885..ab7933bc2 100644 --- a/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid +++ b/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid @@ -3,11 +3,5 @@ modified: 20140912135951542 title: $:/DefaultTiddlers type: text/vnd.tiddlywiki -[[Hire the founder of TiddlyWiki]] -HelloThere -[[Quick Start]] -[[Find Out More]] -[[TiddlyWiki on the Web]] -[[Testimonials and Reviews]] -GettingStarted -Community +[[Multi-Valued Variables]] +[[Let Filter Run Prefix]] diff --git a/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid b/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid new file mode 100644 index 000000000..7b0d8a588 --- /dev/null +++ b/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid @@ -0,0 +1,48 @@ +title: Multi-Valued Variables +created: 20250307212252946 +modified: 20250307212252946 +tags: Concepts Variables + +<<.from-version "5.4.0">> In ordinary usage, [[variables|Variables]] contain a single snippet of text. With the introduction of multi-valued variables. it is now possible to store a list of multiple values in a single variable. When accessed in the usual way, only the first value is returned, but using round brackets instead of angle brackets around the variable name allows access to the complete list of the values. This makes multi-valued variables largely invisible unless you specifically need to use them. + +! Setting Multi-Valued Variables + +!! LetWidget + +The <<.wid let>> widget allows multi-valued variables to be set in one operation, each to the complete list of results obtained from evaluating an attribute that is defined via a filtered transclusion. For example: + +``` +<$let + varname={{{ [all[tiddlers]sort[]] }}} +> +``` + +The <<.wid let>> widget also allows the complete list of return values from a function to be assigned to a multi-valued variable. For example: + +``` +<$let + varname=<> +> +``` + +!! [[Let Filter Run Prefix]] + +The `:let` filter run prefix (or its shortcut syntax `=>`) assigns the complete list of results of a filter run to a multi-valued variable. + +! Retrieving Multi-Valued Variables + +!! [[title Operator]] + +!! Multi-valued Parameters for Filter Operators + + + +! Examples + +For example: + +``` +<$let varname={{{ [all[tiddlers]sort[]] }}}> +<$text text={{{ [(varname)] +[join[-]] }}}/> + +``` diff --git a/editions/tw5.com/tiddlers/variables/Variables.tid b/editions/tw5.com/tiddlers/variables/Variables.tid index 43387eb4f..a4c38c893 100644 --- a/editions/tw5.com/tiddlers/variables/Variables.tid +++ b/editions/tw5.com/tiddlers/variables/Variables.tid @@ -1,13 +1,15 @@ created: 20141002133113496 -modified: 20240422084347375 +modified: 20250307212252946 tags: Concepts WikiText title: Variables type: text/vnd.tiddlywiki !! Introduction -* A <<.def variable>> is a ''snippet of text'' that can be accessed by name. -* The text is referred to as the variable's <<.def value>>. +* A <<.def variable>> is a ''snippet of text'' that can be accessed by name +* The text is referred to as the variable's <<.def value>> + +<<.from-version "5.4.0">> In ordinary usage, variables contain a single snippet of text. With the introduction of [[Multi-Valued Variables]] it is possible to store a list of multiple values in a single variable. When accessed in the usual way, only the first value is returned, but using round brackets instead of the usual angle brackets retrieves the complete list of values. This makes the existence of multi-valued variables invisible unless you specifically need to use them. Variables are defined by [[widgets|Widgets]]. Several core widgets define variables, the most common being the <<.wlink LetWidget>>, <<.wlink SetWidget>> and <<.wlink ListWidget>> widgets. diff --git a/editions/tw5.com/tiddlers/widgets/ActionLogWidget.tid b/editions/tw5.com/tiddlers/widgets/ActionLogWidget.tid index 2fa75cfad..6a807fcf8 100644 --- a/editions/tw5.com/tiddlers/widgets/ActionLogWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ActionLogWidget.tid @@ -25,6 +25,7 @@ In addition there are optional attributes that can be used: |$$message |A message to display as the title of the information logged. Useful when several `action-log` widgets are used in sequence. | |$$all |Set to "yes" to log all variables in a collapsed table. Note that if there is nothing specified to log, all variables are always logged instead.| +<<.from-version "5.4.0">> Any [[multi-valued variables|Multi-Valued Variables]] or attributes are logged as a list of values. <<.tip """A handy tip if an action widget is not behaving as expected is to temporarily change it to an `<$action-log>` widget so that the attributes can be observed.""">> diff --git a/editions/tw5.com/tiddlers/widgets/LetWidget.tid b/editions/tw5.com/tiddlers/widgets/LetWidget.tid index b27306cfb..bcb779d9c 100644 --- a/editions/tw5.com/tiddlers/widgets/LetWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/LetWidget.tid @@ -1,12 +1,12 @@ title: LetWidget created: 20211028115900000 -modified: 20221001094658854 +modified: 20250307212252946 tags: Widgets caption: let ! Introduction -<<.from-version "5.2.1">> The <<.wid let>> widget allows multiple variables to be set in one operation. In some situations it can result in simpler code than using the more flexible <<.wlink SetWidget>> widget. It differs from the <<.wlink VarsWidget>> widget in that variables you're defining may depend on earlier variables defined within the same <<.wid let>>. +<<.from-version "5.2.1">> The <<.wid let>> widget allows more than one variable to be set in one operation. In some situations it can result in simpler code than using the more flexible <<.wlink SetWidget>> widget. It differs from the <<.wlink VarsWidget>> widget in that variables you're defining may depend on earlier variables defined within the same <<.wid let>>. ! Content and Attributes @@ -19,6 +19,18 @@ Attributes are evaluated in the order they are written. Attributes with the same <<.note """<<.from-version "5.2.4">> There is no longer any restriction on using variable names that start with the $ character.""">> +! Multi-Valued Variables + +<<.from-version "5.4.0">> The <<.wid let>> widget also allows [[multi-valued variables|Multi-Valued Variables]] to be set in one operation to the complete list of results obtained from evaluating an attribute. + +Almost all operations that work with variables only consider the first item in the list. Using round brackets instead of angle brackets around the variable name gives access to the complete list of results. For example: + +``` +<$let varname={{{ [all[tiddlers]sort[]] }}}> +<$text text={{{ [(varname)] +[join[-]] }}}/> + +``` + ! Examples Consider a case where you need to set multiple variables, where some depend on the evaluation of others.