I choose a block of marble and chop off whatever I don’t need.
Francois-Auguste Rodin
I have [traversed] the length and breadth of this country and talked with the best people, and I can assure you that data processing is a fad that won’t last out the year.
The editor in charge of business books for a major publishing company, 1957
Doing anything remotely interesting in XSLT involves two related operations: determining which XML nodes to visit (selecting) and determining in what order you want to visit them (traversing). Selecting is largely in the domain of XPath, a separate specification but one intimately related to XSLT. Traversing is a function of built-in control structures of XSLT and how you organize your templates to harness them.
XSLT veterans are unlikely to find much revelation in this particular chapter. Nevertheless, it is important for two reasons. First, the ideas presented in these recipes distinguish XSLT from other programming languages and therefore tend to be things that trip up novices on their first attempts to master XSLT. Second, the examples covered in this section are the primitive building blocks of many more complex recipes covered in later chapters. Virtually everything one does in XSLT involves application of selection and traversal. By analogy to cooking, knowing how to make a good brown stock is a prerequisite to making a sauce espagnole![1] This chapter is comprised of examples of the “brown stock” variety and should be mastered before proceeding to more advanced applications of XSLT.
Although this chapter presents some primitive examples, it is not an
XPath or XSLT tutorial. The reader should know the basics of XPath
covered in Chapter 1. I also assume you know what the default XSLT
processing rules are and how XSLT determines what templates should be
processed next. If you feel you need some help in these areas, I
would recommend either Michael Kay’s XSLT
Programmer’s Reference (Wrox Press, 2004)
or Evan Lenz’ XSLT 1.0 Pocket
Reference (O’Reilly, 2005). You will not
see as much distinction between XSLT 1.0 and 2.0 here as in previous
chapters. This is due to the fact that XSLT 1.0 was fairly complete
in its ability to express most of these operations. The exception is
in cases where the selection is based on constructing groups of nodes
that are related by criteria other than XML’s
hierarchical organization. Here, the new
xsl:for-each-group
instruction is a highly welcome
addition.
Several examples in this section use computer-science terminology commonly associated with algorithms involving tree-data structures. The uninitiated may wonder what trees have to do with XML. The answer is that XML can be viewed as a language for specifying trees and XSLT as a language that processes these trees. In fact, it is possible to make an XSLT processor process non-XML input by making the processor think it is a tree using a SAX driver. Michael Kay demonstrates this technique in XSLT Programmer’s Reference (Wrox, 2001), as does Eric M. Burke in Java and XSLT (O’Reilly, 2001). In any case, the important concept that motivates the examples in this chapter is that thinking in terms of trees allows one to discover several useful XML processing techniques.
The examples provided in the chapter draw heavily on the following two test documents. To avoid repeating them in each recipe, I list them in Example 5-1 and Example 5-2.
<?xml version="1.0" encoding="UTF-8"?> <salesBySalesperson> <salesperson name="John Adams" seniority="1"> <product sku="10000" totalSales="10000.00"/> <product sku="20000" totalSales="50000.00"/> <product sku="25000" totalSales="920000.00"/> </salesperson> <salesperson name="Wendy Long" seniority="5"> <product sku="10000" totalSales="990000.00"/> <product sku="20000" totalSales="150000.00"/> <product sku="30000" totalSales="5500.00"/> </salesperson> <salesperson name="Willie B. Aggressive" seniority="10"> <product sku="10000" totalSales="1110000.00"/> <product sku="20000" totalSales="150000.00"/> <product sku="25000" totalSales="2920000.00"/> <product sku="30000" totalSales="115500.00"/> <product sku="70000" totalSales="10000.00"/> </salesperson> <salesperson name="Arty Outtolunch" seniority="10"/> </salesBySalesperson>
<?xml version="1.0" encoding="UTF-8"?> <employee name="Jil Michel" sex="female"> <employee name="Nancy Pratt" sex="female"> <employee name="Phill McKraken" sex="male"/> <employee name="Ima Little" sex="female"> <employee name="Betsy Ross" sex="female"/> </employee> </employee> <employee name="Jane Doe" sex="female"> <employee name="Walter H. Potter" sex="male"/> <employee name="Wendy B.K. McDonald" sex="female"> <employee name="Craig F. Frye" sex="male"/> <employee name="Hardy Hamburg" sex="male"/> <employee name="Rich Shaker" sex="male"/> </employee> </employee> <employee name="Mike Rosenbaum" sex="male"> <employee name="Cindy Post-Kellog" sex="female"> <employee name="Allen Bran" sex="male"/> <employee name="Frank N. Berry" sex="male"/> <employee name="Jack Apple" sex="male"/> </employee> <employee name="Oscar A. Winner" sex="male"> <employee name="Jack Nicklaus" sex="male"> <employee name="R.P. McMurphy" sex="male"/> </employee> <employee name="Tom Hanks" sex="male"> <employee name="Forrest Gump" sex="male"/> <employee name="Andrew Beckett" sex="male"/> </employee> <employee name="Susan Sarandon" sex="female"> <employee name="Helen Prejean" sex="female"/> </employee> </employee> </employee> </employee>
You want to select all nodes that are unique in a given context based on uniqueness criteria.
Selecting unique nodes is a common application of the
preceding
and preceding-sibling
axes. If the elements you select are not all siblings, then use
preceding
.
The
following code produces a unique list of products from
SalesBySalesperson.xml:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/"> <products> <xsl:for-each select="//product[not(@sku=preceding::product/@sku)]"> <xsl:copy-of select="."/> </xsl:for-each> </products> </xsl:template> </xsl:stylesheet>
If the elements are all siblings, then use
preceding-sibling
:
<products> <product sku="10000" totalSales="10000.00"/> <product sku="10000" totalSales="990000.00"/> <product sku="10000" totalSales="1110000.00"/> <product sku="20000" totalSales="50000.00"/> <product sku="20000" totalSales="150000.00"/> <product sku="20000" totalSales="150000.00"/> <product sku="25000" totalSales="920000.00"/> <product sku="25000" totalSales="2920000.00"/> <product sku="30000" totalSales="5500.00"/> <product sku="30000" totalSales="115500.00"/> <product sku="70000" totalSales="10000.00"/> </products> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/products "> <products> <xsl:for-each select="product[not(@sku=preceding-sibling::product/@sku)]"> <xsl:copy-of select="."/> </xsl:for-each> </products> </xsl:template> </xsl:stylesheet>
To avoid preceding
, which can be inefficient,
travel up to the ancestors that are siblings,
and then use preceding-sibling
and travel down to
the nodes you want to test:
<xsl:for-each select="//product[not(@sku=../preceding-sibling::*/product/@sku)]"> <xsl:copy-of select="."/> </xsl:for-each>
If you are certain that the elements are sorted so that duplicate nodes are adjacent (as in the earlier products), then you only have to consider the immediately preceding sibling:
<xsl:for-each select="/salesperson/product[not(@name=preceding-sibling::product[1]/@name]"> <!-- do something with each uniquely named product --> </xsl:for-each>
You can solve this problem in XSLT 2.0 as a grouping problem. Simply
use for-each-group
with the uniqueness criteria as
the group-by
value. Use the first node in the
current group as the unique one:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/"> <products> <xsl:for-each-group select="//product" group-by="@sku"> <xsl:copy-of select="current-group()[1]"/> </xsl:for-each-group> </products> </xsl:template> </xsl:stylesheet>
Using the node-set()
extension function, you can also do the
following:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt=" http://exslt.org"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/"> <xsl:variable name="products"> <xsl:for-each select="//product"> <xsl:sort select="@sku"/> <xsl:copy-of select="."/> </xsl:for-each> </xsl:variable> <products> <xsl:for-each select="exslt:node-set($products)/product"> <xsl:variable name="pos" select="position()"/> <xsl:if test="$pos = 1 or not(@sku = $products/preceding-sibling::product[1]/@sku"> <xsl:copy-of select="."/> </xsl:if> </xsl:for-each> </products> </xsl:template>
However, I have never found this technique to be faster than using
the preceding
axis. This technique does have an
advantage in situations where the duplicate testing is not trivial.
For example, consider a case where duplicates are determined by the
concatenation of two attributes:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt=" http://exslt.org"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/"> <xsl:variable name="people"> <xsl:for-each select="//person"> <xsl:sort select="concat(@lastname,@firstname)"/> <xsl:copy-of select="."/> </xsl:for-each> </xsl:variable> <products> <xsl:for-each select="exslt:node-set($people/)person"> <xsl:variable name="pos" select="position()"/> <xsl:if test="$pos = 1 or concat(@lastname,@firstname) != concat(people/person[$pos - 1]/@lastname, people/person[$pos - 1]/@firstname)"> <xsl:copy-of select="."/> </xsl:if> </xsl:for-each> </products> </xsl:template>
When you attempt to remove duplicates, the following examples do not work:
<xsl:template match="/">
<products>
<xsl:for-each select="//product[not(@sku=preceding::product[1]/@sku)]">
<xsl:sort select="@sku"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</products>
</xsl:template>
Do not sort to avoid considering all but the immediately preceding
element. The axis is relative to the node’s original
order in the document. The same situation applies when using
preceding-sibling
. The following code is also sure
to fail:
<xsl:template match="/"> <xsl:variable name="products"> <xsl:for-each select="//product"> <!-- sort removed from here --> <xsl:copy-of select="."/> </xsl:for-each> </xsl:variable> <products> <xsl:for-each select="exsl:node-set($products)/product"> <xsl:sort select="@sku"/> <xsl:variable name="pos" select="position()"/> <xsl:if test="$pos = 1 or @sku != $products/product[$pos - 1]/@sku"> <xsl:copy-of select="."/> </xsl:if> </xsl:for-each> </products> </xsl:template>
This code fails because position()
returns the
position after sorting, but the contents of
$products
has not been sorted; instead, an
inaccessible copy of it was.
Sometimes you only want to remove duplicate elements when they are adjacent. Consider, for example, a data set derived from a series of measurements taken at set time intervals. If the system being measured is fairly stable, there may be many adjacent measurements that are equal. One may want to remove these adjacent duplicates without removing other equal measurements that appear later in the sequence.
For this problem, you would still use
xsl:for-each-group
but with
group-adjacent
rather than
group-by
:
<measurements>
<data time="12:00:00" value="1.0"/>
<data time="12:00:01" value="1.0"/>
<data time="12:00:02" value="1.1"/>
<data time="12:00:03" value="1.1"/>
<data time="12:00:04" value="1.0"/>
<data time="12:00:05" value="1.1"/>
<data time="12:00:06" value="1.2"/>
<data time="12:00:07" value="1.3"/>
<data time="12:00:08" value="1.4"/>
<data time="12:00:09" value="1.6"/>
<data time="12:00:10" value="1.9"/>
<data time="12:00:11" value="2.1"/>
<data time="12:00:12" value="1.7"/>
<data time="12:00:13" value="1.5"/>
</measurements>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/measurements">
<xsl:copy>
<xsl:for-each-group select="data" group-adjacent="@value"
>
<xsl:copy-of select="current-group()[1]"/>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
<!--Output -->
<measurements>
<data time="12:00:00" value="1.0"/>
<data time="12:00:02" value="1.1"/>
<data time="12:00:04" value="1.0"/>
<data time="12:00:05" value="1.1"/>
<data time="12:00:06" value="1.2"/>
<data time="12:00:07" value="1.3"/>
<data time="12:00:08" value="1.4"/>
<data time="12:00:09" value="1.6"/>
<data time="12:00:10" value="1.9"/>
<data time="12:00:11" value="2.1"/>
<data time="12:00:12" value="1.7"/>
<data time="12:00:13" value="1.5"/>
</measurements>
The XSLT FAQ (http://www.dpawson.co.uk/xsl/sect2/N2696.html) describes a solution that uses keys and describes solutions to related problems.
You want to select all elements in a specific context, except the ones you choose to exclude.
The best way to select all but a specific element is to say:
<xsl:apply-templates select="*[not(self::element-to-ignore)]"/>
or, if iterating, say:
<xsl:for-each select="*[not(self::element-to-ignore)]"> ... </xsl:for-each>
When XSLT newbies first need to select all but a specific element, they will probably think of the following construct first:
<xsl:apply-templates select="*[name() != 'element-to-ignore']"/>
This code works in many cases, but it could cause trouble when the
document uses namespaces. Recall that name()
returns the node’s
QName: the namespace prefix concatenated to the local part of the
name. However, in any given XML document, nothing forces the author
to use a specific prefix:
<!--This will fail if the author decided to use SALES:product instead of sales:product --> <xsl:apply-templates select="*[name() != 'sales:product']"/>
Alternatively, you could use local-name()
.
However, this prefix would ignore elements from all namespaces that
have that particular local name, which might not be what you want.
This recommendation applies only in the case of elements, not
attributes. If you need to select all but a specific attribute, use
local-name()
. The self axis, when applied to a
name, refers only to elements. In other words, use
<xsl:copy-of select=@*[local-name() !=
'ignored-attribute']/>
and not <xsl:copy-of
select=@*[not(self::ignored-attribute)]/>
.
Finally, just in case of confusion, selecting all but a single element is different from selecting all but a single instance of element. The latter is used in an example discussed earlier in this chapter:
<xsl:apply-templates select="*[generate-id() != generate-id($node-to-ignore)]"/>
Jeni Tennison’s XSLT and XPath on the
Edge (M&T Books, 2001) details when and when not to
use name()
and local-name()
.
There are several ways to solve this problem in XSLT 1.0, but the easiest to understand computes the position of the nodes that should be selected at each step. If you had the following unstructured document and you desired to select the paragraphs that are bracketed by headings, you can use this technique:
<doc> <heading>Structure, I don't need any stinkin structure</heading> <para>1.1</para> <para>1.2</para> <para>1.3</para> <para>1.4</para> <heading>Confessions of a flat earther</heading> <para>2.1</para> <para>2.2</para> <para>2.3</para> <heading>Flat hierarchies save trees!</heading> <para>3.1</para> <para>3.2</para> </doc> <xsl:template match="/doc"> <xsl:copy> <!-- First select the bracketing elements --> <xsl:apply-templates select="heading"/> </xsl:copy> </xsl:template> <!-- Match on the bracketing elements --> <xsl:template match="heading"> <!-- Compute how many of the desired elements (para) follow this heading --> <xsl:variable name="numFollowingPara" select="count(following-sibling::para)"/> <!-- Compute how many of the desired elements (para) follow the next heading and subtract from the preceding count to get the position of the last para in this group--> <xsl:variable name="lastParaInHeading" select="$numFollowingPara - count(following-sibling::heading[1]/following-sibling::para)"/> <!-- You now can select the desired elements by their position relative to the current heading --> <xsl:apply-templates select="following-sibling::para[position() <= $lastParaInHeading]"/> </xsl:template>
This problem is tailor-made for the for-each-group
instruction. Specifically, you would use the
group-starting-with
attribute:
<xsl:template match="/doc"> <xsl:copy> <xsl:for-each-group select="*" group-starting-with="heading"> <!--Select the para elements in the group bracketed by heading --> <xsl:apply-templates select="current-group()[self::para]"/> </xsl:for-each-group> </xsl:copy> </xsl:template>
Selecting nodes based on their position relative to other nodes is a
common requirement in document-oriented XML transformations where
structure is implied rather than literally encoded into the hierarchy
of the document. Clearly, if each group consisting of a heading and
paragraphs was contained in a separate parent element (for example, a
section
element), then the problem would be
trivial. This is a classic trade-off between ease of use for the
document creators vs. ease of use for the document transformers. With
the introduction of for-each-group
in XSLT 2.0,
the trade-off equals out since you can much more easily deal with
unstructured documents.
Recipe 8.8 shows applications of this technique for transforming implicitly structured documents into explicitly structured ones. It also shows other ways the problem can be approached in XSLT 1.0 and 2.0.
Solutions to this recipe have the following general form:
<xsl:template match="node()"> <!-- Do something with current node --> <!--Process children --> <xsl:apply-templates/> </xsl:template>
The term preorder is computer-science jargon for traversing a tree, so you visit the root and recursively visit the children of the root in preorder. This process is arguably the most common means of processing XML. Of this idiom’s many applications, this chapter will consider two. As you review recipes in later sections, you will see this mode of traversal arise frequently.
Consider an organization chart (Example 5-2) encoded in a simplistic fashion so that an employee element B is a child of another employee element A if B reports to A. Example 5-1 employs a preorder traversal to explain who manages whom. Example 5-2 shows the output.
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:strip-space elements="*"/> <xsl:template match="/employee" priority="10"> <xsl:value-of select="@name"/><xsl:text> is the head of the company. </xsl:text> <xsl:call-template name="HeShe"/><xsl:text> manages </xsl:text> <xsl:call-template name="manages"/> <xsl:apply-templates/> </xsl:template> <xsl:template match="employee[employee]"> <xsl:value-of select="@name"/><xsl:text> is a manager. </xsl:text> <xsl:call-template name="HeShe"/> <xsl:text> manages </xsl:text> <xsl:call-template name="manages"/> <xsl:apply-templates/> </xsl:template> <xsl:template match="employee"> <xsl:value-of select="@name"/><xsl:text> has no worries. </xsl:text> <xsl:text>

</xsl:text> </xsl:template> <xsl:template name="HeShe"> <xsl:choose> <xsl:when test="@sex = 'male' "> <xsl:text>He</xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>She</xsl:text> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="manages"> <xsl:for-each select="*"> <xsl:choose> <xsl:when test="position() != last() and last() > 2"> <xsl:value-of select="@name"/><xsl:text>, </xsl:text> </xsl:when> <xsl:when test="position() = last() and last() > 1"> <xsl:text> and </xsl:text><xsl:value-of select="@name"/><xsl:text>. </xsl:text> </xsl:when> <xsl:when test="last() = 1"> <xsl:value-of select="@name"/><xsl:text>. </xsl:text> </xsl:when> <xsl:otherwise> <xsl:value-of select="@name"/> </xsl:otherwise> </xsl:choose> </xsl:for-each> <xsl:text>

</xsl:text> </xsl:template> </xsl:stylesheet>
Jil Michel is the head of the company. She manages Nancy Pratt, Jane Doe, and Mike Rosenbaum. Nancy Pratt is a manager. She manages Phill McKraken and Ima Little. Phill McKraken has no worries. Ima Little is a manager. She manages Betsy Ross. Betsy Ross has no worries. Jane Doe is a manager. She manages Walter H. Potter and Wendy B.K. McDonald. Walter H. Potter has no worries. Wendy B.K. McDonald is a manager. She manages Craig F. Frye, Hardy Hamburg, and Rich Shaker. Craig F. Frye has no worries. Hardy Hamburg has no worries. Rich Shaker has no worries. Mike Rosenbaum is a manager. He manages Cindy Post-Kellog and Oscar A. Winner. Cindy Post-Kellog is a manager. She manages Allen Bran, Frank N. Berry, and Jack Apple. Allen Bran has no worries. Frank N. Berry has no worries. Jack Apple has no worries. Oscar A. Winner is a manager. He manages Jack Nickolas, Tom Hanks, and Susan Sarandon. Jack Nicklaus is a manager. He manages R.P. McMurphy. R.P. McMurphy has no worries. Tom Hanks is a manager. He manages Forrest Gump and Andrew Beckett. Forrest Gump has no worries. Andrew Beckett has no worries. Susan Sarandon is a manager. She manages Helen Prejean. Helen Prejean has no worries.
A more serious application of preorder traversal is the conversion of an expression tree to prefix notation. Given the MathML content fragment in Example 5-3 and Example 5-4, you can create a transformation to a Lisp-like syntax. Example 5-5 shows the output.
<apply> <eq/> <apply> <plus/> <apply> <power/> <ci>x</ci> <cn>2</cn> </apply> <apply> <times/> <cn>4</cn> <ci>x</ci> </apply> <cn>4</cn> </apply> <cn>0</cn> </apply>
<?xml version="1.0" encoding="UTF-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:strip-space elements="*"/> <xsl:template match="apply"> <xsl:value-of select="local-name(*[1])"/> <xsl:text>(</xsl:text> <xsl:apply-templates/> <xsl:text>)</xsl:text> <xsl:if test="following-sibling::*">,</xsl:if> </xsl:template> <xsl:template match="ci|cn"> <xsl:value-of select="."/> <xsl:if test="following-sibling::*">,</xsl:if> </xsl:template> </xsl:stylesheet>
The MathML converter is not the purest example of a preorder
traversal, largely because MathML encodes mathematical expressions
unconventionally. This is largely an instance of the preorder idiom
because the code processes the apply
element first
(and outputs the name of its first child element) and then processes
its child elements recursively (via
<xsl:apply-templates/>
). The code following
apply-templates
balances the parentheses, inserts
commas
where necessary, and involves no further traversal.
You want to recursively process the children of an element first and then the element itself.
Solutions to this recipe have the following general form:
<xsl:template match="node()"> <!--Process children --> <xsl:apply-templates/> <!-- Do something with current node --> </xsl:template>
The term postorder is computer-science jargon for traversing a tree so that you recursively visit the children of the root in postorder and then visit the root. This algorithm produces a stylesheet that processes the outermost leaf nodes and works its way up to the document root.
You can apply a postorder traversal to the organizational chart (orgchart.xml) to produce an explanation of who reports to whom, starting from the bottom, as shown in Example Example 5-6. Example Example 5-7 shows the output.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:strip-space elements="*"/> <xsl:template match="/employee" priority="10"> <xsl:apply-templates/> <xsl:value-of select="@name"/><xsl:text> is the head of the company. </xsl:text> <xsl:call-template name="reportsTo"/> <xsl:call-template name="HimHer"/> <xsl:text>. </xsl:text> <xsl:text>

</xsl:text> </xsl:template> <xsl:template match="employee[employee]"> <xsl:apply-templates/> <xsl:value-of select="@name"/><xsl:text> is a manager. </xsl:text> <xsl:call-template name="reportsTo"/> <xsl:call-template name="HimHer"/> <xsl:text>. </xsl:text> <xsl:text>

</xsl:text> </xsl:template> <xsl:template match="employee"> <xsl:text>Nobody reports to </xsl:text> <xsl:value-of select="@name"/><xsl:text>. 
</xsl:text> </xsl:template> <xsl:template name="HimHer"> <xsl:choose> <xsl:when test="@sex = 'male' "> <xsl:text>him</xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>her</xsl:text> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="reportsTo"> <xsl:for-each select="*"> <xsl:choose> <xsl:when test="position() < last() - 1 and last() > 2"> <xsl:value-of select="@name"/><xsl:text>, </xsl:text> </xsl:when> <xsl:when test="position() = last() - 1 and last() > 1"> <xsl:value-of select="@name"/><xsl:text> and </xsl:text> </xsl:when> <xsl:when test="position() = last() and last() = 1"> <xsl:value-of select="@name"/><xsl:text> reports to </xsl:text> </xsl:when> <xsl:when test="position() = last()"> <xsl:value-of select="@name"/><xsl:text> report to </xsl:text> </xsl:when> <xsl:otherwise> <xsl:value-of select="@name"/> </xsl:otherwise> </xsl:choose> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Nobody reports to Phill McKraken. Nobody reports to Betsy Ross. Ima Little is a manager. Betsy Ross reports to her. Nancy Pratt is a manager. Phill McKraken and Ima Little report to her. Nobody reports to Walter H. Potter. Nobody reports to Craig F. Frye. Nobody reports to Hardy Hamburg. Nobody reports to Rich Shaker. Wendy B.K. McDonald is a manager. Craig F. Frye, Hardy Hamburg, and Rich Shaker report to her. Jane Doe is a manager. Walter H. Potter and Wendy B.K. McDonald report to her. Nobody reports to Allen Bran. Nobody reports to Frank N. Berry. Nobody reports to Jack Apple. Cindy Post-Kellog is a manager. Allen Bran, Frank N. Berry, and Jack Apple report to her. Nobody reports to R.P. McMurphy. Jack Nicklaus is a manager. R.P. McMurphy reports to him. Nobody reports to Forrest Gump. Nobody reports to Andrew Beckett. Tom Hanks is a manager. Forrest Gump and Andrew Beckett report to him. Nobody reports to Helen Prejean. Susan Sarandon is a manager. Helen Prejean reports to her. Oscar A. Winner is a manager. Jack Nickolas, Tom Hanks, and Susan Sarandon report to him. Mike Rosenbaum is a manager. Cindy Post-Kellog and Oscar A. Winner report to him. Jil Michel is the head of the company. Nancy Pratt, Jane Doe, and Mike Rosenbaum report to her.
You have an XML document or fragment that represents an expression to be processed in-order.
An in-order traversal is most applicable to a binary tree. The general form of the algorithm in this case follows:
<xsl:template match="node()"> <!--Process left subtree --> <xsl:apply-templates select="*[1]"/> <!-- Do something with current node --> <!--Process right subtree --> <xsl:apply-templates select="*[2]"/> </xsl:template>
However, in-order traversal can extend to n-ary trees with the following algorithm:
<xsl:template match="node()"> <xsl:variable name="current-node" select="."/> <!--Process left subtree --> <xsl:apply-templates select="*[1]"/> <!-- Do something with $current-node --> <!-- Apply recursively to middle children <xsl:for-each select="*[position() > 1 and position() < last()"> <!-- Process "left" subtree --> <xsl:apply-templates select="."/> <!--Do something with $current-node --> </xsl:for-each> <!--Process right subtree --> <xsl:apply-templates select="*[last()]"/> </xsl:template>
The rationale behind this algorithm can be better understood by considering Figure 5-1, which shows the binary equivalent of an n-ary tree. The generalized n-ary in-order traversal produces the same result as the binary in-order traversal on the binary equivalent tree.
This form of traversal has a much narrower range of applicability then other traversal examples in this chapter. One notable application, shown in Example 5-8 and Example 5-9, is as a component of a stylesheet that converts MathML markup to C or Java-style infix expressions. Example 5-10 shows the output.
<apply> <eq/> <apply> <plus/> <apply> <minus/> <ci>y</ci> <cn>2</cn> </apply> <apply> <times/> <cn>4</cn> <apply> <plus/> <ci>x</ci> <cn>1</cn> </apply> </apply> <cn>8</cn> </apply> <cn>0</cn> </apply>
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:C="http://www.ora.com/XSLTCookbook/nampespaces/C"> <xsl:output method="text"/> <xsl:strip-space elements="*"/> <!-- Table to convert from MathML operation names to C operators --> <C:operator mathML="plus" c="+" precedence="2"/> <C:operator mathML="minus" c="-" precedence="2"/> <C:operator mathML="times" c="*" precedence="3"/> <C:operator mathML="div" c="/" precedence="3"/> <C:operator mathML="mod" c="%" precedence="3"/> <C:operator mathML="eq" c="= =" precedence="1"/> <!-- load operation conversion table into a variable --> <xsl:variable name="ops" select="document('')/*/C:operator"/> <xsl:template match="apply"> <xsl:param name="parent-precedence" select="0"/> <!-- Map mathML operation to operator name and precedence --> <xsl:variable name="mathML-opName" select="local-name(*[1])"/> <xsl:variable name="c-opName" select="$ops[@mathML=$mathML-opName]/@c"/> <xsl:variable name="c-opPrecedence" select="$ops[@mathML=$mathML-opName]/@precedence"/> <!-- Parenthesis required if the precedence of the containing expression is greater than current sub-expression --> <xsl:if test="$parent-precedence > $c-opPrecedence"> <xsl:text>(</xsl:text> </xsl:if> <!-- Recursively process the left sub-tree which is at position 2 in MathML apply element--> <xsl:apply-templates select="*[2]"> <xsl:with-param name="parent-precedence" select="$c-opPrecedence"/> </xsl:apply-templates> <!-- Process the current node (i.e. the operator at position 1 in MathML apply element --> <xsl:value-of select="concat(' ',$c-opName,' ')"/> <!-- Recursively process middle children --> <xsl:for-each select="*[position()>2 and position() < last()]"> <xsl:apply-templates select="."> <xsl:with-param name="parent-precedence" select="$c-opPrecedence"/> </xsl:apply-templates> <xsl:value-of select="concat(' ',$c-opName,' ')"/> </xsl:for-each> <!-- Recursively process right subtree--> <xsl:apply-templates select="*[last()]"> <xsl:with-param name="parent-precedence" select="$c-opPrecedence"/> </xsl:apply-templates> <!-- Parenthesis required if the precedence of the containing expression is greater than current sub-expression --> <xsl:if test="$parent-precedence > $c-opPrecedence"> <xsl:text>)</xsl:text> </xsl:if> </xsl:template> <xsl:template match="ci|cn"> <xsl:value-of select="."/> </xsl:template> </xsl:stylesheet>
Obviously, this stylesheet is not a full-fledged MathML-to-C translator. However, Chapter 9 discusses this problem more thoroughly.
You want to order elements by increasing level (tree depth). In other words, you want to traverse the tree breadth first.
This form of traversal is tailor-made for using
xsl:for-each
along with xsl:sort
:
<xsl:for-each select="//*"> <xsl:sort select="count(ancestor::*)" data-type="number"/> <!-- process the current element --> </xsl:for-each>
This recursive solution is longer and less obvious:
<xsl:template match="/*"> <xsl:call-template name="level-order"/> </xsl:template> <xsl:template name="level-order"> <xsl:param name="max-level" select="10"/> <xsl:param name="current-level" select="1"/> <xsl:choose> <xsl:when test="$current-depth <= $max-level"> <!-- process the current level --> <xsl:call-template name="level-order-aux"> <xsl:with-param name="level" select="$current-level"/> <xsl:with-param name="actual-level" select="$current-level"/> </xsl:call-template> <!-- process the next level --> <xsl:call-template name="level-order"> <xsl:with-param name="current-level" select="$current-level + 1"/> </xsl:call-template> </xsl:when> </xsl:choose> </xsl:template> <xsl:template name="level-order-aux"> <xsl:param name="level" select="1"/> <xsl:param name="actual-level" select="1"/> <xsl:choose> <xsl:when test="$level = 1"> <!-- Process the current element here --> <!-- $actual-level is the number of the current level --> </xsl:when> <xsl:otherwise> <!-- Recursively descend to the next level on all children --> <xsl:for-each select="*"> <xsl:call-template name="level-order-aux"> <xsl:with-param name="level" select="$level - 1"/> <xsl:with-param name="actual-level" select="$actual-level"/> </xsl:call-template> </xsl:for-each> </xsl:otherwise> </xsl:choose> </xsl:template>
This solution requires that you either set an arbitrary bound for the maximum depth of the tree or compute the maximum depth. One way of doing so is by finding the node that has no children and more ancestors than any other node without children:
<xsl:param name="max-level"> <xsl:for-each select="//*[not(*)]"> <xsl:sort select="count(ancestor::*)" data-type="number" order="descending" /> <xsl:if test="position() = 1"> <xsl:value-of select="count(ancestor::*) + 1" /> </xsl:if> </xsl:for-each> </xsl:param>
Arguably, the need to traverse an XML document
by
level does not arise often. Grouping nodes by depth or distance from
the root does not fit into XSLT’s natural control
flow, which is organized to express traversals that descend the
document tree. This is apparent by the fact that that you had to use
xsl:sort
to coerce the stylesheet to process nodes
by their depth or, equivalently, the number of ancestors. The
complexity of the recursive solution provides further evidence of
this unnatural process.
Still, this form of traversal is sometimes handy. For example, you can use it to process an organizational chart that lists employees by their distance from the top spot, as shown in Example 5-11. The output is shown in Example 5-12.
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:template match="/"> <xsl:for-each select="//employee"> <xsl:sort select="count(ancestor::*)" order="ascending"/> <xsl:variable name="level" select="count(ancestor::*)"/> <xsl:choose> <xsl:when test="$level = 0"> <xsl:value-of select="@name"/> <xsl:text> is the head honcho.
</xsl:text> </xsl:when> <xsl:otherwise> <xsl:value-of select="@name"/> <xsl:text> has </xsl:text> <xsl:value-of select="$level"/> <xsl:text> boss(es) to knock off.
</xsl:text> </xsl:otherwise> </xsl:choose> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Jil Michel is the head honcho. Nancy Pratt has 1 boss(es) to knock off. Jane Doe has 1 boss(es) to knock off. Mike Rosenbaum has 1 boss(es) to knock off. Phill McKraken has 2 boss(es) to knock off. Ima Little has 2 boss(es) to knock off. Walter H. Potter has 2 boss(es) to knock off. Wendy B.K. McDonald has 2 boss(es) to knock off. Cindy Post-Kellog has 2 boss(es) to knock off. Oscar A. Winner has 2 boss(es) to knock off. Betsy Ross has 3 boss(es) to knock off. Craig F. Frye has 3 boss(es) to knock off. Hardy Hamburg has 3 boss(es) to knock off. Rich Shaker has 3 boss(es) to knock off. Allen Bran has 3 boss(es) to knock off. Frank N. Berry has 3 boss(es) to knock off. Jack Apple has 3 boss(es) to knock off. Jack Nicklaus has 3 boss(es) to knock off. Tom Hanks has 3 boss(es) to knock off. Susan Sarandon has 3 boss(es) to knock off. R.P. McMurphy has 4 boss(es) to knock off. Forrest Gump has 4 boss(es) to knock off. Andrew Beckett has 4 boss(es) to knock off. Helen Prejean has 4 boss(es) to knock off.
One advantage of the recursive variation is that it is easy to tell when you transition from one level to the next. You could use this information to better format your output, as shown in Example 5-13 and Example 5-14.
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text" version="1.0" encoding="UTF-8"/> <xsl:strip-space elements="*"/> <xsl:template match="/employee"> <xsl:call-template name="level-order"/> </xsl:template> <xsl:template name="level-order"> <xsl:param name="max-depth" select="10"/> <xsl:param name="current-depth" select="0"/> <xsl:choose> <xsl:when test="$current-depth <= $max-depth"> <xsl:variable name="text"> <xsl:call-template name="level-order-aux"> <xsl:with-param name="level" select="$current-depth"/> <xsl:with-param name="actual-level" select="$current-depth"/> </xsl:call-template> </xsl:variable> <xsl:if test="normalize-space($text)"> <xsl:value-of select="$text"/> <xsl:text>
</xsl:text> <xsl:call-template name="level-order"> <xsl:with-param name="current-depth" select="$current-depth + 1"/> </xsl:call-template> </xsl:if> </xsl:when> </xsl:choose> </xsl:template> <xsl:template name="level-order-aux"> <xsl:param name="level" select="0"/> <xsl:param name="actual-level" select="0"/> <xsl:choose> <xsl:when test="$level = 0"> <xsl:choose> <xsl:when test="$actual-level = 0"> <xsl:value-of select="@name"/> <xsl:text> is the head honcho.
</xsl:text> </xsl:when> <xsl:otherwise> <xsl:value-of select="@name"/> <xsl:text> has </xsl:text> <xsl:value-of select="$actual-level"/> <xsl:text> boss(es) to knock off.
</xsl:text> </xsl:otherwise> </xsl:choose> </xsl:when> <xsl:otherwise> <xsl:for-each select="employee"> <xsl:call-template name="level-order-aux"> <xsl:with-param name="level" select="$level - 1"/> <xsl:with-param name="actual-level" select="$actual-level"/> </xsl:call-template> </xsl:for-each> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
Jil Michel is the head honcho. Nancy Pratt has 1 boss(es) to knock off. Jane Doe has 1 boss(es) to knock off. Mike Rosenbaum has 1 boss(es) to knock off. Phill McKraken has 2 boss(es) to knock off. Ima Little has 2 boss(es) to knock off. Walter H. Potter has 2 boss(es) to knock off. Wendy B.K. McDonald has 2 boss(es) to knock off. Cindy Post-Kellog has 2 boss(es) to knock off. Oscar A. Winner has 2 boss(es) to knock off. Betsy Ross has 3 boss(es) to knock off. Craig F. Frye has 3 boss(es) to knock off. Hardy Hamburg has 3 boss(es) to knock off. Rich Shaker has 3 boss(es) to knock off. Allen Bran has 3 boss(es) to knock off. Frank N. Berry has 3 boss(es) to knock off. Jack Apple has 3 boss(es) to knock off. Jack Nickolas has 3 boss(es) to knock off. Tom Hanks has 3 boss(es) to knock off. Susan Sarandon has 3 boss(es) to knock off. R.P. McMurphy has 4 boss(es) to knock off. Forrest Gump has 4 boss(es) to knock off. Andrew Beckett has 4 boss(es) to knock off. Helen Prejean has 4 boss(es) to knock off.
If, for some reason, you wished to have random access to nodes at any specific level, you could define a key as follows:
<xsl:key name="level" match="employee" use="count(ancestor::*)"/> <xsl:template match="/"> <xsl:for-each select="key('level',3)"> <!-- do something with the nodes on level 3 --> </xsl:for-each> </xsl:template>
You can also make the match more specific or use a predicate with the
key
function:
<xsl:key name="level" match="//employee" use="count(ancestor::*)"/> <xsl:template match="/"> <xsl:for-each select="key('level',3)[@sex='female']"> <!-- do something with the female employees on level 3 --> </xsl:for-each> </xsl:template>
The use of XSLT’s key facility is not mandatory
because you can always write
select="//*[count(ancestors::*)=3]
" when you need
access to level-3 elements; however, performance will suffer if your
stylesheet repeatedly evaluates such an expression. In fact, if your
stylesheet has a well-defined structure, you’d be
much better off explicitly navigating to the desired level; e.g.,
select=
"/employee/employee/employee
“, although this
navigation can become quite
unwieldy.
You want to process nodes in a sequence that is a function of their position in a document or node set.
Use xsl:sort
with
the select set to the position()
or last()
functions. The most trivial application of
this example processes nodes in reverse document order:
<xsl:apply-templates> <xsl:sort select="position()" order="descending" data-type="number"/> </xsl:apply-templates>
or:
<xsl:for-each select="*"> <xsl:sort select="position()" order="descending" data-type="number"/> <!-- ... --> </xsl:for-each>
Another common version of this example traverses a node set as if it were a matrix of a specified number of columns. Here, you process all nodes in the first column, then the second, and then the third:
<xsl:for-each select="*"> <xsl:sort select="(position() - 1) mod 3" /> <!-- ... --> </xsl:for-each>
Or, perhaps more cleanly with:
<xsl:for-each select="*[position() mod 3 = 1]"> <xsl:apply-templates select=". | following-sibling::*[position() < 3]" /> </xsl:for-each>
Sometimes you need to use position()
to separate
the first node in a node set from the remaining nodes. Doing so lets
you perform complex aggregation operations on a document using
recursion. I call this example
recursive-aggregation.
The
abstract form of this example follows:
<xsl:template name="aggregation"> <xsl:param name="node-set"/> <xsl:choose> <xsl:when test="$node-set"> <!--We compute some function of the first element that produces a value that we want to aggregate. The function may depend on the type of the element (i.e. it can be polymorphic)--> <xsl:variable name="first"> <xsl:apply-templates select="$node-set[1]" mode="calc"/> </xsl:variable> <!--We recursivly process the remaining nodes using position() --> <xsl:variable name="rest"> <xsl:call-template name="aggregation"> <xsl:with-param name="node-set" select="$node-set[position()!=1]"/> </xsl:call-template> </xsl:variable> <!-- We perform some aggregation operation. This might not require a call to a template. For example, this might be $first + $rest or $first * $rest or concat($first,$rest) etc. --> <xsl:call-template name="aggregate-func"> <xsl:with-param name="a" select="$first"/> <xsl:with-param name="b" select="$rest"/> </xsl:call-template> </xsl:when> <!-- HereIDENTITY-VALUE
should be replaced with the identity under the aggregate-func. For example, 0 is the identity for addition, 1 is the identity for subtraction, "" is the identity for concatenation, etc. --> <xsl:otherwise>IDENTITY-VALUE
</xsl:otherwise> </xsl:template>
XSLT’s natural tendency is to process nodes in document order. This is equivalent to saying that nodes are processed in order of their position. Thus, the following two XSLT fragments are equivalent (the sort is redundant):
<xsl:for-each select="*"> <xsl:sort select="position()"/> <!-- ... --> </xsl:for-each> <xsl:for-each select="*"> <!-- ... --> </xsl:for-each>
You can format our organization’s chart into a two-column report using a variation of this idea, shown in Examples Example 5-15 and Example 5-16.
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text" /> <xsl:strip-space elements="*"/> <xsl:template match="employee[employee]"> <xsl:value-of select="@name"/> <xsl:text>
</xsl:text> <xsl:call-template name="dup"> <xsl:with-param name="input" select=" '-' "/> <xsl:with-param name="count" select="80"/> </xsl:call-template> <xsl:text>
</xsl:text> <xsl:for-each select="employee[(position() - 1) mod 2 = 0]"> <xsl:value-of select="@name"/> <xsl:call-template name="dup"> <xsl:with-param name="input" select=" ' ' "/> <xsl:with-param name="count" select="40 - string-length(@name)"/> </xsl:call-template> <xsl:value-of select="following-sibling::*[1]/@name"/> <xsl:text>
</xsl:text> </xsl:for-each> <xsl:text>
</xsl:text> <xsl:apply-templates/> </xsl:template> <xsl:template name="dup"> <xsl:param name="input"/> <xsl:param name="count" select="1"/> <xsl:choose> <xsl:when test="not($count) or not($input)"/> <xsl:when test="$count = 1"> <xsl:value-of select="$input"/> </xsl:when> <xsl:otherwise> <xsl:if test="$count mod 2"> <xsl:value-of select="$input"/> </xsl:if> <xsl:call-template name="dup"> <xsl:with-param name="input" select="concat($input,$input)"/> <xsl:with-param name="count" select="floor($count div 2)"/> </xsl:call-template> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
Jil Michel ------------------------------------------------------------ Nancy Pratt Jane Doe Mike Rosenbaum Nancy Pratt ------------------------------------------------------------ Phill McKraken Ima Little Ima Little ------------------------------------------------------------ Betsy Ross Jane Doe ------------------------------------------------------------ Walter H. Potter Wendy B.K. McDonald Wendy B.K. McDonald ------------------------------------------------------------ Craig F. Frye Hardy Hamburg Rich Shaker Mike Rosenbaum ------------------------------------------------------------ Cindy Post-Kellog Oscar A. Winner Cindy Post-Kellog ------------------------------------------------------------ Allen Bran Frank N. Berry Jack Apple Oscar A. Winner ------------------------------------------------------------ Jack Nicklaus Tom Hanks Susan Sarandon Jack Nicklaus ------------------------------------------------------------ R.P. McMurphy Tom Hanks ------------------------------------------------------------ Forrest Gump Andrew Beckett Susan Sarandon ------------------------------------------------------------ Helen Prejean
One example of recursive-aggregation is a stylesheet that computes the total commission paid to salespeople whose commission is a function of their total sales over all products, shown in Example 5-17 and Example 5-18.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:template match="salesBySalesperson"> <xsl:text>Total commission = </xsl:text> <xsl:call-template name="total-commission"> <xsl:with-param name="salespeople" select="*"/> </xsl:call-template> </xsl:template> <!-- By default salespeople get 2% commission and no base salary --> <xsl:template match="salesperson" mode="commission"> <xsl:value-of select="0.02 * sum(product/@totalSales)"/> </xsl:template> <!-- salespeople with seniority > 4 get $10000.00 base + 0.5% commission --> <xsl:template match="salesperson[@seniority > 4]" mode="commission" priority="1"> <xsl:value-of select="10000.00 + 0.05 * sum(product/@totalSales)"/> </xsl:template> <!-- salespeople with seniority > 8 get (seniority * $2000.00) base + 0.8% commission --> <xsl:template match="salesperson[@seniority > 8]" mode="commission" priority="2"> <xsl:value-of select="@seniority * 2000.00 + 0.08 * sum(product/@totalSales)"/> </xsl:template> <xsl:template name="total-commission"> <xsl:param name="salespeople"/> <xsl:choose> <xsl:when test="$salespeople"> <xsl:variable name="first"> <xsl:apply-templates select="$salespeople[1]" mode="commission"/> </xsl:variable> <xsl:variable name="rest"> <xsl:call-template name="total-commission"> <xsl:with-param name="salespeople" select="$salespeople[position()!=1]"/> </xsl:call-template> </xsl:variable> <xsl:value-of select="$first + $rest"/> </xsl:when> <xsl:otherwise>0</xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
When using 2.0, one can combine functions and templates to avoid recursion but still exploit the pattern-matching capabilities of templates for computing the commissions:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ckbk="http://www.oreilly.com/catalog/xsltckbk"> <xsl:output method="text"/> <xsl:template match="salesBySalesperson"> <xsl:text>Total commission = </xsl:text> <xsl:value-of select="ckbk:total-commission(*)"/> </xsl:template> <!-- By default salespeople get 2% commission and no base salary --> <xsl:template match="salesperson" mode="commission" as="xs:double"> <xsl:sequence select="0.02 * sum(product/@totalSales)"/> </xsl:template> <!-- salespeople with seniority > 4 get $10000.00 base + 0.5% commission --> <xsl:template match="salesperson[@seniority > 4]" mode="commission" priority="1" as="xs:double"> <xsl:sequence select="10000.00 + 0.05 * sum(product/@totalSales)"/> </xsl:template> <!-- salespeople with seniority > 8 get (seniority * $2000.00) base + 0.8% commission --> <xsl:template match="salesperson[@seniority > 8]" mode="commission" priority="2" as="xs:double"> <xsl:sequence select="@seniority * 2000.00 + 0.08 * sum(product/@totalSales)"/> </xsl:template> <xsl:function name="ckbk:total-commission" as="xs:double"> <xsl:param name="salespeople" as="node()*"/> <xsl:sequence select="sum(for $s in $salespeople return ckbk:commission($s))"/> </xsl:function> <xsl:function name="ckbk:commission" as="xs:double"> <xsl:param name="salesperson" as="node()"/> <xsl:apply-templates select="$salesperson" mode="commission"/> </xsl:function>
Michael Kay has a nice example of recursive-aggregation on page 535 of XSLT Programmer’s Reference (Wrox Press, 2001). He uses this example to compute the total area of various shapes in which the formula for area varies by the type of shape.
Jeni Tennison also provides examples of recursive-aggregation and alternative ways to perform similar types of processing in XSLT and XPath on the Edge (M&T Books, 2001).
[1] Sauce espagnole is highly concentrated brown stock with tomatoes bound with roux and reduced. The brown stock is made from veal and beef shanks. Sauce espagnole is necessary for making demi-glace, the “mother sauce” for many other brown sauces. When it comes to “reuse,” the chefs have the software developers beat!