Search code examples
xmlxslt-1.0exsltnode-set

Issue with using exslt str:replace with a nodeset


I've got an issue that I think is a namespace when using the exslt string replace function. I'd like to replace several strings in a target string by using the nodeset form of the exslt string replace function as per the documentation here. However it seems to only replace the first string of the nodeset and not the others.

Here is my file:

<!DOCTYPE xsl:stylesheet  [
    <!ENTITY nbsp   "&#160;">
    <!ENTITY yen    "&#165;">
    <!ENTITY circle  "&#9679;">
    <!ENTITY raquo "&#187;">
]>
<xsl:stylesheet 
    version="1.0" 
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    xmlns:str="http://exslt.org/strings"
    xmlns:exsl="http://exslt.org/common"
    xmlns:regexp="http://exslt.org/regular-expressions"
    extension-element-prefixes="msxsl str exsl regexp">
<xsl:output 
    method="html" 
    indent="yes" 
    encoding="utf-8" 
    doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" 
    doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" 
/>

<!-- Start of the main template -->
<xsl:template match="/Top">

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://www.facebook.com/2008/fbml">
    <body>
        <xsl:variable name="text">
            -[this]- This is a test string -[that]-
            -[this]- This is another test string -[that]-
        </xsl:variable>
        text: <xsl:value-of select="$text" disable-output-escaping="yes" />

        <xsl:variable name="searches" xmlns="">
            <replacements xmlns="">
                <searches>
                    <search>-[this]-</search>
                    <search>-[that]-</search>
                </searches>
                <replaces>
                    <replace>**[this]**</replace>
                    <replace>**[that]**</replace>
                </replaces>
            </replacements>
        </xsl:variable>

        <xsl:variable name="search_set" select="exsl:node-set($searches)/replacements/searches/search" />
        <xsl:variable name="replace_set" select="exsl:node-set($searches)/replacements/replaces/replace" />
        search_set: <xsl:copy-of select="$search_set" />
        replace_set: <xsl:copy-of select="$replace_set" />
        <xsl:if test="$search_set">
            replaced via function:
            <xsl:value-of select="str:replace($text, $search_set, $replace_set)" disable-output-escaping="yes" />
            replaced via template:
            <xsl:variable name="replaced_tpl">
                <xsl:call-template name="str:replace">
                    <xsl:with-param name="string" select="$text"/>
                    <xsl:with-param name="search" select="$search_set"/>
                    <xsl:with-param name="replace" select="$replace_set"/>
                </xsl:call-template>
            </xsl:variable>
            <xsl:value-of select="$replaced_tpl" disable-output-escaping="yes" />
        </xsl:if>

    </body>
</html>

</xsl:template><!-- / end main template -->

<xsl:template name="str:replace" xmlns="">
   <xsl:param name="string" select="''" />
   <xsl:param name="search" select="/.." />
   <xsl:param name="replace" select="/.." />

   <xsl:choose>
      <xsl:when test="not($string)" />
      <xsl:when test="not($search)">
         <xsl:value-of select="$string" />
      </xsl:when>
      <xsl:when test="function-available('exsl:node-set')">
<!--  this converts the search and replace arguments to node sets
              if they are one of the other XPath types  -->
         <xsl:variable name="search-nodes-rtf">
            <xsl:copy-of select="$search" />
         </xsl:variable>
         <xsl:variable name="replace-nodes-rtf">
            <xsl:copy-of select="$replace" />
         </xsl:variable>
         <xsl:variable name="replacements-rtf">
            <xsl:for-each select="exsl:node-set($search-nodes-rtf)/node()">
               <xsl:variable name="pos"
                             select="position()" />
               <replace search="{.}">
                  <xsl:copy-of select="exsl:node-set($replace-nodes-rtf)/node()[$pos]" />
               </replace>
            </xsl:for-each>
         </xsl:variable>
         <xsl:variable name="sorted-replacements-rtf">
            <xsl:for-each select="exsl:node-set($replacements-rtf)/replace">
               <xsl:sort select="string-length(@search)"
                         data-type="number"
                         order="descending" />
               <xsl:copy-of select="." />
            </xsl:for-each>
         </xsl:variable>
         <xsl:call-template name="str:_replace">
            <xsl:with-param name="string"
                            select="$string" />
            <xsl:with-param name="replacements"
                            select="exsl:node-set($sorted-replacements-rtf)/replace" />
         </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
         <xsl:message terminate="yes">
            ERROR: template implementation of str:replace relies on exsl:node-set().
         </xsl:message>
      </xsl:otherwise>
   </xsl:choose>
</xsl:template>

<xsl:template name="str:_replace">
   <xsl:param name="string"
              select="''" />
   <xsl:param name="replacements"
              select="/.." />
   <xsl:choose>
      <xsl:when test="not($string)" />
      <xsl:when test="not($replacements)">
         <xsl:value-of select="$string" />
      </xsl:when>
      <xsl:otherwise>
         <xsl:variable name="replacement"
                       select="$replacements[1]" />
         <xsl:variable name="search"
                       select="$replacement/@search" />
         <xsl:choose>
            <xsl:when test="not(string($search))">
               <xsl:value-of select="substring($string, 1, 1)" />
               <xsl:copy-of select="$replacement/node()" />
               <xsl:call-template name="str:_replace">
                  <xsl:with-param name="string"
                                  select="substring($string, 2)" />
                  <xsl:with-param name="replacements"
                                  select="$replacements" />
               </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($string, $search)">
               <xsl:call-template name="str:_replace">
                  <xsl:with-param name="string"
                                  select="substring-before($string, $search)" />
                  <xsl:with-param name="replacements"
                                  select="$replacements[position() > 1]" />
               </xsl:call-template>
               <xsl:copy-of select="$replacement/node()" />
               <xsl:call-template name="str:_replace">
                  <xsl:with-param name="string"
                                  select="substring-after($string, $search)" />
                  <xsl:with-param name="replacements"
                                  select="$replacements" />
               </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
               <xsl:call-template name="str:_replace">
                  <xsl:with-param name="string"
                                  select="$string" />
                  <xsl:with-param name="replacements"
                                  select="$replacements[position() > 1]" />
               </xsl:call-template>
            </xsl:otherwise>
         </xsl:choose>
      </xsl:otherwise>
   </xsl:choose>
