2

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?

2
  • use the for-each function Commented Jan 13, 2016 at 7:39
  • @sanjay, There is a much more generic solution, not using variable number of nested replace() function calls. This solution allows an external mapping for the replacement to be specified. Enjoy! Commented Jan 17, 2016 at 3:26

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

Comments

1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.