Forum Moderators: open
I also want to get some peer review, so I'd appreciate any pointers on incorrect information or inconsistencies. I've had to learn XSLT "blind," and I am now in a position where I need to disseminate the knowledge to my co-workers (Thus, the wiki post).
ABSTRACT
In this post, I deconstruct the trick I used in my wee_test example, and open the kimono just a bit on the inner workings of XPath.
This post will go into detail about how XPath iterates through the nodes of the XML document. It does it a LOT. The performance can be an issue in some XSLT processors, but don't forget that the XML Parser loads the XML document into an object instance model that can be accessed fairly easily.
It is also highly confusing as exactly where a given XPath predicate is at any given time. I'm hoping that this page can help to straighten it out.
For our example, I'll use the wacky trick in the wee_test.xsl file:
<xsl:key name="catz" match="wt:elementwrapper" use="@cat"/>
•
•
•
<xsl:for-each select="//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[ 1 ])]">
the reason for this trick was to generate a node set with only unique categories. The @cat attribute is not a unique ID, so several elements may contain the same ID. We want only one element from each category. We don't care which element it is, as long as the @cat attribute is unique in the returned node set. We will use this to iterate in lots per category.
DETAILS
Now, let's make up a teeny little version of our wee_test.xml file:
<weetest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:wee_test wee_test.xsd" xmlns="urn:wee_test">
<elementwrapper id="3" cat="2">
<data_name>Element ID 3, Category 2</data_name>
</elementwrapper>
<elementwrapper id="1" cat="3">
<data_name>Element ID 1, Category 3</data_name>
</elementwrapper>
<elementwrapper id="4" cat="3">
<data_name>Element ID 4, Category 3</data_name>
</elementwrapper>
<elementwrapper id="2" cat="1">
<data_name>Element ID 2, Category 1</data_name>
</elementwrapper>
</weetest>
Now, let's make up an abstract "node map" that reflects how an XML parser might see the file:
The whole file, in an imaginary node list
• NODE 0 name="weetest"
• NODE 1 name="elementwrapper" @id="3" @cat="2"
- NODE 2 name="data_name"
- "Element ID 3, Category 2"
• NODE 3 name="elementwrapper" @id="1" @cat="3"
- NODE 4 name=data_name
- "Element ID 1, Category 3"
• NODE 5 name="elementwrapper" @id="4" @cat="3"
- NODE 6 name="data_name"
- "Element ID 4, Category 3"
• NODE 7 name="elementwrapper" @id="2" @cat="1"
- NODE 8 name="data_name"
- "Element ID 2, Category 1"
The "NODE X" numbers reflect an internal assignment, and have nothing to do with an ID assigned by the "@id" attribute for the element. The structure and order may also be arbitrary. I don't know if there is a spec as to how parsers operate internally, so that if you reference node_list[3], you will always get the same data_name element, regardless of the parser. I would be surprised if that were the case. This ID can be accessed by calling the XPath function generate-id() [w3schools.com]. There is also no guarantee that the ID returned by generate-id() will be useful as an index. It is simply a single, unique ID that can be used to identify a specific node. This plays a major part in our "trick."
One thing to remember is that generate-id() always returns the same number for the same node, no matter how often it is called, or from what context. This will be critical to the operation of this "trick."
By the way, you can iterate a node list like an array, indexing its contents with a bracketed integer, like so: //wt:weetest/wt:elementwrapper[ 1 ], which should give you the second instance of an elementwrapperin the node list[/i] (not necessarily the file itself). Node-list iterators are 1-based.
Now, let's walk through the XPath location step. We'll start by discussing the XSLT xsl:key [w3schools.com] element. This is sort of a "macro" that we declare in the global scope, and that is called into play later, by XPath:
<xsl:key name="catz" match="wt:elementwrapper" use="@cat"/>
The "name" attribute is the "macro name" that will be invoked later in the stylesheet by the XPath predicate.
The "match" attribute is exactly like the "match" attribute in the xsl:template [w3schools.com] element. It selects an xsl:element by name, and establishes it as the context/scope for the expression in the "use" attribute.
The "use" attribute is an XPath expression that is evaluated when the "match" attribute selects a node. This will be the value returned by the key() [topxml.com] function, later in the stylesheet.
Now, let's have fun turning our brains into pretzels:
Here is the hairy beast we'll be studying:
<xsl:for-each select="//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[ 1 ])]">
This is a standard XSLT xsl:for-each [w3schools.com] element that declares a repeating set of styles/formats/operations on a node-list.
That node-list comes from the "select" attribute:
<xsl:for-each select="//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[ 1 ])]">
An important thing to keep in mind here is precedence. Who needs what from whom before they can do their job?
In this case, the xsl:for-each element needs a "groomed" node list. And that node-list is generated by the "select" attribute's XPath location step. That means that, when you are in the XPath location step, you can completely ignore what XSLT element you are in, as well as –now this is important, where the element is in the stylesheet, or where the stylesheet happens to be in transforming the XML document.
Okay, so we forget about the XSLT, and concentrate only on the XPath:
//wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[ 1 ])]
The red part is the node test, and the blue part is the predicate. The axis is implied. Since there is no axis given, it is assumed to be "self::".
self:://wt:weetest/wt:elementwrapper[generate-id() = generate-id(key('catz',@cat)[ 1 ])]
The node test says that the predicate will start testing nodes generated from only the //wt:weetest/wt:elementwrapper elements (NODE 1, NODE 3, NODE 5 and NODE 7). These establish the "root scope" for the predicate, so we can consider references to attributes and elements within the predicate to stem from this root.
Remember what I said about "what does 'self' mean?". In this case, the predicate will be scanning the entire node list from top to bottom, dismissing any nodes that don't fit the node test pattern. That means that the predicate will "see" the following node map as a starting point:
What the predicate sees
• NODE 1 name="elementwrapper" @id="3" @cat="2"
• NODE 3 name="elementwrapper" @id="1" @cat="3"
• NODE 5 name="elementwrapper" @id="4" @cat="3"
• NODE 7 name="elementwrapper" @id="2" @cat="1"
Okay, we have our starting point, and here is what we are doing with each node:
[generate-id() = generate-id(key('catz',@cat)[ 1 ])]
Now, there are actually four different parts to this expression, each one working with a node set, and generating its own node set or value:
[generate-id() = generate-id(key('catz',@cat)[ 1 ])]
Let's start by looking at the key() function:
key('catz',@cat)
This involves the "catz" key we outlined above. That key delivers a node list of "elementwrapper" nodes to its internal XPath expression. In this case, that list is exactly the same as the one we happen to be iterating (NODE 1, NODE 3, NODE 5 and NODE 7). Let me mention one of the more brain-bending things about this XPath location step:
You know how I said the the node test established a context that the predicate has to use as its scope?
Well forget about that for this function. Think of the key() function as an ambassador from the location step scope out to the global scope.
Internally, the key() function develops its own node set, based upon its "match" attribute, which culls its node set from the global scope. It just so happens that this node set matches the one for the node test:
What the inside of the key() function sees
• NODE 1 name="elementwrapper" @id="3" @cat="2"
• NODE 3 name="elementwrapper" @id="1" @cat="3"
• NODE 5 name="elementwrapper" @id="4" @cat="3"
• NODE 7 name="elementwrapper" @id="2" @cat="1"
Now, the key() function's second parameter, @cat, says "Only select nodes where the XPath expression's value matches the value I'm passing in (the value of the current @cat attribute of the current elementwrapper node)."
You with me so far? Good. That was the easy part. It's gonna be a bit rough. The pilot has turned on the seat belt sign. Strap in and take a slug of something strong.
One thing that you need to remember is that the return of the key() function is a node list from its own internal list. It is not related to the context established by the XPath node test. Basically, the two contexts are decoupled. This trick uses the dichotomy to filter out single instances.
Now, look at the [ 1 ]. What is happening here is that we are saying "Return a node list containing only one node, and that node is the very first one that the key() function picked out." It ignores all the rest. generate-id() requires a single node, and returns a unique key for that node.
Now, we go back to the original context, leaving key() in its own little world.
There is no visible for-each loop, but one is implied. XPath operations are applied to each node in the given node list, so remember that the key() function will be handing its node list to the right-side generate-id() function, which will be ignoring the given context.
However, the left-side generate-id() will be operating in the context/scope provided by the node test.
What you have are two different scopes here, and we are simply selecting the intersecting elements:
[generate-id() = generate-id(key('catz',@cat)[ 1 ])]
This makes no sense! You say, tightening your belt and slugging some absinthe.
It wouldn't, except that we have a mole in the enemy camp:
[generate-id() = generate-id(key('catz',@cat)[ 1 ])]
Remember that the predicate is executed for every node in the main node list generated by the node test. Let me refresh your memory:
What the predicate sees (generated by the location step node test)
• NODE 1 name="elementwrapper" @id="3" @cat="2"
• NODE 3 name="elementwrapper" @id="1" @cat="3"
• NODE 5 name="elementwrapper" @id="4" @cat="3"
• NODE 7 name="elementwrapper" @id="2" @cat="1"
Now, in the case of the right-side generate-id() function, the only relevance is in how many times it is called. The node-set is being provided by the enclosed key() function (remember that this will only return one node). In the case of the left-side generate-id() function, it establishes the context, because we aren't passing in a node set. No parameter means the current node being tested.
That mole (@cat) is how we change the node set for the key function during each iteration.
You see, the @cat is in the context of the current node being iterated in the node set being given the predicate, so it will change each time, as each node in the predicate's node-set has a different @cat value (well, actually no, which is the whole point).
Let's walk through the iterations, and you'll see what is happening:
Iteration 1: NODE 1
First, the context:
- Node 1 is an <elementwrapper/> element, with a unique ID of 3, and a category of 2, which may not be unique. These are given in the @id and @cat attributes, respectively.
- Let's say that a generate-id() function on this will return "1" as the unique ID (Has nothing to do with the @id attribute). In fact, to save time, the NODE n number will be used as the result of our generate-id() function. Remember that generate-id() always returns the same ID for the same element. It doesn't make up a new one, which pretty much tells us that "generate" is a lousy term for this function. XSLT and XPath are full of this kind of stuff. A better name would be "get-id()" or even "return-id()", but I didn't write the spec, so "generate-id()" it is...
- The value of @cat is 2
- The value of generate-id() (the left side one, with no parameters) is 1
Now, let's mosey on over to the right side:
generate-id(key('catz',2)[ 1 ])
We are going to tell key() to give us just the nodes that have a @cat value of 2:
What the key() function returns for 2
• NODE 1 name="elementwrapper" @id="3" @cat="2"
There is only one node in this node-set, as we only have one for category 2. That node is fed to generate-id(), and generate-id() returns a value of 1.
This happens to match the left-side generate-id(), so the predicate passes the test, and this node (NODE 1) is added to the node set being built up by the location step.
Iteration 2: NODE 3
First, the context:
- Node 3 is an <elementwrapper/> element, with an ID of 1, and a category of 3.
- The value of @cat is 3
- The value of generate-id() (the left side one, with no parameters) is 3
Now, let's mosey on over to the right side:
generate-id(key('catz',3)[ 1 ])
We are going to tell key() to give us just the nodes that have a @cat value of 3:
What the key() function returns for 3
• NODE 3 name="elementwrapper" @id="1" @cat="3"
• NODE 5 name="elementwrapper" @id="4" @cat="3"
Now, we get 2 nodes, as two of the nodes have a @cat of 3.
We will only return the first node to the right-side generate-id():
What the key() function returns to generate-id()
• NODE 3 name="elementwrapper" @id="1" @cat="3"
The right-side generate-id() generates 3 (The unique ID of the first node in the @cat=3 set).
This happens to match the left-side generate-id(), so the predicate passes the test, and this node (NODE 3) is added to the node set being built up by the location step.
Iteration 3: NODE 5
First, the context:
- Node 5 is an <elementwrapper/> element, with an ID of 4, and a category of 3.
- The value of @cat is 3
- The value of generate-id() (the left side one, with no parameters) is 5
Now, let's mosey on over to the right side:
generate-id(key('catz',3)[ 1 ])
We are going to tell key() to give us just the nodes that have a @cat value of 3:
What the key() function returns for 3
• NODE 3 name="elementwrapper" @id="1" @cat="3"
• NODE 5 name="elementwrapper" @id="4" @cat="3"
Now, we get 2 nodes, as two of the nodes have a @cat of 3.
We will only return the first node to the right-side generate-id():
What the key() function returns to generate-id()
• NODE 3 name="elementwrapper" @id="1" @cat="3"
The right-side generate-id() generates 3 (The unique ID of the first node in the @cat=3 set).
This happens to NOT match the left-side generate-id(), so the predicate fails the test, and this node (NODE 5) is NOT added to the node set being built up by the location step.
Iteration 4: NODE 7
First, the context:
- Node 7 is an <elementwrapper/> element, with an ID of 2, and a category of 1.
- The value of @cat is 1
- The value of generate-id() (the left side one, with no parameters) is 7
Now, let's mosey on over to the right side:
generate-id(key('catz',1)[ 1 ])
We are going to tell key() to give us just the nodes that have a @cat value of 1:
What the key() function returns to generate-id()
• NODE 7 name="elementwrapper" @id="2" @cat="1"
The right-side generate-id() generates 7.
This happens to match the left-side generate-id(), so the predicate passes the test, and this node (NODE 7) is added to the node set being built up by the location step.
Now, we have a node set with three nodes, each of which represents a category. This node set is given to the xsl:for-each loop to iterate. XSLT will sort these nodes and use them to further key a transformation of elements within the categories represented by these nodes, but that's peanuts compared to what we just went through.
The captain has turned off the seat belt sign. Be careful when removing items from the overhead bins, as they may have shifted during flight.
<xsl:for-each select="//wt:weetest/wt:elementwrapper" >
<xsl:case generate-id(this) = generate-id(key('catz',this.@cat)[ 1 ]))>
You use generate-id as part of the node equality test, does this have same meaning?
<xsl:case this = key('catz',this.@cat)[ 1 ]>
<xsl:for-each select="//wt:weetest/wt:elementwrapper" >
<xsl:if generate-id(select) = generate-id(key('catz',select.@cat)[ 1 ]))>
<xsl:if select = key('catz',select.@cat)[ 1 ]>
I just tested it out (<oXygen/> is great for this stuff).
It works. The one issue is that I can't use <xsl:sort> on the filtered categories (<xsl:sort> needs to be directly under <xsl:for-each>). I suspect there is probably a solution for that.
Here's the revised code:
<?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: [bernzilla.com...]
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])]"> --><!-- ################################################################################### -->
<!-- These two lines represent a test of an alternative solution. -->
<xsl:for-each select="//wt:weetest/wt:elementwrapper">
<xsl:if test="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:if>
</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 lines in question are surrounded by <!-- ################################################################################### -->
Its interesting that the if condition corresponds identically to the [] part of the select.
Is there a way to refer explictly to the for-each result so that for example "generate-id()" could be written "generate-id(for-each-result)", this would in my option help readabilty.
Speaking of generate-id(), I take it that the condition
"generate-id(A) = generate-id(B)" is different from the condition "A=B", can you explain.
Its interesting that the if condition corresponds identically to the [] part of the select.
It doesn't really. What's happening is that the for-each is establishing a scope of
//wt:weetest/wt:elementwrapper, so that all XPath inside it evaluate from that scope. If you have just the predicate (the bracketed part), without any axis or node-test, then the predicate assumes that it is being evaluated on the current node, in the current scope. Is there a way to refer explictly to the for-each result so that for example "generate-id()" could be written "generate-id(for-each-result)", this would in my option help readabilty.
That's a good question. I'm not sure. I do know that the people who wrote this wanted to keep the coupling between XSLT and XPath fairly loose. In fact, the W3Schools site lists the key() [w3schools.com] function as an XSLT function [w3schools.com], not an XPath function, which sort of explains why it acts like an "embassy" to XSLT inside of XPATH.
Speaking of generate-id(), I take it that the condition
"generate-id(A) = generate-id(B)" is different from the condition "A=B", can you explain.
I assume that this is because, when you reference a node directly in XPath, it evaluates to the value of the node, not the node itself. The makers of the system probably want to keep node references as opaque as possible.
However, take what I say with a grain of salt. A month ago, I had trouble spelling "XSLT." I'm leaqrning as I go along.
<xsl:if test="generate-id() = generate-id(key('catz',@cat)[ 1 ])">
could be written
<xsl:if test="generate-id(.) = generate-id(key('catz',./@cat)[ 1 ])">
or
<xsl:if test="generate-id(current()) = generate-id(key('catz',current()/@cat)[ 1 ])">
The first alternative should also be Ok within the [] part of the select.
I think it adds to the readability.
What's happening is that the for-each is establishing a scope of //wt:weetest/wt:elementwrapper, so that all XPath inside it evaluate from that scope.
Agree
If you have just the predicate (the bracketed part), without any axis or node-test, then the predicate assumes that it is being evaluated on the current node, in the current scope.
So what is different in environment of positions p1 and p2
<xsl:for-each node-test[p1] >
<xsl:for-each node-test >
<xsl:if p2>
in posn p1 the current node and context node differ
in posn p2 the current node = context node = current node of p1
So provided there is no direct or implied reference to current() that same conditional code should be Ok in either position?
So what is different in environment of positions p1 and p2<xsl:for-each node-test[p1] >
<xsl:for-each node-test >
<xsl:if p2>in posn p1 the current node and context node differ
in posn p2 the current node = context node = current node of p1So provided there is no direct or implied reference to current() that same conditional code should be Ok in either position?
I believe so.
In XPath "Where the heck am I?" is often a difficult question.