developer
developer

Reputation: 7400

XPath ancestor and descendant in XSL copy-of

I am new to XPath, and from what I have read in some tutorials about axes, I am still left wondering how to implement them. They aren't quite behaving as I had expected. I am particularly interested in using ancestor and descendant axes.

I have the following XML structure:

<file>
    <criteria>
        <root>ROOT</root>
        <criterion>AAA</criterion>
        <criterion>BBB</criterion>
        <criterion>CCC</criterion> 
    </criteria>
    <format>
        <sort>BBB</sort>
    </format>
</file>

And I have the following XSL:

<xsl:template match="/">
    <xsl:copy-of select="ancestor::criterion/>
</xsl:template>

which produces nothing!

I expected it to produce:

<file>
    <criteria>
    </criteria>
</file>

Can someone explain ancestor and descendant axes to me in a more helpful way than the tutorials I have previously read?

Thanks!

Upvotes: 5

Views: 18742

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243479

And I have the following XSL:

<xsl:template match="/"> 
    <xsl:copy-of select="ancestor::criterion/> 
</xsl:template>

which produces nothing!

As it should!

ancestor::criterion

is a relative expression, which means that it is evaluated off the current node (matched by the template). But the current node is the document node /.

So, the above is equivalent to:

/ancestor::criterion

However, by definition the document node / has no parents (and that means no ancestors), so this XPath expression doesn't select any node.

I expected it to produce:

<file> 
    <criteria> 
    </criteria> 
</file>

What you probably wanted was:

//criterion/ancestor::*

or

//*[descendant::criterion]

The last two XPath expressions are equivalent and select all elements that have a criterion descendant.

Finally, to produce the output you wanted, here is one possible solution:

<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="root | criterion | format"/>
</xsl:stylesheet>

When this transformation is applied on the provided XML document, the wanted output is produced:

<file>
<criteria>
</criteria>
</file>

Upvotes: 5

Welbog
Welbog

Reputation: 60418

ancestor is for selecting nodes that are higher (closer to the root) in the XML document. descendant is for selecting nodes that are lower (children) in the XML document.

In your example, ancestor::criterion selects nothing because the current node is / (meaning the root of the document - <file> in this case), as indicated by match="/". The root node has no ancestors so the ancestor axis does nothing.

To get every <criterion> element, you should use the descendant axis:

<xsl:template match="/">
  <xsl:copy-of select="descendant::criterion"/>
</xsl:template>

Or its shortcut //:

<xsl:template match="/">
  <xsl:copy-of select="//criterion"/>
</xsl:template>

That will return the following:

<criterion>AAA</criterion>

Using a loop or another template you can get all three of them:

<xsl:template match="/">
  <file>
    <xsl:apply-templates select="//criterion"/>
  </file>
</xsl:template>
<xsl:template match="criterion">
  <xsl:copy-of select="."/>
</xsl:template>

This will produce the following:

<file>
  <criterion>AAA</criterion>
  <criterion>BBB</criterion>
  <criterion>CCC</criterion> 
</file>

If you want to get the <file> element, too, it's a bit more complicated. XPath specifies nodes and simple copies will not copy the elements that contain the elements you select. I can clarify this point more if you're still confused.

Upvotes: 1

Related Questions