28

How can I convert the following XML to an escaped text using XSLT?

Source:

<?xml version="1.0" encoding="utf-8"?>
<abc>
  <def ghi="jkl">
    mnop
  </def>
</abc>

Output:

<TestElement>&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;abc&gt;&lt;def ghi="jkl"&gt;
    mnop
  &lt;/def&gt;&lt;/abc&gt;</TestElement>

Currently, I'm trying the following XSLT and it doesn't seem to work properly:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" encoding="utf-8" />
  <xsl:template match="/">
    <xsl:variable name="testVar">
      <xsl:copy>
        <xsl:apply-templates select="@* | node()"/>
      </xsl:copy>
    </xsl:variable>

    <TestElement>
      <xsl:value-of select="$testVar"/>
    </TestElement>
  </xsl:template>
</xsl:stylesheet>

Output of XSLT statement by the .NET XslCompiledTransform comes out as the following:

<?xml version="1.0" encoding="utf-8"?><TestElement>

    mnop

</TestElement>
Frank Liao
  • 641
  • 1
  • 6
  • 9

8 Answers8

39

Your code works the way it does because xsl:value-of retrieves the string-value of the node set.

To do what you want, I'm afraid that you'll have to code it explicitly:

    <xsl:template match="/">
        <TestElement>
            <xsl:apply-templates mode="escape"/>
        </TestElement>
    </xsl:template>

    <xsl:template match="*" mode="escape">
        <!-- Begin opening tag -->
        <xsl:text>&lt;</xsl:text>
        <xsl:value-of select="name()"/>

        <!-- Namespaces -->
        <xsl:for-each select="namespace::*">
            <xsl:text> xmlns</xsl:text>
            <xsl:if test="name() != ''">
                <xsl:text>:</xsl:text>
                <xsl:value-of select="name()"/>
            </xsl:if>
            <xsl:text>='</xsl:text>
            <xsl:call-template name="escape-xml">
                <xsl:with-param name="text" select="."/>
            </xsl:call-template>
            <xsl:text>'</xsl:text>
        </xsl:for-each>

        <!-- Attributes -->
        <xsl:for-each select="@*">
            <xsl:text> </xsl:text>
            <xsl:value-of select="name()"/>
            <xsl:text>='</xsl:text>
            <xsl:call-template name="escape-xml">
                <xsl:with-param name="text" select="."/>
            </xsl:call-template>
            <xsl:text>'</xsl:text>
        </xsl:for-each>

        <!-- End opening tag -->
        <xsl:text>&gt;</xsl:text>

        <!-- Content (child elements, text nodes, and PIs) -->
        <xsl:apply-templates select="node()" mode="escape" />

        <!-- Closing tag -->
        <xsl:text>&lt;/</xsl:text>
        <xsl:value-of select="name()"/>
        <xsl:text>&gt;</xsl:text>
    </xsl:template>

    <xsl:template match="text()" mode="escape">
        <xsl:call-template name="escape-xml">
            <xsl:with-param name="text" select="."/>
        </xsl:call-template>
    </xsl:template>

    <xsl:template match="processing-instruction()" mode="escape">
        <xsl:text>&lt;?</xsl:text>
        <xsl:value-of select="name()"/>
        <xsl:text> </xsl:text>
        <xsl:call-template name="escape-xml">
            <xsl:with-param name="text" select="."/>
        </xsl:call-template>
        <xsl:text>?&gt;</xsl:text>
    </xsl:template>

    <xsl:template name="escape-xml">
        <xsl:param name="text"/>
        <xsl:if test="$text != ''">
            <xsl:variable name="head" select="substring($text, 1, 1)"/>
            <xsl:variable name="tail" select="substring($text, 2)"/>
            <xsl:choose>
                <xsl:when test="$head = '&amp;'">&amp;amp;</xsl:when>
                <xsl:when test="$head = '&lt;'">&amp;lt;</xsl:when>
                <xsl:when test="$head = '&gt;'">&amp;gt;</xsl:when>
                <xsl:when test="$head = '&quot;'">&amp;quot;</xsl:when>
                <xsl:when test="$head = &quot;&apos;&quot;">&amp;apos;</xsl:when>
                <xsl:otherwise><xsl:value-of select="$head"/></xsl:otherwise>
            </xsl:choose>
            <xsl:call-template name="escape-xml">
                <xsl:with-param name="text" select="$tail"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>

