sanjay
sanjay

Reputation: 1020

XSLT - replace a String multiple times

I have a xml like this, (<p>/text() is varying)

<doc>
    <p> solid 1; thick 2; solid 2;</p>
    <p> double 2; thick 2; dotted 1;</p>
    <p> dotted 1; double 2; dotted 2;</p>
    <p> solid 2; thick 2; dotted 2;</p>
</doc>

My requirment is analize <p> node text and replace following strings,

solid 1; to solid 2;
solid 2; to solid 4;
dotted 1; to dotted 2;
dotted 2; to dotted 4;

SO, the expected output should look like this,

<doc>
    <p> solid 2; thick 2; solid 4;</p>
    <p> double 2; thick 2; dotted 2;</p>
    <p> dotted 2; double 2; dotted 4;</p>
    <p> solid 4; thick 2; dotted 4;</p>
</doc>

I wrote following xslt to do this task,

<xsl:template name='replace-text'>
        <xsl:param name='text'/>
        <xsl:param name='replace'/>
        <xsl:param name='by'/>
        <xsl:choose>
            <xsl:when test='contains($text, $replace)'>
                <xsl:value-of select='substring-before($text, $replace)'/>
                <xsl:value-of select='$by' disable-output-escaping='yes'/>
                <xsl:call-template name='replace-text'>
                    <xsl:with-param name='text' select='substring-after($text, $replace)'/>
                    <xsl:with-param name='replace' select='$replace'/>
                    <xsl:with-param name='by' select='$by'/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select='$text'/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="p/text()">
        <xsl:call-template name="replace-text">
            <xsl:with-param name="text" select="."/>
            <xsl:with-param name="replace" select="'solid 1'"/>
            <xsl:with-param name="by" select="'solid 2'"/>
        </xsl:call-template>
    </xsl:template>

But here I only can pass one parameter at a time. I,m struggling to implement a method in factional programming to this scenario, Can anyone suggest me a method to do this task?

Upvotes: 2

Views: 1753

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243449

A more flexible solution, which doesn't use N number of nested replace() calls:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="p/text()">
    <xsl:for-each select="tokenize(., ';')">
      <xsl:analyze-string select="." regex="((solid)|(dotted)) (\d)">
        <xsl:matching-substring>
          <xsl:value-of select=
               "concat(regex-group(1), ' ', 2*xs:integer(regex-group(4)), ';')"/>
        </xsl:matching-substring>
        <xsl:non-matching-substring>
          <xsl:sequence select="."></xsl:sequence>
        </xsl:non-matching-substring>
      </xsl:analyze-string>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

An even more generic solution, in which the replacements of the tokens is specified in a parameter (and may be specified in a separate XML document):

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pMapping">
   <mapArgOf name="solid" old="1" new="2"/>
   <mapArgOf name="solid" old="2" new="4"/>
   <mapArgOf name="dotted" old="1" new="2"/>
   <mapArgOf name="dotted" old="2" new="4"/>
 </xsl:param>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="p/text()">
    <xsl:for-each select="tokenize(., ';')[.]">
      <xsl:variable name="vTokens" select="tokenize(., '\s+')[.]"/>
      <xsl:variable name="vMatch" 
          select="$pMapping/*[@name eq $vTokens[1] and @old eq $vTokens[2]]"/>

      <xsl:value-of select=
       "concat(replace(., $vTokens[2], ($vMatch/@new, $vTokens[2])[1]), ';')"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Both of these transformations, when applied on the provided XML document:

<doc>
    <p> solid 1; thick 2; solid 2;</p>
    <p> double 2; thick 2; dotted 1;</p>
    <p> dotted 1; double 2; dotted 2;</p>
    <p> solid 2; thick 2; dotted 2;</p>
</doc>

produce the wanted, correct result:

<doc>
    <p> solid 2; thick 2; solid 4;</p>
    <p> double 2; thick 2; dotted 2;</p>
    <p> dotted 2; double 2; dotted 4;</p>
    <p> solid 4; thick 2; dotted 4;</p>
</doc>

Do note:

  1. We can specify as many mappings (not only for "solid" and "dotted") as we want.

  2. Here we no longer assume that the new value is twice the old value -- we even don't assume that the value is a number

For example, if we want to add replacements for "thick", such that 1 is replaced by 5, 2 is replaced by 8 and 3 is replaced by 10, we simply change the mapping like this:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pMapping">
   <mapArgOf name="solid" old="1" new="2"/>
   <mapArgOf name="solid" old="2" new="4"/>
   <mapArgOf name="dotted" old="1" new="2"/>
   <mapArgOf name="dotted" old="2" new="4"/>
   <mapArgOf name="thick" old="1" new="5"/>
   <mapArgOf name="thick" old="2" new="8"/>
   <mapArgOf name="thick" old="3" new="10"/>
 </xsl:param>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="p/text()">
    <xsl:for-each select="tokenize(., ';')[.]">
      <xsl:variable name="vTokens" select="tokenize(., '\s+')[.]"/>
      <xsl:variable name="vMatch" select=
         "$pMapping/*[@name eq $vTokens[1] and @old eq $vTokens[2]]"/>

      <xsl:value-of select=
        "concat(replace(., $vTokens[2], ($vMatch/@new, $vTokens[2])[1]), ';')"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

And now we again get the new, wanted result:

<doc>
    <p> solid 2; thick 5; solid 4;</p>
    <p> double 2; thick 8; dotted 2;</p>
    <p> dotted 2; double 2; dotted 4;</p>
    <p> solid 4; thick 10; dotted 4;</p>
</doc>

Finally, for the most generic multi-replace solution see this answer:

XSL Multiple search and replace function

Upvotes: 1

Martin Vitek
Martin Vitek

Reputation: 159

Just use the replace() function and a for-each loop.

     <doc>
        <xsl:for-each select="p">
            <p>
                <xsl:value-of select="  fn:replace(
                                            fn:replace(
                                                fn:replace(
                                                    fn:replace(.,'solid 2','solid 4'),
                                                'solid 1','solid 2'),
                                            'dotted 2','dotted 4'),
                                        'dotted 1','dotted 2')"/>
            </p>
        </xsl:for-each>
    </doc>

In this case you have to watch out to first replace "solid 2" and after that "solid 1". If you replace "solid 1" by "solid 2" first it will be replaced again by "solid 4" because of the order. The innermost function will be first applied on the string.

Upvotes: 1

Related Questions