Path Trace

1. Get XPath to Node from Node
2. Trace the path to an element
3. Path to current node
4. Unique Node identification
5. Grabbing the path to a particular node

1.

Get XPath to Node from Node

Dimitre Novatchev


> I need to be able to get the node's XPath.   Does anyone have an
> example for how to do this?  I am using MSXML 3.0.
	

Below is a sample xml document and a stylesheet that takes a node-set parameter and produces one valid XPath expression for every member-node.

stylesheet (buildPath.xsl):

<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:msxsl="urn:schemas-microsoft-com:xslt" 
>

<xsl:output method="text"/>
<xsl:variable name="theParmNodes" select="//namespace::*[local-name() =
'myNamespace']"/>
<xsl:template match="/">
  <xsl:variable name="theResult">
    <xsl:for-each select="$theParmNodes">
    <xsl:variable name="theNode" select="."/>
    <xsl:for-each select="$theNode |
$theNode/ancestor-or-self::node()[..]">
      <xsl:element name="slash">/</xsl:element>
      <xsl:choose>
        <xsl:when test="self::*">			
          <xsl:element name="nodeName">
            <xsl:value-of select="name()"/>
            <xsl:variable name="thisPosition" 
                select="count(preceding-sibling::*[name(current()) = 
                        name()])"/>
            <xsl:variable name="numFollowing" 
                select="count(following-sibling::*[name(current()) = 
                        name()])"/>
            <xsl:if test="$thisPosition + $numFollowing > 0">
              <xsl:value-of select="concat('[', $thisPosition +
                                                           1, ']')"/>
            </xsl:if>
          </xsl:element>
        </xsl:when>
        <xsl:otherwise> <!-- This node is not an element -->
          <xsl:choose>
            <xsl:when test="count(. | ../@*) = count(../@*)">	
            <!-- Attribute -->
              <xsl:element name="nodeName">
                <xsl:value-of select="concat('@',name())"/>
              </xsl:element>
            </xsl:when>		
            <xsl:when test="self::text()">	<!-- Text -->
              <xsl:element name="nodeName">
                <xsl:value-of select="'text()'"/>
                <xsl:variable name="thisPosition" 
                          select="count(preceding-sibling::text())"/>
                <xsl:variable name="numFollowing" 
                          select="count(following-sibling::text())"/>
                <xsl:if test="$thisPosition + $numFollowing > 0">
                  <xsl:value-of select="concat('[', $thisPosition + 
                                                           1, ']')"/>
                </xsl:if>
              </xsl:element>
            </xsl:when>		
            <xsl:when test="self::processing-instruction()">
            <!-- Processing Instruction -->
              <xsl:element name="nodeName">
                <xsl:value-of select="'processing-instruction()'"/>
                <xsl:variable name="thisPosition" 
                   select="count(preceding-sibling::processing-instruction())"/>
                <xsl:variable name="numFollowing" 
                    select="count(following-sibling::processing-instruction())"/>
                <xsl:if test="$thisPosition + $numFollowing > 0">
                  <xsl:value-of select="concat('[', $thisPosition + 
                                                            1, ']')"/>
                </xsl:if>
              </xsl:element>
            </xsl:when>		
            <xsl:when test="self::comment()">	<!-- Comment -->
              <xsl:element name="nodeName">
                <xsl:value-of select="'comment()'"/>
                <xsl:variable name="thisPosition" 
                         select="count(preceding-sibling::comment())"/>
                <xsl:variable name="numFollowing" 
                         select="count(following-sibling::comment())"/>
                <xsl:if test="$thisPosition + $numFollowing > 0">
                  <xsl:value-of select="concat('[', $thisPosition + 
                                                            1, ']')"/>
                </xsl:if>
              </xsl:element>
            </xsl:when>		
            <!-- Namespace: -->
            <xsl:when test="count(. | ../namespace::*) = 
                                               count(../namespace::*)">

              <xsl:variable name="apos">'</xsl:variable>
              <xsl:element name="nodeName">
                <xsl:value-of select="concat('namespace::*', 
                '[local-name() = ', $apos, local-name(), $apos, ']')"/>

              </xsl:element>
            </xsl:when>		
          </xsl:choose>
        </xsl:otherwise>			
      </xsl:choose>
    </xsl:for-each>
    <xsl:text>&#xA;</xsl:text>
  </xsl:for-each>
 </xsl:variable>
 <xsl:value-of select="msxsl:node-set($theResult)"/>
</xsl:template>
</xsl:stylesheet>

 
xml source (buildPath.xml):
- -------------------------
<!-- top level Comment -->
<root>
    <nodeA>textA</nodeA>
 <nodeA id="nodeA-2">
  <?myProc ?>
        xxxxxxxx
  <nodeB/>
        <nodeB xmlns:myNamespace="myTestNamespace">
  <!-- Comment within /root/nodeA[2]/nodeB[2] -->
   <nodeC/>
  <!-- 2nd Comment within /root/nodeA[2]/nodeB[2] -->
        </nodeB>
        yyyyyyy
  <nodeB/>
  <?myProc2 ?>
    </nodeA>
</root>
<!-- top level Comment -->

Result:

/root/nodeA[2]/nodeB[2]/namespace::*[local-name() = 'myNamespace']
/root/nodeA[2]/nodeB[2]/nodeC/namespace::*[local-name() =
'myNamespace']

(Eds attempt at humour)

Mike Kay spoils the fun by offering:

If you want a one-line solution rather than 100 lines, then the alternative is

