2
votes

I need to output the following xml to the browser exactly as it appears below:

<?xml version="1.0" encoding="UTF-8"?>
<content>
  <text>
    <h1>Services</h1>
    <h2>Engineering</h2>
    This is the first bit of text:
      <listofitems>
        <listitem>Database Analysis Tools</listitem>
        <listitem>Website Development</listitem>
        <listitem>Intranet Development</listitem>
        <listitem>Database Development</listitem>
      </listofitems> 
    This is the second piece of text:
      <listofitems>
        <listitem>Hardware</listitem>
        <listitem>Software</listitem>
      </listofitems>
    This is the final piece of text.
  </text>
</content>

The problem is with the text strings inside the <text> element - the bits not enclosed in their own tags, i.e. 'This is the first bit of text:', 'This is the second piece of text:' and 'This is the final piece of text.'

I can output all three bits of text in a single block if I use <xsl:value-of select="."/>, but obviously that's not how they appear in the original XML. If I use <xsl:value-of select="text()"/> I get nothing back (I believe text() only references the first child node, which in this case is another element).

I can output the last bit of text with <xsl:value-of select="node()[last()]/self::text()">; I can access each text() node by referencing it directly, e.g. <xsl:value-of select="text()[3]"/>, but as this is meant to be used with random XML which could be of any format I won't be able to reference nodes directly in this way. I tried a for-each on text()[*] but it didn't work. Any ideas how I can do this?

3

3 Answers

1
votes

Write a separate template to match text nodes if they are children of the text element:

<xsl:template match="text()[parent::text]">

That way, any other child elements in between do not interfere. Then, write a second template to keep your XSLT processor from outputting text inside of listitem elements, too:

<xsl:template match="*[parent::text]"/>

If you omitted the template match above, the default behaviour of XSLT would lead to any text nodes that are children of listitem being output as well.

Note that XSLT 1.0 and 2.0 differ in their treatment of text nodes. One of your approaches:

<xsl:value-of select="text()"/>

Does actually output all text nodes in XSLT 2.0. Below is a stylesheet that works for both 1.0 and 2.0.

Stylesheet

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

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

   <xsl:output method="text"/>
   <xsl:strip-space elements="*"/>

   <xsl:template match="//text">
      <xsl:apply-templates/>
   </xsl:template>

   <xsl:template match="text()[parent::text]">
      <xsl:value-of select="."/>
   </xsl:template>

   <xsl:template match="*[parent::text]"/>

</xsl:stylesheet>

Output

This is the first bit of text: This is the second piece of text: This is the final piece of text.

Alternatively, if you do not need to process the child elements of text, add a select attribute to apply-templates:

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

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

   <xsl:template match="//text">
      <xsl:apply-templates select="text()"/>
   </xsl:template>

   <xsl:template match="text()[parent::text]">
      <xsl:copy/>
   </xsl:template>

</xsl:stylesheet>
0
votes

When this stylesheet:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">
    <xsl:strip-space elements="*"/>

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

    <xsl:template match="content/text/text()">
        <span><xsl:value-of select="."/></span>
    </xsl:template>

</xsl:stylesheet>

is applied to your input. It outputs:

<?xml version="1.0" encoding="utf-8"?>
<content>
    <text>
        <h1>Services</h1>
        <h2>Engineering</h2>
        <span>This is the first bit of text:</span>
        <listofitems>
            <listitem>Database Analysis Tools</listitem>
            <listitem>Website Development</listitem>
            <listitem>Intranet Development</listitem>
            <listitem>Database Development</listitem>
        </listofitems>
        <span>This is the second piece of text:</span>
        <listofitems>
            <listitem>Hardware</listitem>
            <listitem>Software</listitem>
        </listofitems>
        <span>This is the final piece of text.</span>
    </text>
</content>

will this solve your problem?

0
votes

After much fiddling around, I think this is close enough to be acceptable:

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

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="*">
  <xsl:param name="indent"/>
    <xsl:if test="parent::* or preceding-sibling::*">
        <br/>
    </xsl:if>
      <xsl:value-of select="$indent" disable-output-escaping="yes"/>
      <!-- open angle brackets -->
      &lt;<xsl:value-of select="name()"/>
      <!-- output any attributes and values -->
      <xsl:for-each select="@*">
        <xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text>
        <xsl:value-of select="name(.)"/>="<b><xsl:value-of select="."/></b>"
      </xsl:for-each>
      <!-- close angle brackets -->
      &gt;
      <!-- output text content -->
      <xsl:apply-templates>
        <xsl:with-param name="indent"><xsl:value-of select="$indent"/>&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;</xsl:with-param>
      </xsl:apply-templates>
      <!-- close tag -->
      <xsl:if test="child::*">
        <br/><xsl:value-of select="$indent" disable-output-escaping="yes"/>
      </xsl:if>
      &lt;/<xsl:value-of select="name()"/>&gt;  
</xsl:template>

<xsl:template match="text()">
    <xsl:value-of select="."/>
</xsl:template>

</xsl:stylesheet>

Which gives this:

<content> 
     <text> 
         <h1>Services</h1> 
         <h2>Engineering</h2> This is the first bit of text: 
         <listofitems> 
             <listitem>Database Analysis Tools</listitem> 
             <listitem>Website Development</listitem> 
             <listitem>Intranet Development</listitem> 
             <listitem>Database Development</listitem> 
        </listofitems> This is the second piece of text: 
         <listofitems> 
             <listitem>Hardware</listitem> 
             <listitem>Software</listitem> 
        </listofitems> This is the final piece of text. 
    </text> 
</content>

Those annoying bits of text are, at least, in roughly the correct area and the template should work with any old XML thrown at it.

Many thanks for the help.