Forum Moderators: open
This will be a long post, but will contain some excellent examples of XSLT in action.
The goal is to take a generic, unsorted XML dump, and present it as a valid, sorted and formatted HTML document.
This is done using the libxslt module, which is the native one for PHP. That means that you can test this in PHP.
This is a very useful demonstration of end-to-end XML, XSLT and PHP implementation of such.
Now, there will be four files involved:
1) The XML file (wee_test.xml). This contains the unsorted data to be transformed.
2) The schema file (wee_test.xsd). This is the equivalent of a DTD (only better). It is used to validate the XML file. It isn't actually used in the transform, but you should always have a schema or DTD for your XML files.
3) The XSLT stylesheet for this transform (wee_test.xsl). It is designed to transfer files defined by the schema into sorted HTML.
4) A PHP 5 file that will operate the transform. It requires PHP 5 with XSLTProcessor enabled (--with-xsl needs to be specified in the parameters).
Here are the files (In subsequent posts):
[edited by: cmarshall at 8:56 pm (utc) on April 30, 2007]
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
This is a completely fictional and nonsensical XML file that is going to be used to demonstrate
how to use XSLT to transform XML into HTML (in this example).Pretend that this XML is the result of a SOAP transfer from a database somewhere. It is a list
of SQL table rows. Each row is represented by an elementwrapper element, and that contains up to
three child elements: A name, a URI and a duration.The elementwrapper element has two attributes: a unique ID and a "category." These are required
attributes.
-->
<weetest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:wee_test wee_test.xsd"
xmlns="urn:wee_test">
<elementwrapper id="9" cat="2">
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="1" cat="1">
<data_duration>01:05:00</data_duration>
<data_name>Test (cat 1)</data_name>

</elementwrapper>
<elementwrapper id="8" cat="1">
<data_duration>10:01:00</data_duration>
<data_name>Test (cat 1)</data_name>
</elementwrapper>
<elementwrapper id="10" cat="10">
<data_duration>20:01:00</data_duration>
<data_name>Test (cat 10)</data_name>
</elementwrapper>
<elementwrapper id="4" cat="2">
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="3" cat="2">
<data_duration>21:35:00</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="11" cat="42">
<data_name>Test (cat 42)</data_name>

