diff --git a/core/modules/filters/substitute.js b/core/modules/filters/substitute.js new file mode 100644 index 000000000..655ef7321 --- /dev/null +++ b/core/modules/filters/substitute.js @@ -0,0 +1,36 @@ +/*\ +title: $:/core/modules/filters/substitute.js +type: application/javascript +module-type: filteroperator + +Filter operator for substituting variables and embedded filter expressions with their corresponding values + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Export our filter function +*/ +exports.substitute = function(source,operator,options) { + var results = [], + operands = []; + $tw.utils.each(operator.operands,function(operand,index){ + operands.push({ + name: (index + 1).toString(), + value: operand + }); + }); + source(function(tiddler,title) { + if(title) { + results.push(options.wiki.getSubstitutedText(title,options.widget,{substitutions:operands})); + } + }); + return results; +}; + +})(); + \ No newline at end of file diff --git a/core/modules/parsers/parseutils.js b/core/modules/parsers/parseutils.js index 6a0902c6f..1f86dd909 100644 --- a/core/modules/parsers/parseutils.js +++ b/core/modules/parsers/parseutils.js @@ -305,10 +305,11 @@ exports.parseAttribute = function(source,pos) { start: pos }; // Define our regexps - var reAttributeName = /([^\/\s>"'=]+)/g, - reUnquotedAttribute = /([^\/\s<>"'=]+)/g, + var reAttributeName = /([^\/\s>"'`=]+)/g, + reUnquotedAttribute = /([^\/\s<>"'`=]+)/g, reFilteredValue = /\{\{\{([\S\s]+?)\}\}\}/g, - reIndirectValue = /\{\{([^\}]+)\}\}/g; + reIndirectValue = /\{\{([^\}]+)\}\}/g, + reSubstitutedValue = /(?:```([\s\S]*?)```|`([^`]|[\S\s]*?)`)/g; // Skip whitespace pos = $tw.utils.skipWhiteSpace(source,pos); // Get the attribute name @@ -361,8 +362,15 @@ exports.parseAttribute = function(source,pos) { node.type = "macro"; node.value = macroInvocation; } else { - node.type = "string"; - node.value = "true"; + var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue); + if(substitutedValue) { + pos = substitutedValue.end; + node.type = "substituted"; + node.rawValue = substitutedValue.match[1] || substitutedValue.match[2]; + } else { + node.type = "string"; + node.value = "true"; + } } } } diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index 8ffee0ab7..3780c05cf 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -380,6 +380,8 @@ Widget.prototype.computeAttribute = function(attribute) { } else if(attribute.type === "macro") { var variableInfo = this.getVariableInfo(attribute.value.name,{params: attribute.value.params}); value = variableInfo.text; + } else if(attribute.type === "substituted") { + value = this.wiki.getSubstitutedText(attribute.rawValue,this) || ""; } else { // String attribute value = attribute.value; } diff --git a/core/modules/wiki.js b/core/modules/wiki.js index ca31da8d2..93e818f21 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1063,6 +1063,34 @@ exports.getTextReferenceParserInfo = function(title,field,index,options) { return parserInfo; } +/* +Parse a block of text of a specified MIME type + text: text on which to perform substitutions + widget + options: see below +Options include: + substitutions: an optional array of substitutions +*/ +exports.getSubstitutedText = function(text,widget,options) { + options = options || {}; + text = text || ""; + var self = this, + substitutions = options.substitutions || [], + output; + // Evaluate embedded filters and substitute with first result + output = text.replace(/\$\{([\S\s]+?)\}\$/g, function(match,filter) { + return self.filterTiddlers(filter,widget)[0] || ""; + }); + // Process any substitutions provided in options + $tw.utils.each(substitutions,function(substitute) { + output = $tw.utils.replaceString(output,new RegExp("\\$" + $tw.utils.escapeRegExp(substitute.name) + "\\$","mg"),substitute.value); + }); + // Substitute any variable references with their values + return output.replace(/\$\((\w+)\)\$/g, function(match,varname) { + return widget.getVariable(varname,{defaultValue: ""}) + }); +}; + /* Make a widget tree for a parse tree parser: parser object diff --git a/editions/test/tiddlers/tests/data/filters/substitute.tid b/editions/test/tiddlers/tests/data/filters/substitute.tid new file mode 100644 index 000000000..873d8e0ba --- /dev/null +++ b/editions/test/tiddlers/tests/data/filters/substitute.tid @@ -0,0 +1,40 @@ +title: Filters/substitute +description: Test substitute operator +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: substitute filter data 1 +tags: Hello There [[Welcome to TiddlyWiki]] GettingStarted + +TiddlyWiki ++ +title: substitute filter data 2 + +The output of the filter `[[substitute filter data 1]tags[]]` is ${[[substitute filter data 1]tags[]]}$. ++ +title: substitute filter data 3 + +Welcome to $(projectname)$ $1$ $2$ $3$. Tiddlers starting with `substitute`: ${[prefix[substitute]format:titlelist[]join[ ]]}$. ++ +title: Output + +\whitespace trim +<$let projectname="TiddlyWiki"> +(<$text text={{{ [[]substitute[]] }}}/>) +(<$text text={{{ [[Hello There, welcome to $TiddlyWiki$]substitute[]] }}}/>) +(<$text text={{{ [[Welcome to $(projectname)$]substitute[]] }}}/>) +(<$text text={{{ [[Welcome to $(projectname)$ $1$]substitute[today]] }}}/>) +(<$text text={{{ [[This is not a valid embedded filter ${ hello )$]substitute[]] }}}/>) +(<$text text={{{ [{substitute filter data 2}substitute[]] }}}/>) +(<$text text={{{ [{substitute filter data 3}substitute[every],[day]] }}}/>) + ++ +title: ExpectedResult + +

