Recipe 16.4. Creating Generic Mapping Functions
Problem
You want to create reusable templates for performing
operations on items in a node set.
Solution
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>
Discussion
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.
|