diff --git a/core/modules/parsers/wikiparser/rules/fnprocdef.js b/core/modules/parsers/wikiparser/rules/fnprocdef.js index 15443bc27..17e069397 100644 --- a/core/modules/parsers/wikiparser/rules/fnprocdef.js +++ b/core/modules/parsers/wikiparser/rules/fnprocdef.js @@ -35,7 +35,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; }; /* @@ -46,12 +46,12 @@ exports.parse = function() { this.parser.pos = this.matchRegExp.lastIndex; // Parse the parameters var params = []; - if(this.match[3]) { - params = $tw.utils.parseParameterDefinition(this.match[4]); + if(this.match[4]) { + params = $tw.utils.parseParameterDefinition(this.match[5]); } // Is this a multiline definition? var reEnd; - if(this.match[5]) { + if(this.match[6]) { // If so, the end of the body is marked with \end reEnd = /(\r?\n\\end[^\S\n\r]*(?:$|\r?\n))/mg; } else { @@ -75,22 +75,25 @@ exports.parse = function() { var parseTreeNodes = [{ type: "set", attributes: { - name: {type: "string", value: this.match[2]}, + name: {type: "string", value: this.match[3]}, value: {type: "string", value: text} }, children: [], params: params }]; - if(this.match[1] === "function") { + if(this.match[2] === "function") { parseTreeNodes[0].isFunctionDefinition = true; - } else if(this.match[1] === "procedure") { + } else if(this.match[2] === "procedure") { parseTreeNodes[0].isProcedureDefinition = true; - } else if(this.match[1] === "widget") { + } else if(this.match[2] === "widget") { parseTreeNodes[0].isWidgetDefinition = true; } if(this.parser.configTrimWhiteSpace) { parseTreeNodes[0].configTrimWhiteSpace = true; } + if(this.match[1] === "+") { + $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],"conditional","yes"); + } return parseTreeNodes; }; diff --git a/core/modules/parsers/wikiparser/rules/macrodef.js b/core/modules/parsers/wikiparser/rules/macrodef.js index cc76ca7ec..c8e88f740 100644 --- a/core/modules/parsers/wikiparser/rules/macrodef.js +++ b/core/modules/parsers/wikiparser/rules/macrodef.js @@ -27,7 +27,7 @@ Instantiate parse rule exports.init = function(parser) { this.parser = parser; // Regexp to match - this.matchRegExp = /^\\define\s+([^(\s]+)\(\s*([^)]*)\)(\s*\r?\n)?/mg; + this.matchRegExp = /^\\(\+?)define\s+([^(\s]+)\(\s*([^)]*)\)(\s*\r?\n)?/mg; }; /* @@ -37,7 +37,7 @@ exports.parse = function() { // Move past the macro name and parameters this.parser.pos = this.matchRegExp.lastIndex; // Parse the parameters - var paramString = this.match[2], + var paramString = this.match[3], params = []; if(paramString !== "") { var reParam = /\s*([A-Za-z0-9\-_]+)(?:\s*:\s*(?:"""([\s\S]*?)"""|"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|([^"'\s]+)))?/mg, @@ -56,7 +56,7 @@ exports.parse = function() { } // Is this a multiline definition? var reEnd; - if(this.match[3]) { + if(this.match[4]) { // If so, the end of the body is marked with \end reEnd = /(\r?\n\\end[^\S\n\r]*(?:$|\r?\n))/mg; } else { @@ -77,16 +77,22 @@ exports.parse = function() { text = ""; } // Save the macro definition - return [{ + var parseTreeNodes = [{ type: "set", attributes: { - name: {type: "string", value: this.match[1]}, + name: {type: "string", value: this.match[2]}, value: {type: "string", value: text} }, children: [], params: params, isMacroDefinition: true }]; + $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],"name",this.match[2]); + $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],"value",text); + if(this.match[1] === "+") { + $tw.utils.addAttributeToParseTreeNode(parseTreeNodes[0],"conditional","yes"); + } + return parseTreeNodes; }; })(); diff --git a/core/modules/widgets/setvariable.js b/core/modules/widgets/setvariable.js index f8e98f390..06bc78678 100755 --- a/core/modules/widgets/setvariable.js +++ b/core/modules/widgets/setvariable.js @@ -47,17 +47,20 @@ SetWidget.prototype.execute = function() { this.setIndex = this.getAttribute("index"); this.setValue = this.getAttribute("value"); this.setEmptyValue = this.getAttribute("emptyValue"); - // Set context variable - if(this.parseTreeNode.isMacroDefinition) { - this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,true); - } else if(this.parseTreeNode.isFunctionDefinition) { - this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,undefined,{isFunctionDefinition: true}); - } else if(this.parseTreeNode.isProcedureDefinition) { - this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,undefined,{isProcedureDefinition: true, configTrimWhiteSpace: this.parseTreeNode.configTrimWhiteSpace}); - } else if(this.parseTreeNode.isWidgetDefinition) { - this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,undefined,{isWidgetDefinition: true, configTrimWhiteSpace: this.parseTreeNode.configTrimWhiteSpace}); - } else { - this.setVariable(this.setName,this.getValue()); + this.setConditional = this.getAttribute("conditional","no") === "yes"; + // Set context variable, checking for a conditional assignment + if(!this.setConditional || this.getVariableInfo(this.setName).text === undefined) { + if(this.parseTreeNode.isMacroDefinition) { + this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,true); + } else if(this.parseTreeNode.isFunctionDefinition) { + this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,undefined,{isFunctionDefinition: true}); + } else if(this.parseTreeNode.isProcedureDefinition) { + this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,undefined,{isProcedureDefinition: true, configTrimWhiteSpace: this.parseTreeNode.configTrimWhiteSpace}); + } else if(this.parseTreeNode.isWidgetDefinition) { + this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,undefined,{isWidgetDefinition: true, configTrimWhiteSpace: this.parseTreeNode.configTrimWhiteSpace}); + } else { + this.setVariable(this.setName,this.getValue()); + } } // Construct the child widgets this.makeChildWidgets(); @@ -111,7 +114,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of */ SetWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); - if(changedAttributes.name || changedAttributes.filter || changedAttributes.select || changedAttributes.tiddler || (this.setTiddler && changedTiddlers[this.setTiddler]) || changedAttributes.field || changedAttributes.index || changedAttributes.value || changedAttributes.emptyValue || + if(changedAttributes.name || changedAttributes.filter || changedAttributes.select || changedAttributes.tiddler || (this.setTiddler && changedTiddlers[this.setTiddler]) || changedAttributes.field || changedAttributes.index || changedAttributes.value || changedAttributes.emptyValue || changedAttributes.conditional || (this.setFilter && this.getValue() != this.variables[this.setName].value)) { this.refreshSelf(); return true; diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index 444e0ce87..d8b201554 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -116,6 +116,7 @@ options: see below Options include params: array of {name:, value:} for each parameter defaultValue: default value if the variable is not defined +allowSelfAssigned: if true, includes the current widget in the context chain instead of just the parent Returns an object with the following fields: @@ -124,32 +125,54 @@ text: text of variable, with parameters properly substituted */ Widget.prototype.getVariableInfo = function(name,options) { options = options || {}; - var actualParams = options.params || [], - parentWidget = this.parentWidget; - // Check for the variable defined in the parent widget (or an ancestor in the prototype chain) - if(parentWidget && name in parentWidget.variables) { - var variable = parentWidget.variables[name], - originalValue = variable.value, - value = originalValue; - // Only substitute parameter and variable references if this variable was defined with the \define pragma - if(variable.isMacroDefinition) { - var params = this.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); - }); - value = this.substituteVariableReferences(value,options); - } - return { - text: value, - params: params, - srcVariable: variable, - isCacheable: originalValue === value + var self = this, + actualParams = options.params || [], + currWidget = options.allowSelfAssigned ? this : this.parentWidget, + processVariable = function(variable) { + var originalValue = variable.value, + value = originalValue, + params = []; + // Only substitute parameter and variable references if this variable was defined with the \define pragma + if(variable.isMacroDefinition) { + 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); + }); + value = self.substituteVariableReferences(value,options); + } + return { + text: value, + params: params, + srcVariable: variable, + isCacheable: originalValue === value + }; }; + // Check for the variable defined in the parent widget (or an ancestor in the prototype chain) + if(currWidget && name in currWidget.variables) { + return processVariable(currWidget.variables[name]); } // If the variable doesn't exist in the parent widget then look for a macro module + var text = this.evaluateMacroModule(name,actualParams); + if(text === undefined) { + // Check for a shadow variable tiddler + var tiddler = this.wiki.getTiddler("$:/global/" + name); + if(tiddler) { + return processVariable({ + value: tiddler.getFieldString("text"), + params: $tw.utils.parseParameterDefinition(tiddler.getFieldString("parameters"),{requireParenthesis: true}), + isMacroDefinition: tiddler.getFieldString("is-macro") === "yes", + isWidgetDefinition: tiddler.getFieldString("is-widget") === "yes", + isProcedureDefinition: tiddler.getFieldString("is-procedure") === "yes", + isFunctionDefinition: tiddler.getFieldString("is-function") === "yes" + }); + } + } + if(text === undefined) { + text = options.defaultValue; + } return { - text: this.evaluateMacroModule(name,actualParams,options.defaultValue), + text: text, srcVariable: {} }; }; @@ -479,12 +502,12 @@ Widget.prototype.makeChildWidget = function(parseTreeNode,options) { options = options || {}; // Check whether this node type is defined by a custom widget definition var variableDefinitionName = "$" + parseTreeNode.type, - variableInfo = this.variables[variableDefinitionName], + variableInfo = this.getVariableInfo(variableDefinitionName,{allowSelfAssigned: true}), isOverrideable = function() { // Widget is overrideable if it has a double dollar user defined name, or if it is an existing JS widget return parseTreeNode.type.charAt(0) === "$" || !!self.widgetClasses[parseTreeNode.type]; }; - if(!parseTreeNode.isNotRemappable && isOverrideable() && variableInfo && variableInfo.value && variableInfo.isWidgetDefinition) { + if(!parseTreeNode.isNotRemappable && isOverrideable() && variableInfo && variableInfo.srcVariable && variableInfo.srcVariable.value && variableInfo.srcVariable.isWidgetDefinition) { var newParseTreeNode = { type: "transclude", children: [ diff --git a/editions/test/tiddlers/tests/data/globals/CustomWidget.tid b/editions/test/tiddlers/tests/data/globals/CustomWidget.tid new file mode 100644 index 000000000..027602661 --- /dev/null +++ b/editions/test/tiddlers/tests/data/globals/CustomWidget.tid @@ -0,0 +1,22 @@ +title: Globals/CustomWidget +description: Global shadow variable defining a custom widget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +<$$mywidget foo="Mahogany"> +Sycamore! + ++ +title: $:/global/$$mywidget +is-widget: yes +parameters: (foo:"bar") + +Koala! <$text text=<>/>, <$slot $name="ts-body"/> ++ +title: ExpectedResult + +

Koala! Mahogany, Sycamore!

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/globals/Functions.tid b/editions/test/tiddlers/tests/data/globals/Functions.tid new file mode 100644 index 000000000..d8febc369 --- /dev/null +++ b/editions/test/tiddlers/tests/data/globals/Functions.tid @@ -0,0 +1,22 @@ +title: Globals/Functions +description: Global functions in shadow variables +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +<$text text=<>/> +| +<$text text=<>/> ++ +title: $:/global/this-is-one +is-function: yes +parameters: (foo:"2") + +[multiply[2.5]] ++ +title: ExpectedResult + +

5|17.5

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/globals/Procedures.tid b/editions/test/tiddlers/tests/data/globals/Procedures.tid new file mode 100644 index 000000000..b20ad396b --- /dev/null +++ b/editions/test/tiddlers/tests/data/globals/Procedures.tid @@ -0,0 +1,27 @@ +title: Globals/Procedures +description: Global procedures in shadow variables +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +<> +| +<> ++ +title: $:/global/this-is-one + +\whitespace trim + +\procedure example() +ONE +\end + +\parameters (foo:"nothing") +<>-<$text text=<>/> ++ +title: ExpectedResult + +

ONE-nothing

|ONE-blah

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/globals/ProceduresWithConditionalDefinitions.tid b/editions/test/tiddlers/tests/data/globals/ProceduresWithConditionalDefinitions.tid new file mode 100644 index 000000000..330b042d3 --- /dev/null +++ b/editions/test/tiddlers/tests/data/globals/ProceduresWithConditionalDefinitions.tid @@ -0,0 +1,26 @@ +title: Globals/ProceduresWithConditionalDefinitions +description: Global procedures with conditional definitions to allow overriding +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +<>|<>~ +<$let example="TWO"><>|<> ++ +title: $:/global/this-is-one + +\whitespace trim + +\+procedure example() +ONE +\end + +\parameters (foo:"nothing") +<>-<$text text=<>/> ++ +title: ExpectedResult + +

ONE-nothing|ONE-blah~TWO-nothing|TWO-blah

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-wikitext-parser.js b/editions/test/tiddlers/tests/test-wikitext-parser.js index 95d68a258..40e83237d 100644 --- a/editions/test/tiddlers/tests/test-wikitext-parser.js +++ b/editions/test/tiddlers/tests/test-wikitext-parser.js @@ -114,7 +114,7 @@ describe("WikiText parser tests", function() { it("should parse macro definitions", function() { expect(parse("\\define myMacro()\nnothing\n\\end\n")).toEqual( - [ { type : 'set', attributes : { name : { type : 'string', value : 'myMacro' }, value : { type : 'string', value : 'nothing' } }, children : [ ], params : [ ], isMacroDefinition : true } ] + [{"type":"set","attributes":{"name":{"name":"name","type":"string","value":"myMacro"},"value":{"name":"value","type":"string","value":"nothing"}},"children":[],"params":[],"isMacroDefinition":true,"orderedAttributes":[{"name":"name","type":"string","value":"myMacro"},{"name":"value","type":"string","value":"nothing"}]}] ); }); @@ -185,7 +185,7 @@ describe("WikiText parser tests", function() { it("should parse comment in pragma area. Comment will be invisible", function() { expect(parse("\n\\define aMacro()\nnothing\n\\end\n")).toEqual( - [ { type : 'set', attributes : { name : { type : 'string', value : 'aMacro' }, value : { type : 'string', value : 'nothing' } }, children : [ ], params : [ ], isMacroDefinition : true } ] + [{"type":"set","attributes":{"name":{"name":"name","type":"string","value":"aMacro"},"value":{"name":"value","type":"string","value":"nothing"}},"children":[],"params":[],"isMacroDefinition":true,"orderedAttributes":[{"name":"name","type":"string","value":"aMacro"},{"name":"value","type":"string","value":"nothing"}]}] ); });