Well, it actually expects the template to be tokenised already (using <span>).
But a recursive template would also do the job, I guess. It would do recursion as long as the source text would be different from the transformed text.
It is possible to implement pure XSLT function, but it is not pretty and it relies on the existence of placeholder-value mapping in the XML source. It would one extra workflow step to “inject” the mapping.
<ROOT>
<replacements>
<item name="placeholder1">ALPHA</item>
<item name="placeholder2" value="BETA"/>
<item name="placeholder3">GAMMA</item>
</replacements>
<input>Some text with <placeholder1/> and <placeholder2/>. Maybe <placeholder1/> again or <placeholder3/></input>
</ROOT>
<result>Dear Arthur,
some text with other variables like 42 or Arthur again
greetings Marvin
</result>
Obviously the source can be delivered in a form of a pure text in a variable, you would have to convert it to a node-set and apply templates as shown. Parameters could be easily delivered as XSLT parameters from outside of the template or as the transformation main data context.
If the data are delivered as a nodeset (within the source or in any other way), you can use this universal transformation:
Source:
<source>
<string>Dear <name/>,
some text with other variables like <age/> or <name/> again
greetings <me/></string>
<parameters>
<name>Arthur</name>
<age>42</age>
<me>Marvin</me>
</parameters>
</source>
XSLT:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- we could have just referenced /source/parameters later all the time
but this would affect performance, it is better to store it in a variable -->
<xsl:variable name="params" select="/source/parameters"/>
<xsl:template match="source">
<!-- applies templates to both parameters and the text itself
as both text() and element() are a node() -->
<result>
<xsl:apply-templates select="string/node()"/>
</result>
</xsl:template>
<!-- here we resolve the parameter elements -->
<xsl:template match="*">
<xsl:value-of select="$params/*[name() = current()/name()]"/>
</xsl:template>
<!-- there is no template for text() as xslt has its default template
that copies any text to the output -->
</xsl:stylesheet>
If the template is delivered as a string parameter (in my example it is hardcoded in a variable) and the replacement values are loaded from the data context, it could look like this:
It looks cleanest and it can incorporate the ideas from other options.
<xsl:template match="name">Arthur</xsl:template>
Is basically a mapping definition which is good enough.
So it looks like we don’t really need a templating service, we just need to use this practice. Of course if you can’t have placeholders in “element” form, it can’t be used in a such easy way.