Note that this solution ignores comment nodes, and inserts unneccessary namespace nodes (as namespace:: axis will include all nodes inherited from parent). Regarding namespaces, however, the resulting quoted XML will be semantically equivalent to the example that you provided in your reply (since those repeated redeclarations don't really change anything).

Also, this won't escape the <?xml ... ?> declaration, simply because it is not present in XPath 1.0 data model (it's not a processing instruction). If you actually need it in the output, you'll have to insert it manually (and make sure that encoding it specifies is consistent with serialization encoding of your XSLT processor).

Pavel Minaev
  • 99,783
  • 25
  • 219
  • 289
22

instead of escaping you can add the text inside a CDATA section. Text inside a CDATA section will be ignored by the parser, similar to if it was escaped.

your example would look like this

<TestElement>
<![CDATA[
<abc>
  <def ghi="jkl">
    mnop
  </def>
</abc>
]]>
</TestElement>

using following XSLT snippet:

<xsl:text disable-output-escaping="yes">&lt;![CDATA[</xsl:text>
 <xsl:copy-of select="/"/>
 <xsl:text disable-output-escaping="yes">]]</xsl:text>
 <xsl:text disable-output-escaping="yes">&gt;</xsl:text>
toca
  • 221
  • 2
  • 3
  • 1
    +1 for helping me with my issue (separate from this question). – Jeff Martin Jan 28 '11 at 17:23
  • 1
    This was very helpful to me also. It seems like a better trick than Saxon serializer. – JM. Oct 02 '14 at 20:16
  • 1
    It did not work for me though with HTML output. The document looks OK but somehow the browser refuses to display the text. I get <![CDATA[ ]]> But the browser only shows "]]>". I am assuming this might be a HTML issue. Can someone confirm that? – Robert Klemme Mar 15 '17 at 10:47
  • @RobertKlemme I had the same issue. I used this code instead, then used JavaScript to format the results. `<!--` `` `-->` ``` – clarmond Jun 10 '20 at 14:20
4

Anyone who is concerned about licensing ambiguity when reusing code snippets from stack overflow may be interested in the following 3-clause BSD-licensed code, which appears to do what is requested by the original poster:

http://lenzconsulting.com/xml-to-string/

jbeard4
  • 12,664
  • 4
  • 57
  • 67
3

I attempted to implement the answer provided by Pavel Minaev and want to point out that this is very dangerous for large strings as each character in the input string is recursed over individually, causing the recursion depth to quickly run out. I attempted to run it over a few lines of text and it caused a stack overflow (lol).

Instead, I use a template that does not need to examine each individual char, rather it will out put the text until it finds a string that needs to be replaced. This can then be used to escape characters:

<xsl:template name="Search-And-Replace">
    <xsl:param name="Input-String"/>
    <xsl:param name="Search-String"/>
    <xsl:param name="Replace-String"/>  
    <xsl:choose>
        <xsl:when test="$Search-String and contains($Input-String, $Search-String)">
            <xsl:value-of select="substring-before($Input-String, $Search-String)"/>
            <xsl:value-of select="$Replace-String"/>        
            <xsl:call-template name="Search-And-Replace">
                <xsl:with-param name="Input-String" select="substring-after($Input-String, $Search-String)"/>
                <xsl:with-param name="Search-String" select="$Search-String"/>
                <xsl:with-param name="Replace-String" select="$Replace-String"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$Input-String"/>
        </xsl:otherwise>
    </xsl:choose>   
</xsl:template> 

Then its just a matter of calling that template for the char that you want to escape..

<xsl:call-template name="Search-And-Replace">
            <xsl:with-param name="Input-String" select="Hi I am a string &amp; I am awesome"/>
            <xsl:with-param name="Search-String" select="'&amp;'"/>
            <xsl:with-param name="Replace-String" select="'&amp;amp;'"/>
    </xsl:call-template>

In order to escape multiple characters in the one string, I used a wrapper template that uses variables...

<xsl:template name="EscapeText">
    <xsl:param name="text" />

    <xsl:variable name="a">
    <xsl:call-template name="Search-And-Replace">
            <xsl:with-param name="Input-String" select="$text"/>
            <xsl:with-param name="Search-String" select="'&amp;'"/>
            <xsl:with-param name="Replace-String" select="'&amp;amp;'"/>
        </xsl:call-template>            
    </xsl:variable>

    <xsl:variable name="b">     
        <xsl:call-template name="Search-And-Replace">
            <xsl:with-param name="Input-String" select="$a"/>
            <xsl:with-param name="Search-String" select="'&quot;'"/>
            <xsl:with-param name="Replace-String" select="'&amp;quot;'"/>
        </xsl:call-template>
    </xsl:variable>

    <xsl:variable name="c">     
        <xsl:call-template name="Search-And-Replace">
            <xsl:with-param name="Input-String" select="$b"/>
            <xsl:with-param name="Search-String">&apos;</xsl:with-param>
            <xsl:with-param name="Replace-String" select="'&amp;apos;'"/>
        </xsl:call-template>
    </xsl:variable>         

    <xsl:variable name="d">     
        <xsl:call-template name="Search-And-Replace">
            <xsl:with-param name="Input-String" select="$c"/>
            <xsl:with-param name="Search-String" select="'&gt;'"/>
            <xsl:with-param name="Replace-String" select="'&amp;gt;'"/>
        </xsl:call-template>
    </xsl:variable>

    <xsl:variable name="e">
        <xsl:call-template name="Search-And-Replace">
            <xsl:with-param name="Input-String" select="$d"/>
            <xsl:with-param name="Search-String" select="'&lt;'"/>
            <xsl:with-param name="Replace-String" select="'&amp;lt;'"/>
        </xsl:call-template>
    </xsl:variable>     
    <!--this is the final output-->
    <xsl:value-of select="$e"/>     
</xsl:template> 

This proved to be much safer for large strings as it no longer has to recurse for each individual character in the input string.

StevenP
  • 177
  • 1
  • 3
  • 15
  • My long strings broke the individual character recursion solution also. This solution worked well for me but I had to add a `disable-output-escaping` to the final output: ``. Thanks! – twamley May 02 '14 at 18:24
1

You can prevent the extra namespace nodes by adding a test in the namespace output:


<xsl:variable name="curnode" select="."/>
    <xsl:for-each select="namespace::*"> 
       <xsl:variable name="nsuri" select="."/>
       <xsl:if test="$curnode/descendant-or-self::*[namespace-uri()=$nsuri]">
       ...
willdarby
  • 11
  • 1
0

Do you need to use XSLT? Because, for reasons explained by Pavel Minaev, it would be much simpler to use another tool. An example with xmlstartlet:

% xmlstarlet escape
<?xml version="1.0" encoding="utf-8"?>
<abc>
  <def ghi="jkl">
    mnop
  </def>
</abc>
[Control-D]
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;abc&gt;
  &lt;def ghi="jkl"&gt;
    mnop
  &lt;/def&gt;
&lt;/abc&gt;
bortzmeyer
  • 34,164
  • 12
  • 67
  • 91
  • Unfortunately, this is outside of what I am asking. Lately, I've seen web services that take a string as a parameter. What's worse is that the string parameter is taking XML as input. Per a required feature, I need to transform an XML document to a SOAP envelope. The problem is that I need to convert the XML document to an escaped text to pass it in as a string parameter of the SOAP envelope (given that everything else is static). Therefore, the issue I asked comes up if you are not using another library of proxy class in the middle. – Frank Liao Jul 22 '09 at 13:34
0

If you have access to it, I would recommend the Saxon extention serialize. It does exactly what you want it to do. If you don't want to do that, you'd have to manually insert the entity references as you build the document. It'd be brittle, but it would work for most documents:

<xsl:template match="/">
    <TestElement>
        <xsl:apply-templates/>
    </TestElement>
</xsl:template>
<xsl:template match="*">
    <xsl:text>&lt;</xsl:text>
    <xsl:value-of select="name()"/>
    <xsl:apply-templates select="@*"/>
    <xsl:text>&gt;</xsl:text>
    <xsl:apply-templates select="node()"/>
    <xsl:text>&lt;/</xsl:text>
    <xsl:value-of select="name()"/>
    <xsl:text>&gt;</xsl:text>
</xsl:template>
<xsl:template match="@*">
    <xsl:text>&#32;</xsl:text>
    <xsl:value-of select="name()"/>
    <xsl:text>="</xsl:text>
    <xsl:value-of select="."/>
    <xsl:text>"</xsl:text>
</xsl:template>
<xsl:template match="text()">
    <xsl:value-of select="."/>
</xsl:template>

Most notably, this will probably break if your attributes have the double-quote character. It's really better to use saxon, or to use a user-written extention that uses a proper serializer if you can't.

Nigel Thorne
  • 21,158
  • 3
  • 35
  • 51
Chris Scott
  • 1,721
  • 14
  • 27
-1

Why can't you just run

<xsl:template match="/">
  <TestElement>
  <xsl:copy-of select="." />
  </TestElement>
</xsl:template>
Tim Ebenezer
  • 2,714
  • 17
  • 23