saxon:path()

Some people always take the easy way out!

2.

Trace the path to an element

Steve Tinney

How can a template be created, such that it will print out
the complete path of current node from the root?

I added an attribute to your tree to give me something to activate on.

tree.xml:
<A>
  <B>
    <C/>
  </B>
  <B>
    <C/>
    <C activate="yes"/>
        <!--call template with current() here -->
  </B>
  <B>
  </B>
</A>
showpath.xsl:
<?xml version='1.0'?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="text"/>

<xsl:template match="*">
  <xsl:if test="./@activate='yes'">
    <xsl:for-each select="ancestor-or-self::*">
      <xsl:call-template name="print-step"/>
    </xsl:for-each>
  </xsl:if>
  <xsl:apply-templates select="*"/>
</xsl:template>

<xsl:template name="print-step">
  <xsl:value-of select="name()"/>
  <xsl:text>:</xsl:text>
  <xsl:value-of select="1+count(preceding-sibling::*)"/>
  <xsl:text>.</xsl:text>
</xsl:template>
</xsl:stylesheet>
Result:
A:1.B:2.C:2.

            

3.

Path to current node

Jeni Tennison

Considering this xml:

<departments>
	<employees>
		<name>Joe Shmo</name>
	</employees>
</employees>

If the current node is "name", is there a simple XSL: way of outputting the "full path", ie: "departments/employees/name/Joe Shmo"

The 'full path' that you're describing are the names of the ancestor elements of the current node, plus the current node, plus the current node's value, separated by '/'s.

There are two ways of doing this: iteration and recursion. I'm going to show you iteration because it keeps everything in one place. You do iteration using an xsl:for-each element. We want to iterate over the list of the ancestor elements of the current node, but we also want to include the current node, and helpfully there is an XPath axis that enables us to do just that: ancestor-or-self. We don't care what the ancestor elements are called. So:

<xsl:for-each select="ancestor-or-self::*">
  ...
</xsl:for-each>

OK, so for each of those nodes we want to output the name of the node, followed by a forward shash and no whitespace:

<xsl:for-each select="ancestor-or-self::*">
  <xsl:value-of select="name()" /><xsl:text>/</xsl:text>
</xsl:for-each>

Once we've outputted the information about the ancestor elements, we want to round it off with information about the value of the current node:

<xsl:value-of select="." />

So, the complete template is:

<xsl:template match="name">
  <xsl:for-each select="ancestor-or-self::*">
    <xsl:value-of select="name()" /><xsl:text>/</xsl:text>
  </xsl:for-each>
  <xsl:value-of select="." />
</xsl:template>

Easy, eh?

4.

Unique Node identification

David Carlisle


 <!-- paths from elements -->
 <xsl:template name="elementPath">
   <xsl:for-each select="(ancestor-or-self::*)">/*[<xsl:value-of
     select="1+count(preceding-sibling::*)"/>]</xsl:for-each>
 </xsl:template>

 <!-- paths from attributes -->
 <xsl:template name="attributePath">
   <xsl:for-each select="parent::*">
     <xsl:call-template name="elementPath" />
   </xsl:for-each>
   <xsl:text>/@</xsl:text>
   <xsl:value-of select="name(.)"/>
 </xsl:template>

    

5.

Grabbing the path to a particular node

Jeni Tennison


>Unless I've missed an easier design,
>one basically has to walk the tree up to the root node, to grab the location
>path. At that point, the location path is in reverse, so one must write
>another recursive template to switch the order. That's the part I'm doing
>now, as the first part was pretty easy.

It only constructs it in the wrong order if you recurse in the wrong place. If you do:

<xsl:template match="*" mode="path">
  <xsl:text>/</xsl:text>
  <xsl:value-of select="name()" />
  <xsl:apply-templates select="parent::*" mode="path" />
</xsl:template>

or something similar, then you are outputting the step for each parent *after* the step for its child, so the path is the wrong way round. If, on the other hand, you do:

<xsl:template match="*" mode="path">
  <xsl:apply-templates select="parent::*" mode="path" />
  <xsl:text>/</xsl:text>
  <xsl:value-of select="name()" />
</xsl:template>

then you generate the information for the parent before the information for the child, which gives you the hierarchy that you want.

It is actually possible to do this without recursion because the ancestor-or-self axis gives you a list of the nodes that are ancestors of the current node (or the current node itself). You can *iterate* over this list instead:

<xsl:for-each select="ancestor-or-self::*">
  <xsl:text>/</xsl:text>
  <xsl:value-of select="name()" />
</xsl:for-each>

You only need to do recursion when the hierarchy path that you're constructing is not the same as the hierarchy path that you have in your source. This happens when, for example, you have a flat structure describing a number of classes each of which have attributes giving links to the parent class.

In addition you should bear in mind that the paths you create using this method do not give you exact directions to the node that you want. To do so, you should add predicates to the path indicating the position of the node relative to those of its siblings with the same name or use some other method to identify the unique properties of a particular element (e.g. the value of an attribute, particularly an ID attribute).

Wendell Piez adds:

It works this way because xsl:for-each, by default, processes the selected nodes in document order, which is down from the root, even though the axis itself goes in reverse. This is pretty subtle.

You can change the order of its processing by using a sort. So (again, without the recursion) to get them in reverse order, we could use

  <xsl:for-each select="ancestor-or-self::*">
    <xsl:sort select="count(ancestor::*)" order="descending"/>
    <xsl:value-of select="concat('/', name())"/>
  </xsl:for-each>