Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding an index on tag value with XSLT: `position()` returning double the expected value

Tags:

xml

xslt

xslt-1.0

I have an input XML:

<root>
<child>somevalue</child>
<child>othervalue</child>
</root>

And a need this output:

<root>
<child>item1_somevalue</child>
<child>item2_othervalue</child>
</root>

I'm using this template:

<xsl:template match="*[local-name()='root' and namespace-uri()='']/*[local-name()='child' and namespace-uri()='']">
<xsl:element name="{local-name()}">
<xsl:text>item</xsl:text>
<xsl:value-of select="position()"/>
<xsl:text>_</xsl:text>
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>

And getting this result:

<child>item2_somevalue</child>
<child>item4_othervalue</child>

Why is position() returning double the expected value?

I'm not worried about the root tag right now.

Must use XSLT 1.0.

like image 306
jpmo22 Avatar asked Dec 07 '25 05:12

jpmo22


2 Answers

Explaining what's going on

The position() function can return unexpected results sometimes. At first glance, it sounds like it should return the position of the context element with respect to its parent container. So for our sample input XML:

<root>
    <child>somevalue</child>
    <child>othervalue</child>
</root>

... we would expect to see that the position() for the first child element would be 1, and the position() for the second child element would be 2.

But when we run our sample XSL against that code, we get the following output:

<child>item2_somevalue</child>
<child>item4_othervalue</child>

The key is that we need to pay special attention to the wording. The position() element doesn't just count elements, it counts nodes -- and that includes text nodes. So in the sample input XML above, the first child element is actually the second node under root, because it's preceded by a text node (the newline and leading whitespace). And, for the same reason, the second child element is actually the fourth node under root. So position() is returning exactly what it should.

Fixing the behavior

There are a couple ways of fixing this. One is to explicitly count child elements, as jpmo22's post does. Another approach, which is more flexible, is to tell the XSL processor to ignore whitespace-only text nodes. We can do this by adding a top-level element to our XSLT code:

<xsl:strip-space elements="*"/>

We can customize the list of elements if needed, by replacing the * with a list of element names.

If we just add that strip-space directive before the template in our sample XSL code, we can get the expected result:

<child>item1_somevalue</child>
<child>item2_othervalue</child>

Other considerations

  • The XPath in the match statement is convoluted.

    <xsl:template match="*[local-name()='root' and namespace-uri()='']/*[local-name()='child' and namespace-uri()='']">
    

    In cases where element names have no prefixes, local-name and name return identical results. Our sample input XML has no prefixes, so there's no reason to use local-name. In fact, we could simply use the desired element names rather than *. Moreover, our sample input XML has no namespace declarations at all, so there's no reason to use namespace-uri.

    A suggested simplification:

    <xsl:template match="root/child">
    
  • The use of element is also a bit convoluted.

    <xsl:element name="{local-name()}">
    

    We already know that we're working with child elements, since that's what we just matched in this template. We could more simply just use the element name in a literal element declaration, like this:

    <child>
    

    If we wanted to genericize our code, we could also just copy over the context element:

    <xsl:copy>
    
like image 175
Eiríkr Útlendi Avatar answered Dec 08 '25 23:12

Eiríkr Útlendi


I found the solution. I changed the position() to count(preceding::*[local-name()='child']) + 1

XSLT:

<xsl:template match="*[local-name()='child']">
<xsl:element name="{local-name()}">
<xsl:text>item</xsl:text>
<xsl:value-of select="count(preceding::*[local-name()='child']) + 1"/>
<xsl:text>_</xsl:text>
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>

Gives the right result:

<child>item1_somevalue</child>
<child>item2_othervalue</child>
like image 20
jpmo22 Avatar answered Dec 09 '25 00:12

jpmo22