Numbering Things

XSLT provides the <xsl:number> element to number the parts of a document. (It can also be used to format a numeric value; more on that later.) In general, <xsl:number> counts something. We’ll look at a variety of examples here.

To fully illustrate how <xsl:number> works, we’ll need an XML document with some things to count. We’ll reuse our list of cars from the previous section:

<?xml version="1.0" encoding="utf-8"?>
<!-- cars.xml -->
<cars>
  <manufacturer name="Chevrolet">
    <car>Cavalier</car>
    <car>Corvette</car>
    <car>Impala</car>
    <car>Malibu</car>
  </manufacturer>
  <manufacturer name="Ford">
    <car>Pinto</car>
    <car>Mustang</car>
    <car>Taurus</car>
  </manufacturer>
  <manufacturer name="Volkswagen">
    <car>Beetle</car>
    <car>Jetta</car>
    <car>Passat</car>
    <car>Touraeg</car>
  </manufacturer>
</cars>

We’ll use <xsl:number> in several different ways to illustrate the various options we have in numbering things. We’ll start with something simple:

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

  <xsl:output method="html"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>Automobile manufacturers and their cars</title>
      </head>
      <body>
        <xsl:for-each select="cars/manufacturer">
          <p>
            <xsl:number format="1. "/>
            <xsl:value-of select="@name"/>
          </p>
        </xsl:for-each>
      </body>
    </html>
  </xsl:template>

</xsl:stylesheet>

We get this HTML document:

<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <title>Automobile manufacturers and their cars</title>
   </head>
   <body>
      <p>1. Chevrolet</p>
      <p>2. Ford</p>
      <p>3. Volkswagen</p>
   </body>
</html>

This is about the simplest example of <xsl:number> that you can write. (You could leave off the format attribute, but you’d get paragraphs such as <p>1Chevrolet</p>—probably not what you want.) Changing the stylesheet to use format="a. " generates these paragraphs:

      <p>a. Chevrolet</p>
      <p>b. Ford</p>
      <p>c. Volkswagen</p>

Here’s what we get with format="i. ":

      <p>i. Chevrolet</p>
      <p>ii. Ford</p>
      <p>iii. Volkswagen</p>

The <xsl:number> element has lots of other attributes and capabilities; we’ll look at the most common ones here. (See the complete description of the <xsl:number> element in Appendix A.) Here’s an example that uses the value attribute:

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

  <xsl:output method="html"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>Automobile manufacturers and their cars</title>
      </head>
      <body>
        <xsl:for-each select="cars/manufacturer">
          <p>
            <xsl:text>Cars produced by </xsl:text>
            <xsl:value-of select="@name"/>
            <xsl:text>: </xsl:text>
            <xsl:number value="count(car)" format="01"/>
          </p>
        </xsl:for-each>
      </body>
    </html>
  </xsl:template>

</xsl:stylesheet>

This stylesheet generates this HTML document:

<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <title>Automobile manufacturers and their cars</title>
   </head>
   <body>
      <p>Cars produced by Chevrolet: 04</p>
      <p>Cars produced by Ford: 03</p>
      <p>Cars produced by Volkswagen: 04</p>
   </body>
</html>

In this case, we could have used <xsl:value-of select="count(car)"/> to get similar results, but <xsl:number> lets us format the number as we want.

Using <xsl:number> with the format attribute is a good way to format any value, whether it comes from the XML source or not. For example, this markup:

<xsl:number value="1965" format="I"/>

produces the text MCMLXV.

As you’d expect, there are more powerful things we can do. Here’s an example that uses the level and count attributes:

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

  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:text>Automobile manufacturers and their cars&#xA;</xsl:text>
    <xsl:for-each select="cars/manufacturer">
      <xsl:number count="manufacturer" format="1. "/>
      <xsl:value-of select="@name"/>
      <xsl:text>&#xA;</xsl:text>
      <xsl:for-each select="car">
        <xsl:number count="manufacturer|car" level="multiple" 
          format="1.1. "/>
        <xsl:value-of select="."/>
        <xsl:text>&#xA;</xsl:text>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

This stylesheet gives us the following text document:

Automobile manufacturers and their cars
1. Chevrolet
1.1. Cavalier
1.2. Corvette
1.3. Impala
1.4. Malibu
2. Ford
2.1. Pinto
2.2. Mustang
2.3. Taurus
3. Volkswagen
3.1. Beetle
3.2. Jetta
3.3. Passat
3.4. Touraeg

The count attribute tells the XSLT processor what elements to count, and level="multiple" counts the manufacturers at one level and the cars per manufacturer at another. Notice that in the second <xsl:for-each< element we used the attribute count="manufacturer|car", even though we’re looking only at <car> elements. That’s because the number 3.2 means the second <car> from the third <manufacturer>. If we don’t include the manufacturers in our count, we won’t get the results we want.

