wasting
wasting

Reputation: 13

Remove all child nodes except last using XSLT

I have an XML file as such:

<a>
  <b>
    <e />
    <f />
  </b>
  <c>
    <g>
      <j />
    </g>
    <g>
      <j />
    </g>
    <g>
      <j />
    </g>
  </c>
  <d>
    <h>
      <i />
    </h>
    <h>
      <i />
    </h>
    <h>
      <i />
    </h>
  </d>
</a>

What I'm trying to do is to apply an XSL transformation to only get the last nodes of c and d (including their child nodes) along with the rest of the file, resulting in:

<a>
  <b>
    <e />
    <f />
  </b>
  <c>
    <g>
      <j />
    </g>
  </c>
  <d>
    <h>
      <i />
    </h>
  </d>
</a>

I am not experienced with XSLT and any help is greatly appreciated.

Upvotes: 1

Views: 1499

Answers (2)

mr.tee
mr.tee

Reputation: 35

the predicate:

select="*[not(last())]" 

will not work !

the processor have CONFUSINGLY TWO kinds of node-filtering:

1, boolean-filter
2. positional-filter

because the filter-group will wrongly output position ZERO:

not(123) == 0

select=*[ 0 ]

then the positional-filter will get precedence, before the boolean-filter.

and because ZERO position does not exist then it will throw ERROR,

and this error will REJECT all the predicate !!!

lastly, the SOLUTION for this problem is:

select="*[ position() < last() ]"

you welcome :)

Upvotes: -2

Don Roby
Don Roby

Reputation: 41145

It's generally best to start with an identity transform and add exceptions, and then sometimes exceptions to the exceptions.

In this transform, the first template is the identity transform, the second skips children of <c> and <d>, and the third overrides that exclusion to include the last child of each <c> and <d> tag.

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

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

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

 <xsl:template match="c/*|d/*"/>

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

</xsl:stylesheet>

I had to modify your input xml to remove some spaces. The construct <x/ > is not really valid according to the specification (section 3.1 Start-tags, end-tags, and empty-element tags).

As noted in comments, this can be made shorter, using only one template in addition to the identity. I wasn't able to get [not(last()] to work, but this shorter template does:

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

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

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

 <xsl:template match="c/*[position() &lt; last()]|d/*[position() &lt; last()]"/>

</xsl:stylesheet>

and there may be improvements possible in the condition.

Which is better is of course a matter of taste. I find my original response slightly clearer.

Upvotes: 4

Related Questions