Jesper Rønn-Jensen
Jesper Rønn-Jensen

Reputation: 111616

XSLT: find position of node in a given nodeset

Imagine an XML tree with nodes in several depths

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <ELEM2>
    <ELEM3/>
  </ELEM2>
  <ELEM2>
    <ELEM3/>
  </ELEM2>
  <ELEM2>
    <ELEM3/>
  </ELEM2>
</root>

I would like to know the unique position of each element in the entire tree.

Desired output:

  <?xml version="1.0"?>
  <root position="1"/>
    <ELEM2 position="2"/>
      <ELEM3 position="3"/>
    <ELEM2 position="4"/>
      <ELEM3 position="5"/>
    <ELEM2 position="6"/>
      <ELEM3 position="7"/>
    

So, basically, I'd like to see the position in the list of all elements, and use that index to find it.

Attempt 1: position(): I tried using position() like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  
  <xsl:template match="*">
    <xsl:copy>
      <xsl:attribute name="position">
        <xsl:value-of select="position()"/>
      </xsl:attribute>
    </xsl:copy>
    <xsl:apply-templates select="node()"/>
  </xsl:template>
</xsl:stylesheet>

which gave me duplicated position attributes:

  <?xml version="1.0"?>
  <root position="1"/>
    <ELEM2 position="2"/>
      <ELEM3 position="2"/>
    <ELEM2 position="4"/>
      <ELEM3 position="2"/>
    <ELEM2 position="6"/>
      <ELEM3 position="2"/>

So, I cannot use position() in the current nodeset, since that position is limited to the current branch in the tree.

Attempt 2: preceding:::

Then I looked at using preceding:: like so:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:template match="*">
    <xsl:copy>
      <xsl:attribute name="position">
        <xsl:value-of select="count(preceding::*)"/>
      </xsl:attribute>
    </xsl:copy>
    <xsl:apply-templates select="node()"/>
  </xsl:template>
</xsl:stylesheet>

But unfortunately I got this:

<?xml version="1.0"?>
<root position="0"/>
  <ELEM2 position="0"/>
    <ELEM3 position="0"/>
  <ELEM2 position="2"/>
    <ELEM3 position="2"/>
  <ELEM2 position="4"/>
    <ELEM3 position="4"/>

So, preceding::: counts siblings on the axis of the parents sibling and up to the root.

Attempt 3: preceding-sibling:::

Not even worth it. This will only look through position of other sibling elements, which mean I will get same positions for each of the ELEM3 elements.

Attempt 4: generate-id():

This was a slightly different approach if we relax the limitation that the @position attribute reflects a node number. This doesn't matter in my setup as long as the generated value is unique.

This one will generate unique @position attributes. Unfortunately the ids are only guaranteed to be same within the same run. This means I cannot use this for tests and our CI/test setup.

Attempt 5: Find current node in given list?

I am thinking if I can in some way provide the list of elements via an XPATH like so: * or ELEM2|ELEM3. Now if I could then take the current() node and find it's position in the list.

Is this possible?

Limitations:

Upvotes: 1

Views: 1246

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243449

Or simply:

count(ancestor::*) + count(preceding::*) +1

Here is a complete transformation (just 10 lines):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <xsl:template match="*">
    <xsl:copy>
      <xsl:attribute name="position" select="count(ancestor::*) + count(preceding::*) +1"/>
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

When applied on the provided XML document:

<root>
    <ELEM2>
        <ELEM3/>
    </ELEM2>
    <ELEM2>
        <ELEM3/>
    </ELEM2>
    <ELEM2>
        <ELEM3/>
    </ELEM2>
</root>

the wanted, correct result is produced:

<root position="1">
      <ELEM2 position="2">
            <ELEM3 position="3"/>
      </ELEM2>
      <ELEM2 position="4">
            <ELEM3 position="5"/>
      </ELEM2>
      <ELEM2 position="6">
            <ELEM3 position="7"/>
      </ELEM2>
</root>

Upvotes: 1

Martin Honnen
Martin Honnen

Reputation: 167506

Use <xsl:number count="*" level="any"/>:

https://xsltfiddle.liberty-development.net/gVhEaiH

Upvotes: 2

Related Questions