Get all ancestors of current node

28,336

Solution 1

Congradulations to Adam for a very quick first answer.

Just to add a little detail:

Your listed expected result does not match your words. the root element also an ancestor node and the document is also an ancestor node.

ancestor::node()

... will return a sequence in this order:

  1. item[@title='b']
  2. item[@title='a']
  3. the root element (a.k.a. the document element)
  4. the root node /

To get the specific result you listed, you need:

ancestor::item/.

The effect of the /. is to change the ordering back to forward document order. The native order of ancestor:: is reverse document order.


Update: Illustration of points made in the comment feed.

This style-sheet (with OP's input)...

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

<xsl:template match="/">
  <xsl:for-each select="//item[@title='c']">
    <xsl:value-of select="ancestor::item[1]/@title" />
    <xsl:value-of select="ancestor::item[2]/@title" />
  </xsl:for-each>  
</xsl:template>         
</xsl:stylesheet>

... will output 'ba' illustrating the point that ancestor:: is indeed a reverse axis. And yet this style-sheet ...

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

<xsl:template match="/">
  <xsl:for-each select="//item[@title='c']">
    <xsl:value-of select="(ancestor::item/@title)[1]" />
    <xsl:value-of select="(ancestor::item/@title)[2]" />
  </xsl:for-each>  
</xsl:template>

</xsl:stylesheet>

... has the opposite result 'ab' . This is instructive because it shows that in XSLT 1.0 (not so in XSLT 2.0), the brackets remove the reverse nature, and it becomes a document ordered node-set.

The OP has asked about a transform something like....

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

<xsl:template match="/">
  <xsl:for-each select="//item[@title='c']">
    <xsl:for-each select="ancestor::item">
      <xsl:value-of select="@title" />
    </xsl:for-each>
  </xsl:for-each>  
</xsl:template>

</xsl:stylesheet>

This one returns 'ab' (in XSLT 2.0 it would return 'ba'). Why? Because in XSLT 1.0, the xsl:for-each instruction ignores the reverse-ness of the axis and processes in document order (unless an xsl:sort instruction says otherwise).

Solution 2

I want to get all ancestors of current node

The designated nodes obviously aren't all ancestors of the "green" node.

To select all ancestors use:

ancestor::node()

This selects all ancestor nodes of the initial conext node (or current node), including the top element and its parent -- the root node / -- which is a node, but isn't an element.

To select all element ancestors use:

ancestor::*

This is similar to the previous expression, but doesn't select the root node (/), because it isn't an element.

To select all ancestors named item, use:

ancestor::item

Do Note: All expressions above assume that (//item[@title='c'])[1] is the initial context node (current node). If this assumption is not correct, then (//item[@title='c'])[1]/ must be prepended to each of the expressions.

Note2: I strongly recommend for learning XPath to use a tool like the XPath Visualizer. For many years this tool has helped many thousands of people learn XPath the fun way -- by playing with XPath expressions and observing the result of their evaluation.

Note: The XPath Visualizer was created by me in year 2000 and was never a financial product. I am recommending it based solely on its value to the users, proven in the course of many years

Solution 3

You can use the xpath ancestor::item

Solution 4

The reason why you were outputting bcdx instead of ab is because your <xsl:for-each> is iterating over all of the item children of the selected item elements, rather than just iterating over the selected elements in the $test variable.

Change your for-each to just iterate over $test:

<xsl:variable name="curr" select="//item[@title = 'c']"></xsl:variable>
<xsl:variable name="test" select="$curr/ancestor::item"></xsl:variable>

<xsl:for-each select="$test">
    <xsl:value-of select="@title"></xsl:value-of>
</xsl:for-each>
Share:
28,336
Moriarty
Author by

Moriarty

Updated on September 13, 2020

Comments

  • Moriarty
    Moriarty over 3 years

    I want to get all ancestors of current node:

    XML:

    <root>
       <item title="a">
           <item title="b">               
               <item title="c"></item> <!--CURRENT-->
               <item title="d"></item>                 
            </item> 
           <item title="x">             
               <item title="y"></item> 
               <item title="z"></item>  
           </item>            
       </item> 
    </root>
    

    Result:

    <item title="a">...</item>
    <item title="b">...</item>
    

    Edit: Answers with axes ancestor are fine. My problem was elsewhere, in XSLT

    XSLT:

    <xsl:variable name="curr" select="//item[@title = 'c']"></xsl:variable>
    <xsl:variable name="test" select="$curr/ancestor::item"></xsl:variable>
    
    <xsl:for-each select="$test/item">
    <xsl:value-of select="@title"></xsl:value-of>
    </xsl:for-each>
    

    Returns:

    bcdx
    

    Edit2: for dimitre and for all who have a similar problem

    All the answers to my question were good.

    Just XSLT (up) returns to me a strange result and @Mads Hansen corrected me.

    FINAL WORKING EXAMPLE:

    XML:

    <?xml version="1.0" encoding="utf-8"?>
    <root>
       <item title="a">
           <item title="b">               
               <item title="c"></item> 
               <item title="d"></item>                 
            </item> 
           <item title="x">             
               <item title="y"></item> 
               <item title="z"></item>  
           </item>            
       </item> 
    </root>
    

    XSLT:

    <?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:variable name="curr" select="//item[@title = 'c']"></xsl:variable>
            <xsl:variable name="test" select="$curr/ancestor::item"></xsl:variable>
    
            <xsl:for-each select="$test">
                <xsl:value-of select="@title"></xsl:value-of>
            </xsl:for-each>
        </xsl:template>
    </xsl:stylesheet>
    

    Returns:

    ab
    
  • Sean B. Durkin
    Sean B. Durkin over 11 years
    Slight error: You need to reverse the order to meet the OP's expectations.
  • Moriarty
    Moriarty over 11 years
    i have same result with /. and without. <xsl:for-each select="//item[@title = 'c']/ancestor::item"> is the same <xsl:for-each select="//item[@title = 'c']/ancestor::item/."> .. result: ab
  • Sean B. Durkin
    Sean B. Durkin over 11 years
    In XPATH 1.0 or 2.0, ancestor:: is indeed a reverse axis (refer w3.org/TR/xpath section 2.4; and w3.org/TR/xpath20 section 3.2.1.1), But how you actually use it XSLT may change the order - and the behaviour depends on XSLT version. ....
  • Sean B. Durkin
    Sean B. Durkin over 11 years
    In XSLT 1.0, an xsl:for-each instruction will ignore the order in the xpath expression, (unless a sort applies). But in XSLT 2.0, the instrinsic order is retained.
  • Sean B. Durkin
    Sean B. Durkin over 11 years
    To understand what I mean, try using a positional predicate. Even in XSLT 1.0 using xsl:for-each, you get the reverse order.
  • Sean B. Durkin
    Sean B. Durkin over 11 years
    I will post a short piece of XSLT 1.0 to illustrate.
  • Sean B. Durkin
    Sean B. Durkin over 11 years
    My statement "Even in XSLT 1.0 using xsl:for-each, you get the reverse order" in earlier comment was very badly worded. Just ignore that one.