views:

688

answers:

4

Hi,

I'm pretty new to XSLT, but need to use it for a CMS using at the moment. I've already come up with a problem, but I'll try to describe my it without going into too much information about the underlying CMS. If you need more context to help me, I can add it in.

So all I want to do is test if a node of my xml is a descendant of a particular node.

<xsl:if test="$currentNode::IsADescendantOf($someNode)">
Write this out.
</xsl:if>

Any ideas anyone?

Thanks in advance :)

+1  A: 

Look at the "ancestor" axis in your XSL/XPath reference

<xsl:if test="ancestor::*[. is $somenode]">

EDIT: I'm learning along with you. See if this works (I wish I had my XSL debugger available on this system :-)

Jim Garrison
@Jim Garrison: Thanks for the fast reply. That seems reasonable to me but I get this error:System.Xml.Xsl.XslLoadException: Expected a node test, found '$'.
Ev
You're right, my solution is incomplete. I don't have my XSL ref handy, but the basic idea is you need to use the ancestor axis to select all ancestor nodes and then filter it based on node equality to $somenode.
Jim Garrison
Thanks a lot. Now I get "is-same-node()' is an unknown XSLT function"I'll keep messing around with it, but let me know if you have any other ideas.
Ev
<xsl:if test="ancestor::*[$currentPage]">runs but always evaluates to true...
Ev
Edited to use `is` node comparison. See http://www.w3.org/TR/xpath20/#id-node-comparisons
Jim Garrison
You should point out that the `is` operator is not available in XPath 1.0, yet XPath 1.0 is by far the more common implementation.
Tomalak
@Tomalak: That's true - the CMS I'm using is using XPath 1.0. Thanks for the input.
Ev
@JimGarrison: Thanks for the help - I've ended up doing it but using a union and node count as in the answer above. Thanks again.
Ev
A: 

Descendant checking

The best way would be to use the descendant::* axis.

If you have a piece of XML looking like this and want to match the descendant <a> nodes from the first <alpha> node.

XML

<root>
<!-- check for descendants of the following alpha node-->
<alpha>
  <!-- match the following node-->
  <a>...</a>
  <b>...</b>
  <c>...</c>
</alpha>
<alpha>
  <a>...</a>
  <c>...</c>
</alpha>
</root>

XSLT

<xsl:if test="/root/alpha[1]/descendant::a[. is current()]">

Ancestor checking

This would probably work in most cases.

<xsl:if test="ancestor::*[name() = $node]">

Where

<xsl:variable name="node" select="name(//alpha)"/>

Or you can just use generate-id() if you have multiple nodes to compare against and need to make sure that it is a node match and not a name match.

<xsl:variable name="node" select="generate-id(//alpha[3])"/>
<xsl:if test="ancestor::*[generate-id(.) = $node]">

The is comparison operator cannot IMO be used when comparing ancestors since it requires a leaf node, and leaf nodes do not have descendants.

But i'd recommend using the descendant::* axis since it's much more readable and flexible.

Peter Lindqvist
+4  A: 

You should use union operation and node-set size comparison:

<xsl:if test="count($someNode|$currentNode/ancestor::*) = count($currentNode/ancestor::*)">
Write this out.
</xsl:if>

If $someNode is an ancestor of $currentNode, $someNode|$currentNode/ancestor::* will return the same node-set as $currentNode/ancestor::* (node-set don't have any doublons).

If not, the first node-set will have one more node than the second because of the union.

Erlock
@Erlock: Thanks a lot for the help. Worked a treat.
Ev
+1  A: 

A portable (XPath 1.0 and 2.0) solution would be:

<xsl:if test="
  $currentNode/ancestor::*[generate-id() = generate-id($someNode)]
">
Write this out.
</xsl:if>

This walks up the ancestor axis and checks every element in it. If (and only if) the unique ID of one of the ancestors matches the unique ID of $someNode, the resulting node-set is not empty.

Non-empty node-sets evaluate to true, so the condition is fulfilled.

Test - find all <baz> that are descendant of <foo>:

<xml>
  <foo>
    <bar>
      <baz>Test 1</baz>
    </bar>
  </foo>
  <baz>Test 2</baz>
</xml>

and

<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
  <xsl:template match="/">
    <xsl:variable name="someNode" select="//foo[1]" />

    <!-- test each <baz> node if it has <foo> as a parent -->
    <xsl:for-each select="//baz">
      <xsl:if test="
        ancestor::*[generate-id() = generate-id($someNode)]
      ">
        <xsl:copy-of select="." />
      </xsl:if>

    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

results in

<baz>Test 1</baz>

Note that if you are referring to the actual current node, like I do in the for-each, there is no need for a separate $currentNode variable. All XPath is relative to the current node by default.

A variant would be the top-down way. It is less efficient though (probably by several orders of magnitude):

<xsl:if test="
  $someNode[//*[generate-id() = generate-id($currentNode)]]
">
Write this out.
</xsl:if>
Tomalak