Reputation: 9800
I wonder, if there is a way to determine a path to a given source node in xslt. Let's assume the following source XML file:
<one>
<two>
<three/>
<three this="true"/>
</two>
<two>
<three/>
</two>
</one>
I'd like to have a function (custom template) that for node three with attribute this="true" would output something like the following: "1,1,2" - which means: starting from the root go into the first element, then to the first element and then to the second element to reach this particular node.
(the purpouse of this is that i want to have unique identifiers to content that i take from a particular place in the source XML document and transform it to my desired output)
EDIT: i found this:
<xsl:template name="genPath">
<xsl:param name="prevPath"/>
<xsl:variable name="currPath" select="concat('/',name(),'[',
count(preceding-sibling::*[name() = name(current())])+1,']',$prevPath)"/>
<xsl:for-each select="parent::*">
<xsl:call-template name="genPath">
<xsl:with-param name="prevPath" select="$currPath"/>
</xsl:call-template>
</xsl:for-each>
<xsl:if test="not(parent::*)">
<xsl:value-of select="$currPath"/>
</xsl:if>
</xsl:template>
under: How do you output the current element path in XSLT?
With several modification, this will output what i need by putting:
<xsl:call-template name="genPath">
Somewhere, but this way of calling it would output the path to the current node. How to modify it to enable to write a path to a specific child of the current node?
Something like:
<xsl:call-template name="genPath" select="n1:tagname/n1:tagname2">
(i know that the syntax above is incorrect)
Upvotes: 0
Views: 1708
Reputation: 4393
This is the code that I give to my students, it displays the XPath address to the current node in only four lines. The if
statement that follows the for-each
accommodates the current node being an attribute node:
<!--walk down the ancestry exposing the elements-->
<xsl:for-each select="ancestor-or-self::*">
<xsl:text/>/<xsl:value-of select="name(.)"/>
<xsl:if test="parent::*">[<xsl:number/>]</xsl:if>
</xsl:for-each>
<xsl:if test="not(self::*)">
<!--the current node must be an attribute-->
<xsl:text/>/@<xsl:value-of select="name(.)"/>
</xsl:if>
Note that a recursive call is unnecessary because one can simply walk the tree.
This code works in both XSLT 1.0 and XSLT 2.0.
Upvotes: 2
Reputation: 70618
If you were using XSLT2.0, you could write something like this:
<xsl:template match="three[@this='true']">
<xsl:value-of select="ancestor-or-self::*/(count(preceding-sibling::*) + 1)" separator="," />
</xsl:template>
But if you only had XSLT 1.0, you could do this by calling another template for each ancestort
<xsl:template match="three[@this='true']">
<xsl:apply-templates select="ancestor-or-self::*" mode="position" />
</xsl:template>
<xsl:template match="*" mode="position">
<xsl:if test="parent::*">,</xsl:if>
<xsl:number count="*" />
</xsl:template>
EDIT: To be able to apply this to any element, put this in a template matching any element, but with a mode attribute. For example
<xsl:template match="*" mode="genPath">
<xsl:apply-templates select="ancestor-or-self::*" mode="position" />
</xsl:template>
<xsl:template match="*" mode="position">
<xsl:if test="parent::*">,</xsl:if>
<xsl:number count="*" />
</xsl:template>
Then, to call it for any "child" element, just do this
<xsl:apply-templates select="n1:tagname1/n1:tagname2" mode="genPath" />
Upvotes: 2
Reputation: 9800
I figured it out.
this generates the output i need:
<xsl:template name="genPath">
<xsl:param name="prevPath"/>
<xsl:param name="childNode"/>
<xsl:if test="$childNode">
<xsl:for-each select="$childNode">
<xsl:call-template name="genPath"/>
</xsl:for-each>
</xsl:if>
<xsl:if test="not($childNode)">
<xsl:if test="$prevPath">
<xsl:variable name="currPath" select="concat(count(preceding-sibling::*)+1,'-',$prevPath)"/>
<xsl:for-each select="parent::*">
<xsl:call-template name="genPath">
<xsl:with-param name="prevPath" select="$currPath"/>
</xsl:call-template>
</xsl:for-each>
<xsl:if test="not(parent::*)">
<xsl:value-of select="$currPath"/>
</xsl:if>
</xsl:if>
<xsl:if test="not($prevPath)">
<xsl:variable name="currPath" select="count(preceding-sibling::*)+1"/>
<xsl:for-each select="parent::*">
<xsl:call-template name="genPath">
<xsl:with-param name="prevPath" select="$currPath"/>
</xsl:call-template>
</xsl:for-each>
<xsl:if test="not(parent::*)">
<xsl:value-of select="$currPath"/>
</xsl:if>
</xsl:if>
</xsl:if>
</xsl:template>
Now i can call:
<xsl:call-template name="genPath">
<xsl:with-param name="childNode" select="n1:tagname1/n1:tagname2"/>
</xsl:call-template>
Upvotes: 0
Reputation: 4739
I am not quite sure why I need position() -1
, maybe someone else could explain it, but here is what I got to get the output:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="@*|node()">
<xsl:apply-templates select="@*|node()" />
</xsl:template>
<xsl:template match="three[@this='true']">
<!-- Loop through all parent elements above this element -->
<xsl:for-each select="ancestor::node()">
<xsl:if test="(position() - 1) > 0">
<xsl:value-of select="position() - 1" /><xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
<!-- Current position of the found element -->
<xsl:value-of select="position()" />
</xsl:template>
</xsl:stylesheet>
Upvotes: 0