views:

7532

answers:

7

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?

+2  A: 

variables are locally scoped and read only in xslt.

Luixv
I see. Do you know an approach I can take to achieve what I am after?
Marcel Tjandraatmadja
First I would say you should avoid to use the foreach construct and the call-template. This are procedural statements and XSLT is recursive. Therefore you should think it recursively instead procedural. What the user @Bewarned is showing is a valid way to increment your counter via a parameter. Then better use the apply-template with a parameter adding 1 each time is called. Just comment this if I am not clear.
Luixv
+3  A: 

XSLT variables cannot be changed. You'll have pass the value along from template to template.

If you are using XSLT 2.0, you can have parameters and use tunneling to propagate the variable to the right templates.

Your template will look something like this:

<xsl:template match="a">
<xsl:param name="count" select="0">
  <xsl:apply-templates>
     <xsl:with-param select="$count+1"/>
  </xsl:apply-templates>
</xsl:template>

Also look at using generate-id() if you want to create ids.

BeWarned
A: 

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

mattRo55
+1  A: 

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.

Alan McBee
I've used that trick before, but it should only be done as a last resort, where structuring it along immutable/functional lines would be prohibitive. But it works. In some scenarios (such as .NET) you can use extension objects to do the same thing outside of the xslt, but again: that doesn't make it a great idea.
Marc Gravell
+1  A: 

You can use the position() function to do what you want. It would look something like this.

<xsl:template match="/">
  <xsl:for-each select="section">
    <xsl:call-template name="section">
      <xsl:with-param name="counter" select="{position()}"/>
    </xsl:call-template>
  </xsl:for-each>
</xsl:template>

<xsl:template name="section">
  <xsl:param name="counter"/>
  <span class="title" id="title-{$counter}">
    <xsl:value-of select="title"/>
  </span>
</xsl:template>
Bhushan
The select attribute of xsl:with-param is an Expression, not a String that can use AVTs.
jelovirt
Also, there's no need to pass the value of position(), because <xsl:call-template> won't change the current node list. You can just as easily access the same value, using position(), from within the "section" template.
Evan Lenz
+2  A: 

Variables in XSLT are immutable so you have to approact the problem with that in mind. You could either use position() directly:

<xsl:template match="/"> 
   <xsl:for-each select="section">
      <xsl:call-template name="section"/>
   </xsl:for-each>
</xsl:template>

<xsl:template name="section">
   <span class="title" id="title-{position()}"><xsl:value-of select="title"/></span>
</xsl:template>

Or in a more template orientated way:

<xsl:template match="/"> 
   <xsl:apply-templates select="section"/>
</xsl:template>

<xsl:template match="section">
   <span class="title" id="title-{position()}"><xsl:value-of select="title"/></span>
</xsl:template>
jelovirt
+8  A: 

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:

<xsl:template name="section">
  <span class="title" id="title-{1 + count(preceding-sibling::section)}">
    <xsl:value-of select="title"/>
  </span>
</xsl:template>

(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 of position() would no longer necessarily correspond to the position of <section> elements in your document. On the other hand, if you use count() 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:

<xsl:template name="section">
  <xsl:variable name="count">
    <xsl:number/>
  </xsl:variable>
  <span class="title" id="title-{$count}">
    <xsl:value-of select="title"/>
  </span>
</xsl:template>

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.

<xsl:template match="/doc">
  <xsl:apply-templates select="section"/>
</xsl:template>

<xsl:template match="section">
  <xsl:variable name="count">
    <xsl:number/>
  </xsl:variable>
  <span class="title" id="title-{$count}">
    <xsl:value-of select="title"/>
  </span>
</xsl:template>

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 the match 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 a match attribute) work, see "How XSLT Works".

Evan Lenz