From 64ee20edd2ea1e2674b0118143ad1a93764d0983 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 10 Feb 2026 11:16:20 +0000 Subject: [PATCH] Further MVV fixes (#9645) * Add ((var)) syntax for passing multi-valued variables through transclude pipeline Introduce ((var)) attribute syntax to explicitly pass MVVs to procedures and functions via $transclude, solving the limitation where <> always resolves to the first value only for backwards compatibility. Also adds ((var||sep)) and (((filter||sep))) inline display syntax for debugging MVV values, and multivalued defaults for parameter attributes * Create pr-draft.md * Revert "Create pr-draft.md" This reverts commit dd116af41b53f1326bd1067e51c47d6293f5f050. * Update change note * Fix linting errors --- core/modules/parsers/parseutils.js | 115 ++++++++++++++---- .../parsers/wikiparser/rules/fnprocdef.js | 2 +- .../wikiparser/rules/mvvdisplayinline.js | 95 +++++++++++++++ core/modules/widgets/parameters.js | 7 +- core/modules/widgets/transclude.js | 28 ++++- core/modules/widgets/widget.js | 20 ++- .../AttributeFirstValue.tid | 16 +++ .../DefaultParameterMVV.tid | 19 +++ .../multi-valued-variables/InlineDisplay.tid | 16 +++ .../InlineDisplaySeparator.tid | 16 +++ .../InlineFilterDisplay.tid | 14 +++ .../InlineFilterDisplaySeparator.tid | 14 +++ .../TranscludeParameter.tid | 19 +++ .../TranscludeParameterFunction.tid | 17 +++ .../tiddlers/pragmas/Pragma_ _function.tid | 2 +- .../tiddlers/pragmas/Pragma_ _procedure.tid | 2 +- .../tiddlers/releasenotes/5.4.0/#8972.tid | 2 +- ...ariables invoked via widget attributes.tid | 4 +- .../variables/Multi-Valued Variables.tid | 49 +++++++- ...Multi-Valued Variable Attribute Values.tid | 37 ++++++ .../wikitext/Transclusion in WikiText.tid | 22 ++++ .../tiddlers/wikitext/Widget Attributes.tid | 4 +- 22 files changed, 478 insertions(+), 42 deletions(-) create mode 100644 core/modules/parsers/wikiparser/rules/mvvdisplayinline.js create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/AttributeFirstValue.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/DefaultParameterMVV.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplay.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplaySeparator.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplay.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplaySeparator.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameter.tid create mode 100644 editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameterFunction.tid create mode 100644 editions/tw5.com/tiddlers/wikitext/Multi-Valued Variable Attribute Values.tid diff --git a/core/modules/parsers/parseutils.js b/core/modules/parsers/parseutils.js index 250a82baed..1708c8f999 100644 --- a/core/modules/parsers/parseutils.js +++ b/core/modules/parsers/parseutils.js @@ -143,7 +143,14 @@ exports.parseParameterDefinition = function(paramString,options) { var paramInfo = {name: paramMatch[1]}, defaultValue = paramMatch[2] || paramMatch[3] || paramMatch[4] || paramMatch[5]; if(defaultValue !== undefined) { - paramInfo["default"] = defaultValue; + // Check for an MVV reference ((varname)) + var mvvDefaultMatch = /^\(\(([^)|]+)\)\)$/.exec(defaultValue); + if(mvvDefaultMatch) { + paramInfo.defaultType = "multivalue-variable"; + paramInfo.defaultVariable = mvvDefaultMatch[1]; + } else { + paramInfo["default"] = defaultValue; + } } params.push(paramInfo); // Look for the next parameter @@ -247,6 +254,46 @@ exports.parseMacroInvocationAsTransclusion = function(source,pos) { return node; }; +/* +Look for an MVV (multi-valued variable) reference as a transclusion, i.e. ((varname)) or ((varname params)) +Returns null if not found, or a parse tree node of type "transclude" with isMVV: true +*/ +exports.parseMVVReferenceAsTransclusion = function(source,pos) { + var node = { + type: "transclude", + isMVV: true, + start: pos, + attributes: {}, + orderedAttributes: [] + }; + // Define our regexps + var reVarName = /([^\s>"'=:)]+)/g; + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Look for a double opening parenthesis + var token = $tw.utils.parseTokenString(source,pos,"(("); + if(!token) { + return null; + } + pos = token.end; + // Get the variable name + token = $tw.utils.parseTokenRegExp(source,pos,reVarName); + if(!token) { + return null; + } + $tw.utils.addAttributeToParseTreeNode(node,"$variable",token.match[1]); + pos = token.end; + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Look for a double closing parenthesis + token = $tw.utils.parseTokenString(source,pos,"))"); + if(!token) { + return null; + } + node.end = token.end; + return node; +}; + /* Parse macro parameters as attributes. Returns the position after the last attribute */ @@ -321,19 +368,20 @@ exports.parseMacroParameterAsAttribute = function(source,pos) { node.type = "indirect"; node.textReference = indirectValue.match[1]; } else { - // Look for a unquoted value - var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute); - if(unquotedValue) { - pos = unquotedValue.end; - node.type = "string"; - node.value = unquotedValue.match[1]; + // Look for a macro invocation value + var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos); + if(macroInvocation && isNewStyleSeparator) { + pos = macroInvocation.end; + node.type = "macro"; + node.value = macroInvocation; } else { - // Look for a macro invocation value - var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos); - if(macroInvocation && isNewStyleSeparator) { - pos = macroInvocation.end; + // Look for an MVV reference value + var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos); + if(mvvReference && isNewStyleSeparator) { + pos = mvvReference.end; node.type = "macro"; - node.value = macroInvocation; + node.value = mvvReference; + node.isMVV = true; } else { var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue); if(substitutedValue && isNewStyleSeparator) { @@ -341,6 +389,14 @@ exports.parseMacroParameterAsAttribute = function(source,pos) { node.type = "substituted"; node.rawValue = substitutedValue.match[1] || substitutedValue.match[2]; } else { + // Look for a unquoted value + var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute); + if(unquotedValue) { + pos = unquotedValue.end; + node.type = "string"; + node.value = unquotedValue.match[1]; + } else { + } } } } @@ -471,19 +527,20 @@ exports.parseAttribute = function(source,pos) { node.type = "indirect"; node.textReference = indirectValue.match[1]; } else { - // Look for a unquoted value - var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute); - if(unquotedValue) { - pos = unquotedValue.end; - node.type = "string"; - node.value = unquotedValue.match[1]; + // Look for a macro invocation value + var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos); + if(macroInvocation) { + pos = macroInvocation.end; + node.type = "macro"; + node.value = macroInvocation; } else { - // Look for a macro invocation value - var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos); - if(macroInvocation) { - pos = macroInvocation.end; + // Look for an MVV reference value + var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos); + if(mvvReference) { + pos = mvvReference.end; node.type = "macro"; - node.value = macroInvocation; + node.value = mvvReference; + node.isMVV = true; } else { var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue); if(substitutedValue) { @@ -491,8 +548,16 @@ exports.parseAttribute = function(source,pos) { node.type = "substituted"; node.rawValue = substitutedValue.match[1] || substitutedValue.match[2]; } else { - node.type = "string"; - node.value = "true"; + // Look for a unquoted value + var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute); + if(unquotedValue) { + pos = unquotedValue.end; + node.type = "string"; + node.value = unquotedValue.match[1]; + } else { + node.type = "string"; + node.value = "true"; + } } } } diff --git a/core/modules/parsers/wikiparser/rules/fnprocdef.js b/core/modules/parsers/wikiparser/rules/fnprocdef.js index 8c71372c5c..01184497a3 100644 --- a/core/modules/parsers/wikiparser/rules/fnprocdef.js +++ b/core/modules/parsers/wikiparser/rules/fnprocdef.js @@ -32,7 +32,7 @@ Instantiate parse rule exports.init = function(parser) { this.parser = parser; // Regexp to match - this.matchRegExp = /\\(function|procedure|widget)\s+([^(\s]+)\((\s*([^)]*))?\)(\s*\r?\n)?/mg; + this.matchRegExp = /\\(function|procedure|widget)\s+([^(\s]+)\((\s*([^)]*(?:\)\)[^)]*)*))?\)(\s*\r?\n)?/mg; }; /* diff --git a/core/modules/parsers/wikiparser/rules/mvvdisplayinline.js b/core/modules/parsers/wikiparser/rules/mvvdisplayinline.js new file mode 100644 index 0000000000..f826b5e5dd --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/mvvdisplayinline.js @@ -0,0 +1,95 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/mvvdisplayinline.js +type: application/javascript +module-type: wikirule + +Wiki rule for inline display of multi-valued variables and filter results. + +Variable display: ((varname)) or ((varname||separator)) +Filter display: (((filter))) or (((filter||separator))) + +The default separator is ", " (comma space). + +\*/ + +"use strict"; + +exports.name = "mvvdisplayinline"; +exports.types = {inline: true}; + +exports.init = function(parser) { + this.parser = parser; +}; + +exports.findNextMatch = function(startPos) { + var source = this.parser.source; + var nextStart = startPos; + while((nextStart = source.indexOf("((",nextStart)) >= 0) { + if(source.charAt(nextStart + 2) === "(") { + // Filter mode: (((filter))) or (((filter||sep))) + var match = /^\(\(\(([\s\S]+?)\)\)\)/.exec(source.substring(nextStart)); + if(match) { + // Check for separator: split on last || before ))) + var inner = match[1]; + var sepIndex = inner.lastIndexOf("||"); + if(sepIndex >= 0) { + this.nextMatch = { + type: "filter", + filter: inner.substring(0,sepIndex), + separator: inner.substring(sepIndex + 2), + start: nextStart, + end: nextStart + match[0].length + }; + } else { + this.nextMatch = { + type: "filter", + filter: inner, + separator: ", ", + start: nextStart, + end: nextStart + match[0].length + }; + } + return nextStart; + } + } else { + // Variable mode: ((varname)) or ((varname||sep)) + var match = /^\(\(([^()|]+?)(?:\|\|([^)]*))?\)\)/.exec(source.substring(nextStart)); + if(match) { + this.nextMatch = { + type: "variable", + varName: match[1], + separator: match[2] !== undefined ? match[2] : ", ", + start: nextStart, + end: nextStart + match[0].length + }; + return nextStart; + } + } + nextStart += 2; + } + return undefined; +}; + +/* +Parse the most recent match +*/ +exports.parse = function() { + var match = this.nextMatch; + this.nextMatch = null; + this.parser.pos = match.end; + var filter, sep = match.separator; + if(match.type === "variable") { + filter = "[(" + match.varName + ")join[" + sep + "]]"; + } else { + filter = match.filter + " +[join[" + sep + "]]"; + } + return [{ + type: "text", + attributes: { + text: {name: "text", type: "filtered", filter: filter} + }, + orderedAttributes: [ + {name: "text", type: "filtered", filter: filter} + ] + }]; +}; diff --git a/core/modules/widgets/parameters.js b/core/modules/widgets/parameters.js index 638db80542..e6cc6b4dfc 100644 --- a/core/modules/widgets/parameters.js +++ b/core/modules/widgets/parameters.js @@ -61,7 +61,9 @@ ParametersWidget.prototype.execute = function() { if(name.substr(0,2) === "$$") { name = name.substr(1); } - var value = pointer.getTransclusionParameter(name,index,self.getAttribute(attr.name,"")); + var defaultValue = (self.multiValuedAttributes && self.multiValuedAttributes[attr.name]) + || self.getAttribute(attr.name,""); + var value = pointer.getTransclusionParameter(name,index,defaultValue); self.setVariable(name,value); }); // Assign any metaparameters @@ -80,7 +82,8 @@ ParametersWidget.prototype.execute = function() { if(name.substr(0,2) === "$$") { name = name.substr(1); } - var value = self.getAttribute(attr.name,""); + var value = (self.multiValuedAttributes && self.multiValuedAttributes[attr.name]) + || self.getAttribute(attr.name,""); self.setVariable(name,value); }); } diff --git a/core/modules/widgets/transclude.js b/core/modules/widgets/transclude.js index f1ed74e348..0b6a5a1493 100755 --- a/core/modules/widgets/transclude.js +++ b/core/modules/widgets/transclude.js @@ -158,8 +158,10 @@ Collect string parameters TranscludeWidget.prototype.collectStringParameters = function() { var self = this; this.stringParametersByName = Object.create(null); + this.multiValuedParametersByName = Object.create(null); if(!this.legacyMode) { $tw.utils.each(this.attributes,function(value,name) { + var attrName = name; // Save original attribute name for MVV lookup if(name.charAt(0) === "$") { if(name.charAt(1) === "$") { // Attributes starting $$ represent parameters starting with a single $ @@ -170,6 +172,9 @@ TranscludeWidget.prototype.collectStringParameters = function() { } } self.stringParametersByName[name] = value; + if(self.multiValuedAttributes && self.multiValuedAttributes[attrName]) { + self.multiValuedParametersByName[name] = self.multiValuedAttributes[attrName]; + } }); } }; @@ -313,7 +318,16 @@ TranscludeWidget.prototype.parseTransclusionTarget = function(parseAsInline) { if(name.charAt(0) === "$") { name = "$" + name; } - $tw.utils.addAttributeToParseTreeNode(parser.tree[0],name,param["default"]) + if(param.defaultType === "multivalue-variable") { + // Construct MVV attribute for the default + var mvvNode = {type: "transclude", isMVV: true, attributes: {}, orderedAttributes: []}; + $tw.utils.addAttributeToParseTreeNode(mvvNode,"$variable",param.defaultVariable); + $tw.utils.addAttributeToParseTreeNode(parser.tree[0],{ + name: name, type: "macro", isMVV: true, value: mvvNode + }); + } else { + $tw.utils.addAttributeToParseTreeNode(parser.tree[0],name,param["default"]); + } }); } else if(srcVariable && !srcVariable.isFunctionDefinition) { // For macros and ordinary variables, wrap the parse tree in a vars widget assigning the parameters to variables named "__paramname__" @@ -364,7 +378,11 @@ TranscludeWidget.prototype.getOrderedTransclusionParameters = function() { // Collect the parameters for(var name in this.stringParametersByName) { var value = this.stringParametersByName[name]; - result.push({name: name, value: value}); + var param = {name: name, value: value}; + if(this.multiValuedParametersByName[name]) { + param.multiValue = this.multiValuedParametersByName[name]; + } + result.push(param); } // Sort numerical parameter names first result.sort(function(a,b) { @@ -394,10 +412,16 @@ Fetch the value of a parameter */ TranscludeWidget.prototype.getTransclusionParameter = function(name,index,defaultValue) { if(name in this.stringParametersByName) { + if(this.multiValuedParametersByName[name]) { + return this.multiValuedParametersByName[name]; + } return this.stringParametersByName[name]; } else { var name = "" + index; if(name in this.stringParametersByName) { + if(this.multiValuedParametersByName[name]) { + return this.multiValuedParametersByName[name]; + } return this.stringParametersByName[name]; } } diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index a7cb84e01e..dfa9b6be5e 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -381,19 +381,31 @@ filterFn: only include attributes where filterFn(name) returns true Widget.prototype.computeAttributes = function(options) { options = options || {}; var changedAttributes = {}, - self = this; + self = this, + newMultiValuedAttributes = Object.create(null); $tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) { if(options.filterFn) { if(!options.filterFn(name)) { return; } } - var value = self.computeAttribute(attribute); - if(self.attributes[name] !== value) { + var value = self.computeAttribute(attribute), + multiValue = null; + if($tw.utils.isArray(value)) { + multiValue = value; + newMultiValuedAttributes[name] = multiValue; + value = value[0] || ""; + } + var changed = (self.attributes[name] !== value); + if(!changed && multiValue && self.multiValuedAttributes) { + changed = !$tw.utils.isArrayEqual(self.multiValuedAttributes[name] || [], multiValue); + } + if(changed) { self.attributes[name] = value; changedAttributes[name] = true; } }); + this.multiValuedAttributes = newMultiValuedAttributes; return changedAttributes; }; @@ -431,7 +443,7 @@ Widget.prototype.computeAttribute = function(attribute,options) { }); // Invoke the macro var variableInfo = this.getVariableInfo(macroName,{params: params}); - if(options.asList) { + if(options.asList || attribute.isMVV) { value = variableInfo.resultList; } else { value = variableInfo.text; diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/AttributeFirstValue.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/AttributeFirstValue.tid new file mode 100644 index 0000000000..9f7fe150c5 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/AttributeFirstValue.tid @@ -0,0 +1,16 @@ +title: MultiValuedVariables/AttributeFirstValue +description: ((var)) on non-MVV-aware widget attribute returns first value only +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$let items={{{ [all[tiddlers]sort[]] }}}> +<$text text=((items))/> + ++ +title: ExpectedResult + +

$:/core

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/DefaultParameterMVV.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/DefaultParameterMVV.tid new file mode 100644 index 0000000000..6a47f5d3ef --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/DefaultParameterMVV.tid @@ -0,0 +1,19 @@ +title: MultiValuedVariables/DefaultParameterMVV +description: Procedure default parameter value using ((var)) syntax to provide MVV default +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +\procedure showItems(itemList:((defaults))) +<$text text={{{ [(itemList)join[-]] }}}/> +\end +<$let defaults={{{ [all[tiddlers]sort[]] }}}> +<> + ++ +title: ExpectedResult + +

$:/core-ExpectedResult-Output

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplay.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplay.tid new file mode 100644 index 0000000000..5cac7de8fe --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplay.tid @@ -0,0 +1,16 @@ +title: MultiValuedVariables/InlineDisplay +description: ((var)) in inline wikitext displays MVV with default comma-space separator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$let items={{{ [all[tiddlers]sort[]] }}}> +((items)) + ++ +title: ExpectedResult + +

$:/core, ExpectedResult, Output

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplaySeparator.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplaySeparator.tid new file mode 100644 index 0000000000..5074f3b4b4 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineDisplaySeparator.tid @@ -0,0 +1,16 @@ +title: MultiValuedVariables/InlineDisplaySeparator +description: ((var||separator)) in inline wikitext displays MVV with custom separator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$let items={{{ [all[tiddlers]sort[]] }}}> +((items||:)) + ++ +title: ExpectedResult + +

$:/core:ExpectedResult:Output

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplay.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplay.tid new file mode 100644 index 0000000000..8b418d6d1d --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplay.tid @@ -0,0 +1,14 @@ +title: MultiValuedVariables/InlineFilterDisplay +description: (((filter))) in inline wikitext displays filter results with default comma-space separator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +((([all[tiddlers]sort[]]))) ++ +title: ExpectedResult + +

$:/core, ExpectedResult, Output

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplaySeparator.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplaySeparator.tid new file mode 100644 index 0000000000..9147e49c16 --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/InlineFilterDisplaySeparator.tid @@ -0,0 +1,14 @@ +title: MultiValuedVariables/InlineFilterDisplaySeparator +description: (((filter||separator))) in inline wikitext displays filter results with custom separator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +((([all[tiddlers]sort[]]||:))) ++ +title: ExpectedResult + +

$:/core:ExpectedResult:Output

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameter.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameter.tid new file mode 100644 index 0000000000..b7975b788b --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameter.tid @@ -0,0 +1,19 @@ +title: MultiValuedVariables/TranscludeParameter +description: Multi-valued variable passed as procedure parameter via ((var)) syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +\procedure showItems(itemList) +<$text text={{{ [(itemList)join[-]] }}}/> +\end +<$let items={{{ [all[tiddlers]sort[]] }}}> +<$transclude $variable="showItems" itemList=((items))/> + ++ +title: ExpectedResult + +

$:/core-ExpectedResult-Output

++ diff --git a/editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameterFunction.tid b/editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameterFunction.tid new file mode 100644 index 0000000000..0deca01d8a --- /dev/null +++ b/editions/test/tiddlers/tests/data/multi-valued-variables/TranscludeParameterFunction.tid @@ -0,0 +1,17 @@ +title: MultiValuedVariables/TranscludeParameterFunction +description: Multi-valued variable passed as function parameter via ((var)) in $transclude +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +\function showItems(itemList) [(itemList)sort[]join[-]] +<$let items={{{ [all[tiddlers]] }}}> +<$transclude $variable="showItems" itemList=((items))/> + ++ +title: ExpectedResult + +

$:/core-ExpectedResult-Output

++ diff --git a/editions/tw5.com/tiddlers/pragmas/Pragma_ _function.tid b/editions/tw5.com/tiddlers/pragmas/Pragma_ _function.tid index 253c8b452a..876da22f46 100644 --- a/editions/tw5.com/tiddlers/pragmas/Pragma_ _function.tid +++ b/editions/tw5.com/tiddlers/pragmas/Pragma_ _function.tid @@ -22,6 +22,6 @@ There is also a single line form for shorter functions: \function ([:],[:]...) ``` -The first line of the definition specifies the function name and any parameters. Each parameter has a name and, optionally, a default value that is used if no value is supplied on a particular call to the function. The lines that follow contain the text of the function (i.e. the snippet represented by the function name), until `\end` appears on a line by itself: +The first line of the definition specifies the function name and any parameters. Each parameter has a name and, optionally, a default value that is used if no value is supplied on a particular call to the function. <<.from-version "5.4.0">> The default value can also be a [[multi-valued variable reference|Multi-Valued Variables]] using the `((var))` syntax (e.g. `\function show(items:((defaults)))`). The lines that follow contain the text of the function (i.e. the snippet represented by the function name), until `\end` appears on a line by itself: See [[Functions]] for more details. \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/pragmas/Pragma_ _procedure.tid b/editions/tw5.com/tiddlers/pragmas/Pragma_ _procedure.tid index 06b0251d5b..e1ac5d1b75 100644 --- a/editions/tw5.com/tiddlers/pragmas/Pragma_ _procedure.tid +++ b/editions/tw5.com/tiddlers/pragmas/Pragma_ _procedure.tid @@ -22,7 +22,7 @@ There is also a single line form for shorter procedures: \procedure ([:],[:]...) ``` -The first line of the definition specifies the procedure name and any parameters. Each parameter has a name and, optionally, a default value that is used if no value is supplied on a particular call to the procedure. The lines that follow contain the text of the procedure text (i.e. the snippet represented by the procedure name), until `\end` appears on a line by itself: +The first line of the definition specifies the procedure name and any parameters. Each parameter has a name and, optionally, a default value that is used if no value is supplied on a particular call to the procedure. <<.from-version "5.4.0">> The default value can also be a [[multi-valued variable reference|Multi-Valued Variables]] using the `((var))` syntax (e.g. `\procedure show(items:((defaults)))`). The lines that follow contain the text of the procedure text (i.e. the snippet represented by the procedure name), until `\end` appears on a line by itself: For example: diff --git a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid index bebbb5a6e1..49ed52412b 100644 --- a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid +++ b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972.tid @@ -4,7 +4,7 @@ release: 5.4.0 tags: $:/tags/ChangeNote change-type: enhancement change-category: hackability -github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/8972 https://github.com/TiddlyWiki/TiddlyWiki5/pull/9614 +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/8972 https://github.com/TiddlyWiki/TiddlyWiki5/pull/9614 https://github.com/TiddlyWiki/TiddlyWiki5/pull/9645 github-contributors: Jermolene saqimtiaz 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. diff --git a/editions/tw5.com/tiddlers/variables/Behaviour of variables invoked via widget attributes.tid b/editions/tw5.com/tiddlers/variables/Behaviour of variables invoked via widget attributes.tid index 3784c94e34..4baa7672bd 100644 --- a/editions/tw5.com/tiddlers/variables/Behaviour of variables invoked via widget attributes.tid +++ b/editions/tw5.com/tiddlers/variables/Behaviour of variables invoked via widget attributes.tid @@ -8,4 +8,6 @@ type: text/vnd.tiddlywiki |!how declared|!behaviour| |\define|Textual substitution of parameters is performed on the body text. No further processing takes place. The result after textual substitution is used as the attribute's value| |<<.wlink SetWidget>>, <<.wlink LetWidget>>, <<.wlink VarsWidget>>, \procedure, \widget|Body text is retrieved as-is and used as the attribute's value.| -|\function|When a function (e.g. `.myfun`) is invoked as `
>/>`, it is a synonym for `
`. As with any filtered transclusion (i.e. triple curly braces), all results except the first are discarded. That first result is used as the attribute's value. Note that functions are recursively processed even when invoked in this form. In other words a filter expression in a function can invoke another function and the processing will continue| \ No newline at end of file +|\function|When a function (e.g. `.myfun`) is invoked as `
>/>`, it is a synonym for `
`. As with any filtered transclusion (i.e. triple curly braces), all results except the first are discarded. That first result is used as the attribute's value. Note that functions are recursively processed even when invoked in this form. In other words a filter expression in a function can invoke another function and the processing will continue| + +<<.from-version "5.4.0">> Using the [[multi-valued variable attribute|Multi-Valued Variable Attribute Values]] syntax `((var))` instead of `<>` passes the complete list of values to the attribute rather than just the first value. This is primarily useful for passing [[multi-valued variables|Multi-Valued Variables]] to procedure and function parameters via the TranscludeWidget. \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid b/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid index 3d239034ef..6e23678e0b 100644 --- a/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid +++ b/editions/tw5.com/tiddlers/variables/Multi-Valued Variables.tid @@ -59,14 +59,57 @@ For example: ``` \function myfunc(tiddlers) [(tiddlers)sort[]] -<$let varname={{{ [all[tiddlers]limit[50]] }}}> +<$let varname={{{ [all[tiddlers]limit[50]] }}}> <$text text={{{ [function[myfunc],(varname)] +[join[-]] }}}/> ``` -! Examples +!! Passing Multi-Valued Variables to Procedures and Functions -For example: +<<.from-version "5.4.0">> Multi-valued variables can be passed to procedures and functions using the `((var))` syntax in [[widget attributes|Multi-Valued Variable Attribute Values]]. This is the multi-valued counterpart of the `<>` syntax: + +``` +\procedure showItems(itemList) +<$text text={{{ [(itemList)join[-]] }}}/> +\end + +<$let items={{{ [all[tiddlers]sort[]] }}}> +<$transclude $variable="showItems" itemList=((items))/> + +``` + +The `((var))` syntax can also be used in procedure and function parameter defaults: + +``` +\procedure showItems(itemList:((defaults))) +<$text text={{{ [(itemList)join[-]] }}}/> +\end +``` + +! Displaying Multi-Valued Variables + +<<.from-version "5.4.0">> Multi-valued variables can be displayed inline in wikitext using the `((var))` syntax. The values are joined with a comma and space by default: + +``` +<$let items={{{ [all[tiddlers]sort[]] }}}> +((items)) + +``` + +A custom separator can be specified using the `||` delimiter: + +``` +((items||:)) +``` + +A similar syntax with triple round brackets is available for displaying the results of a filter expression: + +``` +((( [all[tiddlers]sort[]] ))) +((( [all[tiddlers]sort[]] ||: ))) +``` + +! Examples ``` <$let varname={{{ [all[tiddlers]sort[]] }}}> diff --git a/editions/tw5.com/tiddlers/wikitext/Multi-Valued Variable Attribute Values.tid b/editions/tw5.com/tiddlers/wikitext/Multi-Valued Variable Attribute Values.tid new file mode 100644 index 0000000000..b26dd8b9ba --- /dev/null +++ b/editions/tw5.com/tiddlers/wikitext/Multi-Valued Variable Attribute Values.tid @@ -0,0 +1,37 @@ +created: 20250208120000000 +modified: 20250208120000000 +tags: WikiText [[Widget Attributes]] +title: Multi-Valued Variable Attribute Values +type: text/vnd.tiddlywiki + +<<.from-version "5.4.0">> Multi-valued variable attribute values are indicated with double round brackets around the variable name. This passes the complete list of values of a [[multi-valued variable|Multi-Valued Variables]] to the attribute, rather than just the first value. + +``` +<$transclude $variable="myproc" items=((myvar))/> +``` + +This is the multi-valued counterpart to the [[Variable Attribute Values]] syntax `<>`, which only returns the first value for backwards compatibility. The relationship mirrors the existing convention in filter operands where `` returns a single value and `(var)` returns all values. + +! Non-MVV-Aware Attributes + +When `((var))` is used on a widget attribute that does not support multi-valued variables (such as the `text` attribute of the TextWidget), only the first value is used: + +``` +<$text text=((myvar))/> +``` + +! Passing Multi-Valued Variables to Procedures + +The primary use case for this syntax is passing multi-valued variables through the `$transclude` pipeline to procedures and functions: + +``` +\procedure showItems(itemList) +<$text text={{{ [(itemList)join[-]] }}}/> +\end + +<$let items={{{ [all[tiddlers]sort[]] }}}> +<$transclude $variable="showItems" itemList=((items))/> + +``` + +In this example, the complete list of tiddler titles stored in `items` is passed to the `itemList` parameter of the `showItems` procedure. diff --git a/editions/tw5.com/tiddlers/wikitext/Transclusion in WikiText.tid b/editions/tw5.com/tiddlers/wikitext/Transclusion in WikiText.tid index 29f8bf9f18..bd6f0ef08e 100644 --- a/editions/tw5.com/tiddlers/wikitext/Transclusion in WikiText.tid +++ b/editions/tw5.com/tiddlers/wikitext/Transclusion in WikiText.tid @@ -62,6 +62,28 @@ or, when used with a template, `{{{ [tag[mechanism]]||TemplateTitle }}}` expands <<.tip "Install the //Internals// plugin to enable the display of the generated widget tree in the preview pane of the editor">> +!! Multi-Valued Variable Display + +<<.from-version "5.4.0">> The `((var))` syntax can be used inline to display the values of a [[multi-valued variable|Multi-Valued Variables]], joined with a comma and space by default: + +``` +((myvar)) +((myvar||:)) +``` + +The optional `||` delimiter specifies a custom separator string. + +!! Inline Filter Display + +<<.from-version "5.4.0">> The `(((filter)))` syntax displays the results of a filter expression inline, joined with a comma and space by default: + +``` +((( [all[tiddlers]sort[]] ))) +((( [all[tiddlers]sort[]] ||: ))) +``` + +The optional `||` delimiter specifies a custom separator string. This is the inline display counterpart to the filtered transclusion `{{{ }}}` syntax. + --- See also: diff --git a/editions/tw5.com/tiddlers/wikitext/Widget Attributes.tid b/editions/tw5.com/tiddlers/wikitext/Widget Attributes.tid index 02b3a540e8..1d805aecd4 100644 --- a/editions/tw5.com/tiddlers/wikitext/Widget Attributes.tid +++ b/editions/tw5.com/tiddlers/wikitext/Widget Attributes.tid @@ -11,6 +11,7 @@ Attributes of [[HTML elements|HTML in WikiText]] and widgets can be specified in * [[a transclusion of a macro/variable|Variable Attribute Values]] * [[as the result of a filter expression|Filtered Attribute Values]] * <<.from-version "5.3.0">> [[as the result of performing filter and variable substitutions on the given string|Substituted Attribute Values]] +* <<.from-version "5.4.0">> [[as a multi-valued variable reference|Multi-Valued Variable Attribute Values]] |attribute type|syntax|h |literal |single, double or triple quotes or no quotes for values without spaces | @@ -18,9 +19,10 @@ Attributes of [[HTML elements|HTML in WikiText]] and widgets can be specified in |variable |double angle brackets around a macro or variable invocation | |filtered |triple curly braces around a filter expression| |substituted|single or triple backticks around the text to be processed for substitutions| +|multi-valued variable |double round brackets around a variable name | -<$list filter="[[Literal Attribute Values]] [[Transcluded Attribute Values]] [[Variable Attribute Values]] [[Filtered Attribute Values]] [[Substituted Attribute Values]]"> +<$list filter="[[Literal Attribute Values]] [[Transcluded Attribute Values]] [[Variable Attribute Values]] [[Filtered Attribute Values]] [[Substituted Attribute Values]] [[Multi-Valued Variable Attribute Values]]"> <$link>

<$text text=<>/>

<$transclude mode="block"/>