</elementwrapper>
<elementwrapper id="7" cat="2">
<data_duration>10:00:01</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="0" cat="4">
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="5" cat="4">
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="6" cat="4">
<data_duration>11:01:20</data_duration>
<data_name>Test (cat 4)</data_name>
</elementwrapper>
<elementwrapper id="2" cat="1">
<data_name>Test (cat 1)</data_name>
<data_uri/>
</elementwrapper>
</weetest>
<?xml version="1.0"?>
<!--
This is the schema for our fictional database transfer.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:wee_test"
elementFormDefault="qualified">
<xs:element name="weetest">
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="elementwrapper">
<xs:complexType>
<xs:sequence>
<xs:element name="data_duration" minOccurs="0" type="xs:time"/>
<xs:element name="data_name" type="xs:string"/>
<xs:element minOccurs="0" name="data_uri" type="xs:anyURI"/>
</xs:sequence>
<xs:attribute name="cat" type="xs:integer" />
<xs:attribute name="id" type="xs:integer" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="urn:wee_test" exclude-result-prefixes="wt">
<xsl:output indent="yes" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3c//DTD HTML 4.01//EN"/><!--
This is a very, very basic demonstration of XSLT in practice. It simply transforms a basic XML list into
an HTML 4.01 Strict page.The original data (given in wee_test.xml) is a list of abstract "items" that each have a category (Required),
a URI (Optional), a time duration (Optional) and a name (Required).The list can be given in any order. The schema is given in wee_test.xsd.
This stylesheet first extracts a list of categories and generates a list of these categories in a sorted
fashion. It then uses this list to extract the items in each category, so an ordered list is presented
in the resultant HTML 4.01 Strict page.This is a completely fictional and useless implementation, so don't expect it to make sense. I wanted it
to demonstrate some key aspects of XSLT in as little space as possible.
--><!-- This is a key that is defined to be used in the hacky XPath select (described in detail later). -->
<xsl:key name="catz" match="wt:elementwrapper" use="@cat"/><!-- This is the basic "default" template. It matches all elements at the top level of the page. -->
<xsl:template match="/">
<!-- We simply create a very, very simple HTML header here, with nothing but a title. -->
<html>
<head>
<title>XML/XSLT Wee Test Demo</title>
</head>
<body>
<!--
Now, this is a pretty whacky XPath hack that shows both the power and the inscrutability of XSLT.I got the basic idea from this post by Bernie Zimmermann [URI REDACTED]
The original article has a nasty typo that is corrected in the second comment by Jesse Weinstein.
let's look at the XPath expression in the select here:
//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[1])]The axis/node-test, which is this part:
//wt:weetest/wt:elementwrapper
says that we will iterate through the second-layer-down elements. If you look at the schema (wee_test.xsd),
you will see that elementwrapper defines each of the basic "rows" of the data. The "wt:" is a namespace
that we assign to the elements to make sure that they remain unique. XML namespaces are a HUGE pain, but
we really need to use them. The "xsl:" namespace is the standard namespace for XSL[t].The predicate is where the whackiness comes in:
[generate-id() = generate-id(key('catz',@cat)[1])]What happens here, is that we are using the key we defined at the beginning of the stylesheet to create a
node-list of nodes for each of the "cat" attributes.XPath is a whacky language to get your mind around. It is a "language within a language." You are inside a
for-each loop, but the predicate is in its own world, and that world doesn't really care where the XSLT
for-each loop happens to be, because it's being executed before the loop begins.
This is basically a function generating a list that is subsequently iterated by the for-each loop.The result of the select attribute is a node-set. That is what xsl:for-each needs.
The node-test constrains the context for the predicate to test. In this case, the predicate will only be
applied to every single //wt:weetest/wt:elementwrapper in the XML file. It's context will be set to that
element for each test, so you don't need to specify //wt:weetest/wt:elementwrapper. That's where the
predicate begins its test.Now, here is what happens. Strap yourself in and take a big slug of absinthe. You'll need it:
Preparatory:Remember that the XPath predicate cycles through the entire XML file. The axis/node-test simply
tells it where to start its context for each test. Every node that fits the given context is tested.Not only that, each part of the predicate also cycles through the entire document. This means
that key('catz',@cat) generates a node-set by looking for every single //wt:weetest/wt:elementwrapper
with a cat that equals //wt:weetest/wt:elementwrapper@cat. This means that it cycles through each
//wt:weetest/wt:elementwrapper, extracts the @cat from that, and then filters out all the other
nodes with the same cat value and adds them to the node-set.Yes, it cycles through that puppy a LOT. XSLT can have...performance issues.
1)We first use an XPath function, called "generate-id()" (http://www.w3schools.com/xsl/func_generateid.asp)
to generate a unique ID for the *first* node that contains the value of the cat attribute:
generate-id(key('catz',@cat)[1])
This is where we get the first node of a given cat:
key('catz',@cat)[1] <- That [1] selects only the first node of the resulting node-set.
If there are fifty nodes with a cat attribute of "1," only the first will be selected for inclusion in the
node-set given to generate-id(). generate-id() creates an ID that is unique for this first node. That is the
right-hand portion of the test.2)Now, we use the default variety of the generate-id() function to generate the ID of the current node. Remember
that XPath keeps cycling through the ENTIRE XML file each time, so every node gets a unique ID generated. This
ID is then compared with the entire list of first instances of each cat. If they match, then that node is added
to the main node-set to be returned to the xsl:for-each loop. The result is that you will iterate on only the
first element of each cat. The inner loop will then use this as a key to extract all the various nodes that have
a cat equal to the one being iterated.If this seems incredibly awkward, it is; but this is how XSLT works. This allows us to have a segmentation and sorting
of the data in the XML. Each category (as specified by the cat attribute in the elementwrapper element) is segregated,
and the elements in that category are sorted by ID. The categories themselves are sorted by category ID.Now, why didn't we just specify a predicate of [key('catz',@cat)[1]]?
I have a prepared object lesson below. Uncomment the line I have commented out, and comment out the one above that to see
what hapens when we don't do the generate-id().Because that would have resulted in many instances of the sorted categories. Remember that the nodes are ALL searched, EVERY
time. The xsl:for-each loop would go through each node, extract its cat, then it would get a list of the first node for that
cat, then it would generate a list of all the nodes within that category. It would be useless. The way we specify means that
only one node is selected to represent each category. We could care less about what the node contains. We only want its cat
attribute and the fact that it will only happen once. We will use the cat attribute to generate a list of nodes with that
category.And you thought those one-line C programs were hairy?
-->
<xsl:for-each select="//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[1])]">
<!-- If you want to see this NOT work, uncomment this, and comment out the above line. -->
<!-- <xsl:for-each select="//wt:weetest/wt:elementwrapper[key('catz',@cat)[1]]"> --><!-- Here, we sort the categories numerically. We will display the nodes within each category. -->
<xsl:sort data-type="number" select="number(@cat)"/>
<!-- Display all the nodes within a category. We pass in the category ID. -->
<xsl:call-template name="render_cat">
<!-- Tell the template to show us the elements within this category. -->
<xsl:with-param name="catg" select="@cat"/>
</xsl:call-template>
</xsl:for-each>
</body>
</html>
</xsl:template><!-- This template renders all the elements within a given category. It sorts these nodes by their ID. -->
<xsl:template name="render_cat">
<!-- This is the ID of the category to render. -->
<xsl:param name="catg"/>
<!-- We give it a header. -->
<h1>Category <xsl:value-of select="$catg"/></h1>
<!-- Cycle through only the elements with a cat ID that equals that passed in. -->
<xsl:for-each select="//wt:weetest/wt:elementwrapper[@cat=$catg]">
<!-- This is where we sort the elements by their ID. -->
<xsl:sort data-type="number" select="number(@id)"/>
<!--
The xsl:for-each loop establishes a context of each of the elementwrapper
elements with their cat equal to the given category ID.
Now, we render each of its child nodes.
-->
<xsl:call-template name="render_one"/>
</xsl:for-each>
</xsl:template><!--
This actually writes each child node out to the HTML result. The containing element has been
isolated and sorted into place by the outer loops, so all we have left is to render the child
nodes of this element.
-->
<xsl:template name="render_one">
<!-- Each of the elements is wrapped in a <div> -->
<xsl:element name="div">
<xsl:attribute name="class">
<!-- This is how we can specify an alternating pattern of lines. We specify an alternating class. -->
<xsl:text>alt_</xsl:text>
<!-- If the number is divisible by 2, it will be given a class of "alt_0," if not, it is given a class of "alt_1." -->
<xsl:number value="position() mod 2" format="1"/>
<!-- Give the <div> a class that corresponds to the category ID. -->
<xsl:text> cat_</xsl:text>
<xsl:value-of select="@cat"/>
</xsl:attribute>
<!-- Since each of the XML elementwrapper nodes has a unique ID, we can give it one here. -->
<xsl:attribute name="id">
<xsl:text>id_</xsl:text>
<xsl:value-of select="@id"/>
</xsl:attribute>
<!-- Now that we have all the attributes for this element set up, we get to work on the contents. --><!-- First up, if there is a duration, it gets displayed in a <div> element here. -->
<xsl:call-template name="display_duration">
<xsl:with-param name="duration" select="wt:data_duration"/>
</xsl:call-template><!-- The name is displayed in another <div>. If there is a URI supplied, then the name is wrapped in an anchor tag. -->
<div class="display">
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:element>
</xsl:template><!-- This template looks for a duration element. If one is present, it creates a <div>, and translates the duration to plain English. -->
<xsl:template name="display_duration">
<xsl:param name="duration"/>
<xsl:if test="$duration!= ''">
<xsl:variable name="hours" select="substring-before($duration, ':')"/>
<xsl:variable name="minutes" select="substring-before(substring-after($duration, ':'), ':')"/>
<xsl:element name="div">
<xsl:attribute name="class">duration_display</xsl:attribute>
<xsl:text>The duration is </xsl:text>
<xsl:if test="floor($hours)>0">
<xsl:value-of select="floor($hours)"/>
<xsl:choose>
<xsl:when test="floor($hours)>1">
<xsl:text> hours</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> hour</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:if test="floor($minutes)>0">
<xsl:if test="floor($hours)>0">
<xsl:text> and </xsl:text>
</xsl:if>
<xsl:value-of select="floor($minutes)"/>
<xsl:choose>
<xsl:when test="floor($minutes)>1">
<xsl:text> minutes</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> minute</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:text> long.</xsl:text>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
<?php
/*
This shows how to implement XSLT, using PHP 5's built-in XSLT
processor. You really don't want to use the older version of this,
as the performance was less than optimal.This needs to be in the same directory as the XML file and the
XSLT file.
*/// First, read the file contents into a string.
$xml_file = file_get_contents ( "wee_test.xml" );
$xsl_file = file_get_contents ( "wee_test.xsl" );// Next, create a couple of DOM documents to parse the XML.
$xml_file_dom = new DOMDocument('1.0', 'iso-8859-1');
$xsl_file_dom = new DOMDocument('1.0', 'iso-8859-1');// Next, load up the two DOM documents.
$xml_file_dom->loadXML ( $xml_file );
$xsl_file_dom->loadXML ( $xsl_file );// Create a new XSLT processor.
$xsl = new XSLTProcessor();// Give it our stylesheet.
$xsl->importStyleSheet( $xsl_file_dom );// Now, transform a target XML document, and output the result.
echo $xsl->transformToXML ( $xml_file_dom );
?>
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="urn:wee_test" exclude-result-prefixes="wt">
<xsl:output indent="no" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3C//DTD HTML 4.01//EN"/><!--
This is a very, very basic demonstration of XSLT in practice. It simply transforms a basic XML list into
an HTML 4.01 Strict page.The original data (given in wee_test.xml) is a list of abstract "items" that each have a category (Required),
a URI (Optional), a time duration (Optional) and a name (Required).The list can be given in any order. The schema is given in wee_test.xsd.
This stylesheet first extracts a list of categories and generates a list of these categories in a sorted
fashion. It then uses this list to extract the items in each category, so an ordered list is presented
in the resultant HTML 4.01 Strict page.This is a completely fictional and useless implementation, so don't expect it to make sense. I wanted it
to demonstrate some key aspects of XSLT in as little space as possible.
--><!-- This is a key that is defined to be used in the hacky XPath select (described in detail later). -->
<xsl:key name="catz" match="wt:elementwrapper" use="@cat"/><!-- This is the basic "default" template. It matches all elements at the top level of the page. -->
<xsl:template match="/">
<!-- We simply create a very, very simple HTML header here, with nothing but a title. -->
<html>
<head>
<title>XML/XSLT Wee Test Demo</title>
</head>
<body>
<!--
Now, this is a pretty whacky XPath hack that shows both the power and the inscrutability of XSLT.I got the basic idea from this post by Bernie Zimmermann: [URI REDACTED]
The original article has a nasty typo that is corrected in the second comment by Jesse Weinstein.
let's look at the XPath expression in the select here:
//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[1])]The axis/node-test, which is this part:
//wt:weetest/wt:elementwrapper
says that we will iterate through the second-layer-down elements. If you look at the schema (wee_test.xsd),
you will see that elementwrapper defines each of the basic "rows" of the data. The "wt:" is a namespace
that we assign to the elements to make sure that they remain unique. XML namespaces are a HUGE pain, but
we really need to use them. The "xsl:" namespace is the standard namespace for XSL[t].The predicate is where the whackiness comes in:
[generate-id() = generate-id(key('catz',@cat)[1])]What happens here, is that we are using the key we defined at the beginning of the stylesheet to create a
node-list of nodes for each of the "cat" attributes.XPath is a whacky language to get your mind around. It is a "language within a language." You are inside a
for-each loop, but the predicate is in its own world, and that world doesn't really care where the XSLT
for-each loop happens to be, because it's being executed before the loop begins.
This is basically a function generating a list that is subsequently iterated by the for-each loop.The result of the select attribute is a node-set. That is what xsl:for-each needs.
The node-test constrains the context for the predicate to test. In this case, the predicate will only be
applied to every single //wt:weetest/wt:elementwrapper in the XML file. It's context will be set to that
element for each test, so you don't need to specify //wt:weetest/wt:elementwrapper. That's where the
predicate begins its test.Now, here is what happens. Strap yourself in and take a big slug of absinthe. You'll need it:
Preparatory:Remember that the XPath predicate cycles through the entire XML file. The axis/node-test simply
tells it where to start its context for each test. Every node that fits the given context is tested.Not only that, each part of the predicate also cycles through the entire document. This means
that key('catz',@cat) generates a node-set by looking for every single //wt:weetest/wt:elementwrapper
with a cat that equals //wt:weetest/wt:elementwrapper@cat. This means that it cycles through each
//wt:weetest/wt:elementwrapper, extracts the @cat from that, and then filters out all the other
nodes with the same cat value and adds them to the node-set.Yes, it cycles through that puppy a LOT. XSLT can have...performance issues.
1)We first use an XPath function, called "generate-id()" (http://www.w3schools.com/xsl/func_generateid.asp)
to generate a unique ID for the *first* node that contains the value of the cat attribute:
generate-id(key('catz',@cat)[1])
This is where we get the first node of a given cat:
key('catz',@cat)[1] <- That [1] selects only the first node of the resulting node-set.
If there are fifty nodes with a cat attribute of "1," only the first will be selected for inclusion in the
node-set given to generate-id(). generate-id() creates an ID that is unique for this first node. That is the
right-hand portion of the test.2)Now, we use the default variety of the generate-id() function to generate the ID of the current node. Remember
that XPath keeps cycling through the ENTIRE XML file each time, so every node gets a unique ID generated. This
ID is then compared with the entire list of first instances of each cat. If they match, then that node is added
to the main node-set to be returned to the xsl:for-each loop. The result is that you will iterate on only the
first element of each cat. The inner loop will then use this as a key to extract all the various nodes that have
a cat equal to the one being iterated.If this seems incredibly awkward, it is; but this is how XSLT works. This allows us to have a segmentation and sorting
of the data in the XML. Each category (as specified by the cat attribute in the elementwrapper element) is segregated,
and the elements in that category are sorted by ID. The categories themselves are sorted by category ID.Now, why didn't we just specify a predicate of [key('catz',@cat)[1]]?
I have a prepared object lesson below. Uncomment the line I have commented out, and comment out the one above that to see
what hapens when we don't do the generate-id().Because that would have resulted in many instances of the sorted categories. Remember that the nodes are ALL searched, EVERY
time. The xsl:for-each loop would go through each node, extract its cat, then it would get a list of the first node for that
cat, then it would generate a list of all the nodes within that category. It would be useless. The way we specify means that
only one node is selected to represent each category. We could care less about what the node contains. We only want its cat
attribute and the fact that it will only happen once. We will use the cat attribute to generate a list of nodes with that
category.And you thought those one-line C programs were hairy?
-->
<xsl:for-each select="//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[1])]">
<!-- If you want to see this NOT work, uncomment this, and comment out the above line. -->
<!-- <xsl:for-each select="//wt:weetest/wt:elementwrapper[key('catz',@cat)[1]]"> --><!-- Here, we sort the categories numerically. We will display the nodes within each category. -->
<xsl:sort data-type="number" select="number(@cat)"/>
<!-- Display all the nodes within a category. We pass in the category ID. -->
<xsl:call-template name="render_cat">
<!-- Tell the template to show us the elements within this category. -->
<xsl:with-param name="catg" select="@cat"/>
</xsl:call-template>
</xsl:for-each>
</body>
</html>
</xsl:template><!-- This template renders all the elements within a given category. It sorts these nodes by their ID. -->
<xsl:template name="render_cat">
<!-- This is the ID of the category to render. -->
<xsl:param name="catg"/>
<!-- We give it a header. -->
<h1>Category <xsl:value-of select="$catg"/></h1>
<xsl:element name="dl">
<xsl:attribute name="class">cat_list cat_<xsl:value-of select="$catg"/></xsl:attribute>
<!-- Cycle through only the elements with a cat ID that equals that passed in. -->
<xsl:for-each select="//wt:weetest/wt:elementwrapper[@cat=$catg]">
<!-- This is where we sort the elements by their ID. -->
<xsl:sort data-type="number" select="number(@id)"/>
<!--
The xsl:for-each loop establishes a context of each of the elementwrapper
elements with their cat equal to the given category ID.
Now, we render each of its child nodes.
-->
<xsl:call-template name="render_one"/>
</xsl:for-each>
</xsl:element>
</xsl:template><!--
This actually writes each child node out to the HTML result. The containing element has been
isolated and sorted into place by the outer loops, so all we have left is to render the child
nodes of this element.
-->
<xsl:template name="render_one">
<!-- Each of the elements is wrapped in a <div> -->
<xsl:element name="dt">
<xsl:attribute name="class">
<!-- This is how we can specify an alternating pattern of lines. We specify an alternating class. -->
<xsl:text>alt_</xsl:text>
<!-- If the number is divisible by 2, it will be given a class of "alt_0," if not, it is given a class of "alt_1." -->
<xsl:number value="position() mod 2" format="1"/>
<!-- Give the <div> a class that corresponds to the category ID. -->
<xsl:text> cat_</xsl:text>
<xsl:value-of select="@cat"/>
</xsl:attribute><!-- Since each of the XML elementwrapper nodes has a unique ID, we can give it one here. -->
<xsl:attribute name="id">
<xsl:text>id_</xsl:text>
<xsl:value-of select="@id"/>
</xsl:attribute><!-- Now that we have all the attributes for this element set up, we get to work on the contents. -->
<!-- If there is a URI supplied, then the name is wrapped in an anchor tag. -->
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</xsl:element><!-- If there is a duration, it gets displayed in a <dd> element here. -->
<xsl:call-template name="display_duration">
<xsl:with-param name="duration" select="wt:data_duration"/>
</xsl:call-template>
</xsl:template><!-- This template looks for a duration element. If one is present, it creates a <dd>, and translates the duration to plain English. -->
<xsl:template name="display_duration">
<xsl:param name="duration"/>
<xsl:if test="$duration">
<!-- We use this rather primitive way of parsing the time, because we can only use XSLT 1.0 functions. -->
<xsl:variable name="hours" select="substring-before($duration, ':')"/>
<xsl:variable name="minutes" select="substring-before(substring-after($duration, ':'), ':')"/><!-- The duration is displayed in a <dd> element. -->
<xsl:element name="dd">
<xsl:attribute name="class">duration_display</xsl:attribute>
<xsl:element name="a">
<!-- The duration is displayed in a JavaScript anchor, simply to show that it can be done. -->
<xsl:attribute name="href">
<xsl:text>javascript:alert('</xsl:text>
<xsl:text>The duration is </xsl:text>
<!-- If we have an hour to display... -->
<xsl:if test="floor($hours)>0">
<xsl:value-of select="floor($hours)"/>
<!-- See if we need a plural or a singular. -->
<xsl:choose>
<xsl:when test="floor($hours)>1">
<xsl:text> hours</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> hour</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if><!-- If we have a minute to display... -->
<xsl:if test="floor($minutes)>0">
<!-- If we also displayed an hour, then we need to add an "and." -->
<xsl:if test="floor($hours)>0">
<xsl:text> and </xsl:text>
</xsl:if>
<xsl:value-of select="floor($minutes)"/>
<!-- See if we need a plural or a singular. -->
<xsl:choose>
<xsl:when test="floor($minutes)>1">
<xsl:text> minutes</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> minute</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:text> long.')</xsl:text>
</xsl:attribute>
<!-- This is the string that is displayed. -->
<xsl:text>Duration</xsl:text>
</xsl:element>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The devil in the details may be performance. I have no idea how this would perform with a massive file.
The implementation I use in my own work is not this. It is the previous presorted method. Since the DB can deliver the XML already sorted (and a lot more precisely, at that), there's no need for me to use this trick.
I hope that I can help people to learn XSLT. I have come to believe in the tech, but it is being so badly handled, as far as PR, evangelism and education, that it has a very real chance of croaking.