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]] }}}/>)
+$let>
++
+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">
+
+
+$let>
+
++
+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=<