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))/> +$let> ++ +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[]] }}}> +<$:/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)) +$let> ++ +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||:)) +$let> ++ +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))/> +$let> ++ +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))/> +$let> ++ +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