XSLT Replace Multiple Strings

07 Sep
September 7, 2010

I scoured the internet for an XSLT template that replaces multiple strings (and multiple occurrences of those strings) and substitutes them with a particular value, I found something that almost does the job at this website, but the solution required an XML file to be referenced from within the XSLT, which meant that my CSharp WebService kicked up a fuss due to not having setup the appropriate security permissions when I was using the XmlTransform object, which needs to be setup by properly instantiating an XmlUrlResolver object.

Anyway to cut a story short, I decided to go down the recursive XSLT template calls route, just to keep everything neat and tidy and in one place, especially since I use Altova MapForce (Xml mapping tool, pretty cool actually) almost daily to generate some basic XSLTs, and its easy enough to add any template as an entity there and link it up to any mapping.

So I wrote this XSLT template which takes the following as input:

<xsl:template name="string-replace-all">
    <xsl:param name="text"/>
    <xsl:param name="replace" select="'the ,on ,in ,because ,'"/>
    <xsl:param name="by"/>
    <xsl:param name="times" select="1"/>
    ...
</xsl:template>

The purpose of each parameter is indicated below

  • $text: which is the original text
  • $replace: which are the strings you want to replace delimited by a comma. You also need to add a comma at the end of the parameter (so: ‘and,or,if,why,’)
  • $by: which is the replacement value, only one value.
  • $times: this is just an internal debug value to count the total number of iterations the XSLT template goes through.

Now the code uses recursion to go through the $text value and replace as many occurrences of comma delimited strings in the $replace value by the one string in the $by value. It is important to remember that some programs (or functions) that do XML transformations have a limit on the number of iterations the code goes through, so for example I think Altova has a limit of 256 iterations, after which you will get a ‘Stack Overflow’ error. Its cool when you are still new to recursion or coding in XSLT, but gets pretty annoying if you know what your doing and just need to go through large iterations.

The XSLT template gets recursively called from different function flows (using the choose, when and otherwise XSLT program flow functions).

The first choose statement checks to see if the string to be replaced is contained in the $text string, if so, then the template gets recursively called with the $text having the replaced string (string replacement happens through a series of substring XSLT functions).

<xsl:when test="contains($text, substring-before($replace,','))">
        <xsl:call-template name="string-replace-all">
        <xsl:with-param name="text"
           select="concat(substring-before($text,substring-before($replace,',')),$by,substring-after($text,substring-before($replace,',')))"/>
        <xsl:with-param name="replace" select="$replace"/>
        <xsl:with-param name="by" select="$by"/>
        <xsl:with-param name="times" select="$times+1"/>
    </xsl:call-template>
</xsl:when>

If the first $replace string is not contained in the $text string, then the second $replace string is tested.

<xsl:when test="contains($text, substring-before(substring-after($replace,','),','))">
    <xsl:call-template name="string-replace-all">
        <xsl:with-param name="text"
           select="concat(substring-before($text,substring-before(substring-after($replace,','),',')),$by,substring-after($text,substring-before(substring-after($replace,','),',')))"/>
        <xsl:with-param name="replace" select="substring-after($replace,',')"/>
        <xsl:with-param name="by" select="$by"/>
        <xsl:with-param name="times" select="$times+1"/>
    </xsl:call-template>
</xsl:when>

And again, if it is not contained, the code is recursively called with the next $replace value.

<xsl:call-template name="string-replace-all">
        <xsl:with-param name="text" select="$text"/>
        <xsl:with-param name="replace" select="substring-after($replace,',')"/>
        <xsl:with-param name="by" select="$by"/>
        <xsl:with-param name="times" select="$times+1"/>
</xsl:call-template>

Putting everything together…

<!--Author: Ibrahim Naji-->
<!-- Replace substring function-->
<xsl:template name="string-replace-all">
    <xsl:param name="text"/>
    <xsl:param name="replace" select="'the ,on ,in ,because ,'"/>
    <xsl:param name="by"/>
    <xsl:param name="times" select="1"/>
    <xsl:choose>
        <!--Checks if $text contains the first replace string in the $replace value-->
        <xsl:when test="contains($text, substring-before($replace,','))">
            <xsl:call-template name="string-replace-all">
                <xsl:with-param name="text" select="concat(substring-before($text,substring-before($replace,',')),$by,substring-after($text,substring-before($replace,',')))"/>
                <xsl:with-param name="replace" select="$replace"/>
                <xsl:with-param name="by" select="$by"/>
                <xsl:with-param name="times" select="$times+1"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:choose>
                <!--Checks to see if there is any strings left to replace-->
                <xsl:when test="substring-after($replace,',') != ''">
                    <xsl:choose>
                        <!--Here you could call the template directly and the iteration will do the check to see if the next replace string is contained in the $text value-->
                        <!--but to save some iteration loops I did the work here-->
                        <xsl:when test="contains($text, substring-before(substring-after($replace,','),','))">
                            <xsl:call-template name="string-replace-all">
                                <xsl:with-param name="text" select="concat(substring-before($text,substring-before(substring-after($replace,','),',')),$by,substring-after($text,substring-before(substring-after($replace,','),',')))"/>
                                <xsl:with-param name="replace" select="substring-after($replace,',')"/>
                                <xsl:with-param name="by" select="$by"/>
                                <xsl:with-param name="times" select="$times+1"/>
                            </xsl:call-template>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:call-template name="string-replace-all">
                                <xsl:with-param name="text" select="$text"/>
                                <xsl:with-param name="replace" select="substring-after($replace,',')"/>
                                <xsl:with-param name="by" select="$by"/>
                                <xsl:with-param name="times" select="$times+1"/>
                            </xsl:call-template>
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$text"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

There might be better ways to do this, and you could very easily extends this script to take multiple replacement strings in the $by parameter, but for the purpose of what I was trying to do this was enough.

* * * ½   5 votes

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>