The solution involves recursively processing the elements in
$nodes
and invoking the generic function,
$func
, on each element. You allow the possibility
that the function specified by $func
is
parameterized. This parameter can be specified by
$func-param
. You further state that the default
value of the $func-param
is obtained from an
attribute, @param1
, in the generic functions tag.
This stipulation allows the default to be a function of the specified
generic:
<xsl:template name="generic:map"> <xsl:param name="nodes" select="/.."/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="result" select="/.."/> <xsl:choose> <xsl:when test="$nodes"> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$nodes[1]"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="$nodes[position( ) > 1]"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i +1"/> <xsl:with-param name="result" select="$result | exslt:node-set($temp)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="$result" mode="generic:map"/> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="/ | node( ) | @*" mode="generic:map"> <node> <xsl:copy-of select="."/> </node> </xsl:template>
You can see the effect of this process by considering the
incr
generic function:
<generic:func name="incr" param1="1"/> <xsl:template match="generic:func[@name='incr']"> <xsl:param name="x"/> <xsl:param name="param1" select="@param1"/> <xsl:value-of select="$x + $param1"/> </xsl:template>
The incr
generic function’s
parameter specifies the amount by which to increment and is defaulted
to one. Here you create a stylesheet that maps
incr
across a node set of numbers, first using the
default parameter and then by setting it to 10
.
For good measure, you also extend the set of generics to include a
reciprocal
function and map the numbers using that
function:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--Add a generic element function for computing reciprocal --> <generic:func name="reciprocal"/> <xsl:template match="generic:func[@name='reciprocal']"> <xsl:param name="x"/> <xsl:value-of select="1 div $x"/> </xsl:template> <!--Test map functionality --> <xsl:template match="numbers"> <results> <incr> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'incr' "/> </xsl:call-template> </incr> <incr10> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'incr' "/> <xsl:with-param name="func-param1" select="10"/> </xsl:call-template> </incr10> <recip> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'reciprocal' "/> </xsl:call-template> </recip> </results> </xsl:template> </xsl:stylesheet>
This results in the following output:
<incr> <node>11</node> <node>4.5</node> <node>5.44</node> <node>78.7777</node> <node>-7</node> <node>2</node> <node>445</node> <node>2.1234</node> <node>8.77</node> <node>4.1415927</node> </incr> <incr10> <node>20</node> <node>13.5</node> <node>14.440000000000001</node> <node>87.7777</node> <node>2</node> <node>11</node> <node>454</node> <node>11.1234</node> <node>17.77</node> <node>13.1415927</node> </incr10> <recip> <node>0.1</node> <node>0.2857142857142857</node> <node>0.2252252252252252</node> <node>0.012857155714298572</node> <node>-0.125</node> <node>1</node> <node>0.0022522522522522522</node> <node>0.8901548869503294</node> <node>0.1287001287001287</node> <node>0.31830988148145367</node> </recip> </results>
Map can extract a subset of a node set that meets specified criteria.
Here you use a generic function that is a predicate. You achieve the
desired effect by structuring your predicates so that they return
their input when the predicate is true
and nothing
when the predicate is false
. For example:
<generic:func name="less-than"/> <xsl:template match="generic:func[@name='less-than']"> <xsl:param name="x"/> <!-- limit --> <xsl:param name="param1"/> <xsl:if test="$x < $param1"><xsl:value-of select="$x"/></xsl:if> </xsl:template>
You then capture the nodes of the result with a simple filtering template:
xsl:template match="/ | node( ) | @*" mode="generic:map"> <xsl:if test="string(.)"> <node> <xsl:copy-of select="."/> </node> </xsl:if> </xsl:template>
Here is a sample of the technique in action:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!--Test map functionality --> <xsl:template match="numbers"> <results> <less-than-5> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'less-than' "/> <xsl:with-param name="func-param1" select="5"/> </xsl:call-template> </less-than-5> <greater-than-5> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'greater-than' "/> <xsl:with-param name="func-param1" select="5"/> </xsl:call-template> </greater-than-5> </results> </xsl:template> <xsl:template match="/ | node( ) | @*" mode="generic:map"> <xsl:if test="string(.)"> <node> <xsl:copy-of select="."/> </node> </xsl:if> </xsl:template>
A test of this stylesheet produces the following output:
<results> <less-than-5> <node>3.5</node> <node>4.44</node> <node>-8</node> <node>1</node> <node>1.1234</node> <node>3.1415927</node> </less-than-5> <greater-than-5> <node>10</node> <node>77.7777</node> <node>444</node> <node>7.77</node> </greater-than-5> </results>
Mapping is not limited to numerical processing. Consider the
following stylesheet that finds the length of all
<para>
elements in a DocBook document:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--Add a generic element function for computing reciprocal --> <generic:func name="length"/> <xsl:template match="generic:func[@name='length']"> <xsl:param name="x"/> <xsl:value-of select="string-length($x)"/> </xsl:template> <!--Test map functionality --> <xsl:template match="/"> <para-lengths> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="//para"/> <xsl:with-param name="func" select=" 'length' "/> </xsl:call-template> </para-lengths> </xsl:template> <xsl:template match="/ | node( ) | @*" mode="generic:map"> <length> <xsl:copy-of select="."/> </length> </xsl:template> </xsl:stylesheet>
Or this example, which creates a document summary by extracting the first three sentences of each paragraph:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--A generic function for extracting sentences --> <generic:func name="extract-sentences" param1="1"/> <xsl:template match="generic:func[@name='extract- sentences']" name="generic:extract-sentences"> <xsl:param name="x"/> <xsl:param name="param1" select="@param1"/> <xsl:choose> <xsl:when test="$param1 >= 1 and contains($x,'.')"> <xsl:value-of select="substring-before($x,'.')"/> <xsl:text>.</xsl:text> <xsl:call-template name="generic:extract-sentences"> <xsl:with-param name="x" select="substring-after($x,'.')"/> <xsl:with-param name="param1" select="$param1 - 1"/> </xsl:call-template> </xsl:when> <xsl:otherwise/> </xsl:choose> </xsl:template> <xsl:template match="/"> <summary> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="//para"/> <xsl:with-param name="func" select=" 'extract-sentences' "/> <xsl:with-param name="func-param1" select="3"/> </xsl:call-template> </summary> </xsl:template> <xsl:template match="/ | node( ) | @*" mode="generic:map"> <para> <xsl:copy-of select="."/> </para> </xsl:template> </xsl:stylesheet>
These examples are convoluted ways of creating results that can easily be obtained with more straightforward stylesheets. Creating stylesheets that perform transformations a node at a time is easy. For example, the summary stylesheet is more clearly implemented as follows:
<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 name="extract-sentences"> <xsl:param name="text"/> <xsl:param name="num-sentences" select="1"/> <xsl:choose> <xsl:when test="$num-sentences >= 1 and contains($text,'.')"> <xsl:value-of select="substring-before($text,'.')"/> <xsl:text>.</xsl:text> <xsl:call-template name="extract-sentences"> <xsl:with-param name="text" select="substring-after($text,'.')"/> <xsl:with-param name="num-sentences" select="$num-sentences - 1"/> </xsl:call-template> </xsl:when> <xsl:otherwise/> </xsl:choose> </xsl:template> <xsl:template match="/"> <summary> <xsl:apply-templates select=".//para"/> </summary> </xsl:template> <xsl:template match="para"> <para> <xsl:call-template name="extract-sentences"> <xsl:with-param name="text" select="."/> <xsl:with-param name="num-sentences" select="3"/> </xsl:call-template> </para> </xsl:template> </xsl:stylesheet>
However, if in a single stylesheet you need to perform several map-like operations, then the generic implementation could result in less custom-written code.
An alternate mapping generic function is
generic:map2
. Rather than applying a unary
function to a single node set, generic:map2
applies a binary function to the elements of two node sets and
returns the resulting node set:
<xsl:template name="generic:map2"> <xsl:param name="nodes1" select="/.."/> <xsl:param name="nodes2" select="/.."/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="result" select="/.."/> <xsl:choose> <xsl:when test="$nodes1 and $nodes2"> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:aggr-func and @name = $func]"> <xsl:with-param name="x" select="$nodes1[1]"/> <xsl:with-param name="accum" select="$nodes2[1]"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:map2"> <xsl:with-param name="nodes1" select="$nodes1[position( ) > 1]"/> <xsl:with-param name="nodes2" select="$nodes2[position( ) > 1]"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i +1"/> <xsl:with-param name="result" select="$result | exslt:node-set($temp)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="$result" mode="generic:map"/> </xsl:otherwise> </xsl:choose> </xsl:template>
generic:map2
operates on two independent node sets
in parallel and thus can apply any binary generic operation to the
successive nodes. As with generic:map
, the utility
is a function of the frequency of usage.