I am processing an XML file where I want to keep count of the number of nodes, so that I can use it as an ID as I write new nodes.
At the moment I have a global variable called 'counter'. I am able to access it within a template, but I haven't found a way of manipulating it within a template.
Here is a condensed version of my XSLT file:
<xsl:variable name="counter" select="1" as="xs:integer"/>
<xsl:template match="/">
<xsl:for-each select="section">
<xsl:call-template name="section"></xsl:call-template>
</xsl:for-each>
</xsl:template>
<xsl:template name="section">
<!-- Increment 'counter' here -->
<span class="title" id="title-{$counter}"><xsl:value-of select="title"/></span>
</xsl:template>
Any suggestions how to go from here?
Others have already explained how variables are immutable--that there are no assignment statements in XSLT (as with purely functional programming languages in general).
I have an alternative to the solutions that have been proposed so far. It avoids parameter passing (which is verbose and ugly in XSLT--even I'll admit that).
In XPath, you can simply count the number of
<section>
elements that precede the current one:(Note: the whitespace code formatting won't appear in your result, as whitespace-only text nodes get stripped from the stylesheet automatically. So don't feel compelled to put instructions on the same line.)
One big advantage of this approach (as opposed to using
position()
) is that it's only dependent on the current node, not on the current node list. If you changed your processing somehow (e.g., so<xsl:for-each>
processed not only sections but some other element too), then the value ofposition()
would no longer necessarily correspond to the position of<section>
elements in your document. On the other hand, if you usecount()
as above, then it will always correspond to the position of each<section>
element. This approach reduces coupling with other parts of your code, which is generally a very good thing.An alternative to count() would be to use the
<xsl:number>
instruction. It's default behavior will number all like-named elements at the same level, which happens to be what you want:It's a trade-off in verbosity (requiring an additional variable declaration if you still want to use the attribute value template curly braces), but only slightly so, as it also drastically simplifies your XPath expression.
There's yet more room for improvement. While we've removed dependency on the current node list, we still are dependent on the current node. That, in and of itself, is not a bad thing, but it's not immediately clear from looking at the template what the current node is. All we know is that the template is named "
section
"; to know for sure what's being processed, we have to look elsewhere in our code. But even that doesn't have to be the case.If you ever feel led to use
<xsl:for-each>
and<xsl:call-template>
together (as in your example), step back and figure out how to use<xsl:apply-templates>
instead.Not only is this approach less verbose (
<xsl:apply-templates/>
replaces both<xsl:for-each>
and<xsl:call-template/>
), but it also becomes immediately clear what the current node is. All you have to do is look at thematch
attribute, and you instantly know that you're processing a<section>
element and that<section>
elements are what you're counting.For a succinct explanation of how template rules (i.e.
<xsl:template>
elements that have amatch
attribute) work, see "How XSLT Works".Depending on your XSLT processor, you may be able to introduce scripted functions into your XLST. For example, the Microsoft XML library supports the inclusion of javascript. See http://msdn.microsoft.com/en-us/library/aa970889(VS.85).aspx for an example. This tactic obviously won't work if you're planning to deploy/execute XSLT on public client browsers; it has to be done by a specific XSLT processor.
Variables in XSLT are immutable so you have to approact the problem with that in mind. You could either use
position()
directly:Or in a more template orientated way:
variables are locally scoped and read only in xslt.
Use
<xsl:variable name="RowNum" select="count(./preceding-sibling::*)" />
and $RowNum as an incrementing value.Eg:
<xsl:template name="ME-homeTiles" match="Row[@Style='ME-homeTiles']" mode="itemstyle"> <xsl:variable name="RowNum" select="count(./preceding-sibling::*)" /> ...<a href="{$SafeLinkUrl}" class="tile{$RowNum}"><img ....></a>
This will create classes for link with values tile1, tile2, tile3 etc...
Haven't tried this myself, but you could try and pass a parameter to the template. In your first template you set the parameter to count() (or current() maybe?) within the for-each statement and then pass that value to your "section" template.
Here's more on passing parameters to templates