Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In XSLT how do I increment a global variable from a different scope?

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?

like image 776
Marcel Avatar asked May 07 '09 06:05

Marcel


People also ask

How do you increment a global variable in XSLT?

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. Show activity on this post. Use <xsl:variable name="RowNum" select="count(./preceding-sibling::*)" /> and $RowNum as an incrementing value.

How do you reassign a variable value in XSLT?

Variables in XSLT are not really variables, as their values cannot be changed. They resemble constants from conventional programming languages. The only way in which a variable can be changed is by declaring it inside a for-each loop, in which case its value will be updated for every iteration.

Which element allows you to do looping in XSLT?

The <xsl:for-each> element allows you to do looping in XSLT.

What is match in XSLT?

The match attribute is used to associate a template with an XML element. The match attribute can also be used to define a template for the entire XML document. The value of the match attribute is an XPath expression (i.e. match="/" defines the whole document).


3 Answers

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".

like image 50
Evan Lenz Avatar answered Sep 19 '22 18:09

Evan Lenz


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.

like image 22
BeWarned Avatar answered Sep 22 '22 18:09

BeWarned


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>
like image 38
jelovirt Avatar answered Sep 20 '22 18:09

jelovirt