The values for level are single, multiple, and any. The value single, the default, counts only an item’s siblings, while multiple counts an item along with any of its ancestors (that’s what we did in the previous example). level="any" counts an item along with everything that occurred before it in the document, whether it’s an ancestor of the current item or not. Here’s an example that uses level="any":

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

  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:text>Automobile manufacturers and their cars&#xA;</xsl:text>
    <xsl:for-each select="cars/manufacturer">
      <xsl:number count="manufacturer|car" level="any" format="1. "/>
      <xsl:value-of select="@name"/>
      <xsl:text>&#xA;</xsl:text>
      <xsl:for-each select="car">
        <xsl:number count="manufacturer|car" level="any" format="1. "/>
        <xsl:value-of select="."/>
        <xsl:text>&#xA;</xsl:text>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

Here we use an identical <xsl:number> element in both places. We’re counting all of the <manufacturer> and <car> elements, so we need to use level="any" and count="manufacturer|car" in both places. Here are the results:

Automobile manufacturers and their cars
1. Chevrolet
2. Cavalier
3. Corvette
4. Impala
5. Malibu
6. Ford
7. Pinto
8. Mustang
9. Taurus
10. Volkswagen
11. Beetle
12. Jetta
13. Passat
14. Touraeg

Also keep in mind that you can use <xsl:number> at isolated times. Here’s a contrived example that counts only even-numbered cars from each manufacturer:

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

  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:text>Automobile manufacturers and their cars&#xA;</xsl:text>
    <xsl:for-each select="cars/manufacturer">
      <xsl:value-of select="@name"/>
      <xsl:text>&#xA;</xsl:text>
      <xsl:for-each select="car">
        <xsl:text>  </xsl:text>
        <xsl:if test="(position() mod 2) = 0">
          <xsl:number count="manufacturer|car" level="multiple"
            format="1.1. "/>
        </xsl:if>
        <xsl:value-of select="."/>
        <xsl:text>&#xA;</xsl:text>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

To select only even-numbered items, we use the XPath mod operator. If we divide the position of the current element by 2 and the result is zero, we know we have an even-numbered item. This stylesheet gives us the following list:

Automobile manufacturers and their cars
Chevrolet
  Cavalier
  1.2. Corvette
  Impala
  1.4. Malibu
Ford
  Pinto
  2.2. Mustang
  Taurus
Volkswagen
  Beetle
  3.2. Jetta
  Passat
  3.4. Touraeg

In this example, we used <xsl:number> only for even-numbered cars from each manufacturer, and we never used <xsl:number> for the <manufacturer> element at all. Despite that, the <xsl:number> element calculates the correct value based on the position of the current item in the source document.

That can lead to some complications, however. If we sort the source document as we process it, our numbers look a little strange. We’ll add a <xsl:sort> element to our stylesheet:

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

  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:text>Automobile manufacturers and their cars&#xA;</xsl:text>
    <xsl:for-each select="cars/manufacturer">
      <xsl:value-of select="@name"/>
      <xsl:text>&#xA;</xsl:text>
      <xsl:for-each select="car">
        <xsl:sort select="."/>
        <xsl:text>  </xsl:text>
        <xsl:if test="(position() mod 2) = 0">
          <xsl:number count="manufacturer|car" level="multiple"
            format="1.1. "/>
        </xsl:if>
        <xsl:value-of select="."/>
        <xsl:text>&#xA;</xsl:text>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

And our results look like this:

Automobile manufacturers and their cars
Chevrolet
  Cavalier
  1.2. Corvette
  Impala
  1.4. Malibu
Ford
  Mustang
  2.1. Pinto
  Taurus
Volkswagen
  Beetle
  3.2. Jetta
  Passat
  3.4. Touraeg

The number for Pinto isn’t what we expected. That’s because the position() function is based on the current (sorted) context, while the numbering we’re doing is based on the original document order. In the sorted order Pinto is the second item, so the test of our <xsl:if> element is true. When we use <xsl:number>, however, Pinto is the first <car> in the source document. The result is the number 2.1 instead of the 2.2 we expected.

It’s not pretty, but here’s a stylesheet that fixes the problem. We use <xsl:number> to count <manufacturer> elements; that generates the first part of the number. Next we use position() to output the position of the sorted element. The stylesheet looks like this:

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

  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:text>Automobile manufacturers and their cars&#xA;</xsl:text>
    <xsl:for-each select="cars/manufacturer">
      <xsl:value-of select="@name"/>
      <xsl:text>&#xA;</xsl:text>
      <xsl:for-each select="car">
        <xsl:sort select="."/>
        <xsl:text>  </xsl:text>
        <xsl:if test="(position() mod 2) = 0">
          <xsl:number count="manufacturer" level="multiple"
            format="1."/>
          <xsl:value-of select="position()"/>
          <xsl:text>. </xsl:text>
        </xsl:if>
        <xsl:value-of select="."/>
        <xsl:text>&#xA;</xsl:text>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

This numbers the Pinto using the sorted order:

Automobile manufacturers and their cars
Chevrolet
  Cavalier
  1.2. Corvette
  Impala
  1.4. Malibu
Ford
  Mustang
  2.2. Pinto
  Taurus
Volkswagen
  Beetle
  3.2. Jetta
  Passat
  3.4. Touraeg

We used <xsl:number> to count the position of the current car within the current manufacturer.

Get XSLT, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.