() +(Hello There, welcome to $TiddlyWiki$) +(Welcome to TiddlyWiki) +(Welcome to TiddlyWiki today) +(This is not a valid embedded filter ${ hello )$) +(The output of the filter `[[substitute filter data 1]tags[]]` is Hello.) +(Welcome to TiddlyWiki every day $3$. Tiddlers starting with `substitute`: [[substitute filter data 1]] [[substitute filter data 2]] [[substitute filter data 3]].)

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/SubstitutedAttributes.tid b/editions/test/tiddlers/tests/data/widgets/SubstitutedAttributes.tid new file mode 100644 index 000000000..408d202c6 --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/SubstitutedAttributes.tid @@ -0,0 +1,19 @@ +title: Widgets/SubstitutedAttributes +description: Attributes specified as string that should have substitution performed. +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$let project="TiddlyWiki" disabled="true"> +
+
+ + ++ +title: ExpectedResult + +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-html-parser.js b/editions/test/tiddlers/tests/test-html-parser.js index cdc8dee47..d2266ca5e 100644 --- a/editions/test/tiddlers/tests/test-html-parser.js +++ b/editions/test/tiddlers/tests/test-html-parser.js @@ -161,6 +161,16 @@ describe("HTML tag new parser tests", function() { expect($tw.utils.parseAttribute(" attrib1>",0)).toEqual( { type : 'string', value : 'true', start : 0, name : 'attrib1', end : 8 } ); + expect($tw.utils.parseAttribute("p=`blah` ",1)).toEqual(null); + expect($tw.utils.parseAttribute("p=`blah` ",0)).toEqual( + { start: 0, name: 'p', type: 'substituted', rawValue: 'blah', end: 8 } + ); + expect($tw.utils.parseAttribute("p=```blah``` ",0)).toEqual( + { start: 0, name: 'p', type: 'substituted', rawValue: 'blah', end: 12 } + ); + expect($tw.utils.parseAttribute("p=`Hello \"There\"`",0)).toEqual( + { start: 0, name: 'p', type: 'substituted', rawValue: 'Hello "There"', end: 17 } + ); }); it("should parse HTML tags", function() { diff --git a/editions/tw5.com/tiddlers/filters/examples/substitute Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/substitute Operator (Examples).tid new file mode 100644 index 000000000..45a25e3e0 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/substitute Operator (Examples).tid @@ -0,0 +1,37 @@ +created: 20230614225302905 +modified: 20230614233448662 +tags: [[Operator Examples]] [[substitute Operator]] +title: substitute Operator (Examples) +type: text/vnd.tiddlywiki + +\define time() morning +\define field() modified +\procedure sentence() This tiddler was last $(field)$ on ${[{!!modified}format:date[DDth MMM YYYY]]}$ +\define name() Bugs Bunny +\define address() Rabbit Hole Hill + +!Substitute <<.op substitute[]>> operator parameters +<<.operator-example 1 "[[Hi, I'm $1$ and I live in $2$]substitute[Bugs Bunny],[Rabbit Hole Hill]]">> + +!Substitute variables +This example uses the following variables: + +* name: <$codeblock code=<>/> +* address: <$codeblock code=<
>/> + +<<.inline-operator-example "[[Hi, I'm $(name)$ and I live in $(address)$]substitute[]]">> + +!Substitute variables and operator parameters +This example uses the following variable: + +* time: <$codeblock code=<