XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

Getting Loopy

Getting Loopy

August 01, 2001

Programming languages use loops to execute an action or series of actions multiple times. After performing the last action of such a series, the program "loops" back up to the first one. It may repeat these actions a specific number of times—for example, five times or thirty times. It may repeat the actions until a specified condition is true—for example, until there's no more input to read or until a prime number greater than a million has been calculated. XSLT offers two ways to repeat a series of actions:

  • The xsl:for-each instruction lets you perform a group of instructions on a given set of nodes. The specification of those nodes can take full advantage of the options offered by XPath's axis specifiers, node tests, and predicate conditions. In other words, for any set of a source tree's nodes that you can describe with an XPath expression, there's a way to say "perform this set of actions on these nodes". While this provides a way to execute a set of instructions repeatedly, you're repeating them over a set of nodes, not for an arbitrary number of iterations, which is what a "for" loop does in most programming languages.

  • By having a named template call itself recursively with parameters, you can execute a series of instructions for a fixed number of times or until a given condition is true. This technique comes from one of XSLT's ancestors, LISP, a programming language developed for artificial intelligence work in the 1960s. It may be unfamiliar to programmers accustomed to the "for" and "while" loops available in procedural like Java, C++, and Visual Basic, but it can perform the same tasks.

Iteration Across Nodes with xsl:for-each

When do you need xsl:for-each? Less often than you might think. If there's something you need to do with (or to) a particular set of nodes, an xsl:template template rule may be the best way to do it. Reviewing this approach will make it easier to understand what the xsl:for-each instruction can offer us.

In an xsl:template template rule, you specify a pattern in the match attribute that describes which nodes you want the rule to act on. For example, let's say we want to list the figure titles in the following document. (The sample document, stylesheets, and output are available in this zip file.)

<chapter>
<para>Then with expanded wings he steers his flight</para>
<figure><title>"Incumbent on the Dusky Air"</title>
<graphic fileref="pic1.jpg"/></figure>
<para>Aloft, incumbent on the dusky Air</para>
<sect1>
<para>That felt unusual weight, till on dry Land</para>
<figure><title>"He Lights"</title>
<graphic fileref="pic2.jpg"/></figure>
<para>He lights, if it were Land that ever burned</para>
<sect2>
<para>With solid, as the Lake with liquid fire</para>
<figure><title>"The Lake with Liquid Fire"</title>
<graphic fileref="pic3.jpg"/></figure>
</sect2>
</sect1>
</chapter>

The following stylesheet adds only these title elements to the result tree. It suppresses the para elements, which are the only other elements that have character data.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes" indent="no"/>

  <xsl:template match="figure/title">
    <xsl:apply-templates/>
  </xsl:template>

  <xsl:template match="para"/>

</xsl:stylesheet>

This stylesheet creates the following result from the document above:

"Incumbent on the Dusky Air"

"He Lights"

"The Lake with Liquid Fire"

Simple template rules aren't always enough to perform a series of actions on a specified set of nodes. What if you want to list the figure titles at the top of the result document and then output the rest of the source document under that list? A stylesheet, like the one above, that goes through the document adding only the figure's title elements to the result tree won't do this because the para elements need to be added as well. Our new stylesheet needs to add all the figure titles to the result tree as soon as it reaches the beginning of the source tree's chapter element, and then it must continue through the rest of the source tree processing the remaining nodes.

The xsl:for-each instruction is great for this. The following template rule has four children:

  • A text node with the string "Pictures:".

  • An xsl:for-each instruction to add the figure elements' title subelements to the result tree.

  • A text node with the string "Chapter: ".

  • An xsl:apply-templates element to add the chapter element's contents to the result tree.

<xsl:template match="chapter">
  <!-- Odd indenting to make result line up better -->
Pictures:
<xsl:for-each select="descendant::figure">
<xsl:value-of select="title"/><xsl:text>
</xsl:text>
  </xsl:for-each>

Chapter:<xsl:apply-templates/>
</xsl:template>

The xsl:for-each element's select attribute indicates which nodes to iterate over. The XPath expression used for this attribute value has an axis specifier of "descendant" and a node test of "figure", so taken together, the expression means "all the descendants of the chapter node (the one named in the template's match pattern) with 'figure' as their node name". This demonstrates a key advantage of the descendant axis over the default child one: the descendant::figure XPath expression gets the title element nodes from the chapter element's child, grandchild, and great-grandchild figure elements.

The contents of the xsl:for-each element consists of two things to add to the result tree for each of the nodes that the xsl:for-each iterates over:

  • The xsl:value-of element adds the content of each figure element's title child.

  • The xsl:text element with a single carriage return as its content adds that carriage return after each title value.

The following is the result:

  
Pictures:
"Incumbent on the Dusky Air"
"He Lights"
"The Lake with Liquid Fire"

Chapter:
Then with expanded wings he steers his flight
"Incumbent on the Dusky Air"

Aloft, incumbent on the dusky Air

That felt unusual weight, till on dry Land
"He Lights"

He lights, if it were Land that ever burned

With solid, as the Lake with liquid fire
"The Lake with Liquid Fire"

Rearranging a document's structure as you copy it to the result tree is one of the most popular uses of XSLT. Thus the xsl:for-each element is particularly valuable because of its ability to grab a copy of a set of nodes that aren't located together in the source tree, perform changes on those nodes, and then put them together in the result tree wherever you like.

Another advantage of acting on a set of nodes with an xsl:for-each element instead of with an xsl:template element lies in a limitation of template rules that XSLT novices often don't notice. While it may appear that you can use XPath expressions in an xsl:template element's match attribute, you're actually limited to the subset of XPath expressions known as patterns. In the xsl:for-each element's select attribute, however, you have the full power of XPath expressions available.

For example, you can't use the ancestor axis specifier in match patterns, but you can in an xsl:for-each element's select attribute. The following template uses it to list the names of all of a title element's ancestors.

<xsl:template match="title">
  <xsl:text>title ancestors:</xsl:text> 
  <xsl:for-each select="ancestor::*">
   <xsl:value-of select="name()"/>
    <!-- Output a comma if it's not the last one in
         the node set that for-each is going through. -->
   <xsl:if test="position() != last()">
   <xsl:text>,</xsl:text>
   </xsl:if>
  </xsl:for-each>
</xsl:template>

<xsl:template match="para"/>

The second template rule suppresses para elements from the result tree. The first template's "title ancestors:" and "," text nodes are inside of xsl:text elements to prevent the adjacent carriage returns from being copied to the result. This way, each title element's ancestor list is on one line right after the title introducing it.

This stylesheet outputs the following when applied to the document we saw earlier:

title ancestors:chapter,figure

title ancestors:chapter,sect1,figure

title ancestors:chapter,sect1,sect2,figure
    

Also in Transforming XML

Automating Stylesheet Creation

Appreciating Libxslt

Push, Pull, Next!

Seeking Equality

The Path of Control

Like the xsl:value-of instruction, xsl:for-each is a great way to grab some set of nodes from the source tree while the XSLT processor is applying a template to any other node. The xsl:value-of element has one crucial limitation that highlights the value of xsl:for-each: if you tell xsl:value-of to get a set of nodes, it only returns a string version of the first one in that set. If you tell xsl:for-each to get a set of nodes, it gets the whole set. As you iterate across that set of nodes you can do anything you want with them. (The xsl:copy-of instruction can also grab nearly any set of nodes, but with xsl:for-each you can do something with them before copying them to the result tree, like the addition of the text nodes in the example above.)

Note The xsl:sort instruction lets you sort the node set that your xsl:for-each element is iterating through.

Pages: 1, 2

Next Pagearrow







close