Comparing XSLT and XQuery
by J. David Eisenberg
|
Pages: 1, 2, 3, 4
Let us now turn our attention to the XSLT that provides the list items with four pig names per entry. The numbers in the circles refer to the notes that follow the listing.
<xsl:template match="animal" mode="indexList">
<xsl:variable name="start"
select="(position()-1)*$perPage + 1"/>
<xsl:variable name="end">
<xsl:choose>
<xsl:when test="$start + $perPage >
count(/pig-rescue/animal)">
<xsl:value-of select="count(/pig-rescue/animal)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$start + $perPage - 1"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="filename">animals<xsl:value-of
select="position()"/>.html</xsl:variable>
<li>
<xsl:for-each
select="/pig-rescue/animal[position() >= $start and
position() <= $end]">
<xsl:variable name="url"><xsl:value-of
select="$filename"/>#a<xsl:value-of
select="$start+position()-1"/></xsl:variable>
<a href="{$url}">
<xsl:value-of select="name"/>
</a>
<xsl:call-template name="seriesSeparator">
<xsl:with-param name="start" select="$start"/>
<xsl:with-param name="end" select="$end"/>
</xsl:call-template>
</xsl:for-each>
</li>
<xsl:call-template name="makeSubfile">
<xsl:with-param name="start" select="$start"/>
<xsl:with-param name="end" select="$end"/>
<xsl:with-param name="filename" select="$filename"/>
</xsl:call-template>
</xsl:template>
- We can’t just use
position()for this; though the calling template selected every fourth item, the template sees the resulting nodes in that list as being numbered one, two, three, etc. - The last animal to be processed is the starting animal plus the number per page or the total number of animals, whichever is less.
- The pigs’ names have to link to the page where their
full descriptions will be. For the first four pigs, this is
animals1.html, for the next four it isanimals2.html, etc. - Construct the destination URL for each pig.
- Listing the names separated by commas in a grammatically correct manner is tricky business, so we hand that off to a named template.
- Finally, as long as we have figured out which pigs to process, we pass that information to a template that will construct the file we named in step 4 above.
The XQuery equivalent for this is the
local:make-name-list function.
The logic is the same, so the notes will concentrate on the XQuery-specific
aspects.
declare function local:
make-name-list( $animalList as element()* ) as item()+
{
for $pig at $pos in
$animalList[position() mod $perPage = 1]
let
$n := count($animalList),
$filename := fn:concat("animals", $pos, ".html"),
$start := ($pos - 1) * $perPage + 1,
$end := if ($start + $perPage > $n)
then
$n
else
$start + $perPage - 1
return
(
<li>
{
for $animal at $pos in $animalList[position() >= $start and
position() <= $end]
return (
<a href="{$filename}#a{$start + $pos - 1}">
{$animal/name/text()}
</a>,
local:series-separator( $start, $pos, $end )
)
}
</li>,
local:make-subfile( $animalList, $start, $end, $filename)
)
};
-
In an XQuery function, you should always specify the types of function parameters and return values. In this case, we need to specify that the
$animalListparameter will consist ofelement()*, which means zero or more elements. The function returnsitem()+, which means one or more items.If you do not specify a type for the parameter or return value, XQuery assigns
item()*, meaning zero or more items, where anitem()is equivalent to XML Schema’sxs:anyType. This is normally not what you want. -
Here is a
forclause, stepping through every fourth animal in the list. Theat $posmodifier has the same effect as<xsl:value-of select="position()">. In XQuery, you can use theposition()function only inside a predicate of an XPath expression. -
You may do several different assignments within a
letclause by separating them with commas. Notice the assignment to$end, which uses anifexpression. Since this is an expression and not a statement, you must always have both athenandelseso that it always yields a value. -
The
return’s first value is the list item. The<li>puts us into direct constructor mode, so we need braces to re-enter XQuery mode to create the contents. This line is the reason we needed to declare
$animalListaselement()*. You cannot use an anonymous item as a path step; you must have a node or an element.Also, we can’t just say
$animal/name. Unlike<xsl:value-of select="animal/name"/>, which yields a text value,$animal/nameputs a copy of the<name>element, tags and all, into the return value. If we want just the text, we have to explicitly add the extratext()step to the XPath expression.-
Making the page with the pigs’ description is a task that we hand off to another local function. Its output will be the second item in this function’s return value (note the comma on the preceding line), and that value will eventually make its way into the output, so the function will have to return the null string as its value.
Notice that the return expression switches between
direct element constructor mode and XQuery expression evaluation mode
several times.
In XSLT, the difference between commands to the XSLT processor and
elements destined for output is fairly easy to distinguish
due to the leading xsl: prefix. When you first
start writing XQuery, it can be
difficult to see—but always important to remember—which mode
you are in.
Putting the correct separator after a pig’s name boils down to one of four cases:
- last pig in the series: no comma
- next to last pig in a group of two: “ and ”
- next to last pig in a group of three or more: “ , and ”
- other pig in a series: a comma followed by a blank
In XSLT, this is a simple <xsl:choose>, and
we won’t show it here. In XQuery,
it is a simple nested if.
The types in the following declaration are based on XML Schema’s
predefined types, which means you also get all the quirks and
non-extensibility of the XML Schema type list. The function doesn’t
need a FLWOR expression; the result of the nested if is the
function’s return value.
declare function local:
series-separator( $start as xs:integer, $pos as xs:integer,
$end as xs:integer) as xs:string
{
if (($start + $pos < $end) and ($end - $start > 1))
then
", "
else if (($start + $pos = $end) and ($end - $start >= 2))
then
", and "
else if (($start + $pos = $end) and ($end - $start = 1))
then
" and "
else
""
};