</xsl:template>


</xsl:stylesheet>

And here is the output:

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://www.facebook.com/2008/fbml"><head><style type="text/css"></style></head><body>
        text: 
            -[this]- This is a test string -[that]-
            -[this]- This is another test string -[that]-

        search_set: <search xmlns="">-[this]-</search><search xmlns="">-[that]-</search>
        replace_set: <replace xmlns="">**[this]**</replace><replace xmlns="">**[that]**</replace>
            replaced via function:

            **[this]** This is a test string -[that]-
            **[this]** This is another test string -[that]-

            replaced via template:


            **[this]** This is a test string **[that]**
            **[this]** This is another test string **[that]**

</body></html>

As you can see in the output. In when using the function, only the string of the first node gets replaced. The second one does not. I copied the template code from exslt.org into the file as you can see and at first it didn't work until I added the xmlns="" to the str:replace template like so:

<xsl:template name="str:replace" xmlns="">

At that point the template form works which leads me to believe this is a name space issue. I believe that in the function when it sorts the nodes and creates its own replace nodes like so:

<replace search="{.}">
    <xsl:copy-of select="exsl:node-set($replace-nodes-rtf)/node()[$pos]" />
</replace>

That node ends up in a different namespace maybe and so the subsequent loop cannot address them. Adding the xmlns attribute to the str:replace puts any nodes created therein in the same null namespace as the nodes I'm passing in and then it works. However no matter what I try I cannot get the function version to work. I even removed all namespaces from the file and the xml nodeset I create and still it doesn't work. Frankly all this namespace stuff is a bit confusing to me. Maybe that's not even the problem at all.

Any help would be greatly appreciated, thanks!


Solution

  • I can use the function normally to replace strings by passing in just strings for the 2nd and 3rd parameters.

    It could be a problem with how the function is implemented in your processor. I suggest you eliminate all other possible causes of failure and try applying the following stylesheet:

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:str="http://exslt.org/strings"
    extension-element-prefixes="str">
    <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
    
    <xsl:template match="/input">
        <output>
            <xsl:choose>
                <xsl:when test="function-available('str:replace')">
                    <xsl:value-of select="str:replace(string, search, replace)" />  
                </xsl:when>
                <xsl:otherwise>
                    <xsl:text>function str:replace() is not supported</xsl:text>
                </xsl:otherwise>
            </xsl:choose>
        </output>
    </xsl:template>
    
    </xsl:stylesheet>
    

    to this input:

    <input>
        <string>Mary had a little lamb, its fleece was white as snow. 
    And everywhere that Mary went, the lamb was sure to go.</string>
        <search>Mary</search>
        <search>lamb</search>
        <search>fleece</search>
        <replace>John</replace>
        <replace>dog</replace>
        <replace>fur</replace>
    </input>
    

    and report back the results.


    Continued:

    Trying that I get <output>John had a little lamb, its fleece was white as snow. And everywhere that John went, the lamb was sure to go.</output>.

    Well then obviously the function is not implemented according to the specification. That's not such a bad thing, since most processors do not implement the str:replace() function at all. All you need to is fill-in the missing part by calling a named template before calling the function, for example:

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:exsl="http://exslt.org/common"
    xmlns:str="http://exslt.org/strings"
    extension-element-prefixes="exsl str">
    <xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
    
    <xsl:variable name="dictionary"> 
        <search>Mary</search>
        <search>lamb</search>
        <search>fleece</search>
        <replace>John</replace>
        <replace>dog</replace>
        <replace>fur</replace>
    </xsl:variable>
    
    <xsl:template match="/input">
        <output>
            <xsl:call-template name="multi-replace">
                <xsl:with-param name="string" select="string"/>
                <xsl:with-param name="search-strings" select="exsl:node-set($dictionary)/search"/>
                <xsl:with-param name="replace-strings" select="exsl:node-set($dictionary)/replace"/>
            </xsl:call-template>
        </output>
    </xsl:template>
    
    <xsl:template name="multi-replace">
        <xsl:param name="string"/>
        <xsl:param name="search-strings"/>
        <xsl:param name="replace-strings"/>
        <xsl:choose>
            <xsl:when test="$search-strings">
                <xsl:call-template name="multi-replace">
                    <xsl:with-param name="string" select="str:replace($string, $search-strings[1], $replace-strings[1])"/> 
                    <xsl:with-param name="search-strings" select="$search-strings[position() > 1]"/>
                    <xsl:with-param name="replace-strings" select="$replace-strings[position() > 1]"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$string"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    
    </xsl:stylesheet>
    

    Applied to the following test input:

    <input>
        <string>Mary had a little lamb, its fleece was white as snow. And everywhere that Mary went, the lamb was sure to go.</string>
    </input>
    

    you should be seeing the following result:

    <?xml version="1.0" encoding="utf-8"?>
    <output>John had a little dog, its fur was white as snow. And everywhere that John went, the dog was sure to go.</output>
    

    (I can't test this myself, since none of my processors support the function).

    This should get you very close to the specified behavior, except for one thing: the specification states that "The longest search strings are replaced first". If you want to implement this too, you must sort the dictionary strings first - and if you want the implementation to be simple, you should enter the dictionary string as pairs.