[xsl] Map of functions compared to xsl:attribute-set

Subject: [xsl] Map of functions compared to xsl:attribute-set
From: "Tony Graham" <tgraham@xxxxxxxxxx>
Date: Mon, 25 Nov 2013 18:22:59 -0000 (GMT)
As part of trying out XSLT 3.0 techniques [1], I've been looking at using
a map of functions that return attributes as an alternative to using
xsl:attribute-set.  However, in the work so far, neither is significantly
better than the other IMO.

The current input is a JATS [6] document that contains two <table-wrap>
[3][4] that each contain a <table>.  For the sake of the exercise, the
<table> in the second <table-wrap> has 'style="orange"' [5].

There's currently two branches in the repository: 'master' uses
xsl:attribute-set, and 'table-map' uses maps of functions.

Also for the sake of the exercise there's multiple stylesheets in use:

 - xhtml-tables-fo3xsl - Base table module

   'master': [7]    'table-map': [8]

 - red.xsl - Styles text is the table head as red

   'master': [9]    'table-map': [10]

 - blue.xsl - Styles text is the table body as blue

   'master': [11]   'table-map': [12]

 - red-blue.xsl - Imports both 'red.xsl' and
   'blue.xsl' to achieve a combined effect (or try to)

   'master': [13]   'table-map': [14]

 - orange.xsl - Styles table background as orange

   'master': [15]   'table-map': [16]

There's also an Oxygen project file to make it easier to run the
different-coloured stylesheets.

The xsl:attribute-set approach uses an attribute set named after each
table-related element, and the different stylesheets add attribute
instructions to the appropriate attribute set.  'red-blue.xsl' takes the
convenient approach of just importing 'red.xsl' and 'blue.xsl' (and,
coincidentally, manages to import the whole JATS stylesheets twice, but
that's the price you pay for convenience) and the xsl:attribute-set from
the different stylesheets just combine (since at present there's no
overlap/conflict to worry about) [19].

For the table that wants to be orange, 'orange.xsl' passes specific
attributes in a 'table-attributes' parameter:

-----
<xsl:template match="table">
  <xsl:param name="table-attributes"
             as="attribute()*"
             tunnel="yes" />

  <fo:table xsl:use-attribute-sets="table fo:table">
    <xsl:sequence select="$table-attributes" />
    <xsl:apply-templates />
  </fo:table>
</xsl:template>
-----

that override attributes defined in the 'table' attribute set:

-----
<xsl:template match="table">
  <xsl:param name="table-attributes"
             as="attribute()*"
             tunnel="yes" />

  <fo:table xsl:use-attribute-sets="table fo:table">
    <xsl:sequence select="$table-attributes" />
    <xsl:apply-templates />
  </fo:table>
</xsl:template>
-----

For the 'map of functions' approach, the templates for the table-related
elements each have a 'table-functions' tunnel parameter that is a map of
the functions to use for the appropriate table-related element(s).  These
override the default functions, which don't do anything.  Making a default
map is analogous to needing to define empty attribute sets since calling a
non-existent attribute set is an error [17], but an alternative would be
to only call the function for the current element if it exists in the
current $table-functions map.

'red.xsl' and 'blue.xsl' work by passing appropriate maps of functions. 
'red-blue.xsl' is the same as for the xsl:attribute-set approach, and it
doesn't produce red table head text because, with the way that import
precedence works, the template for 'thead' in 'red.xsl' is never used.

The template for the table that wants to be orange uses the same mechanism
as the general table templates and passes a map containing a function to
use for the <table> element:

-----
<xsl:function name="x3tb:orange-table" as="attribute()*">
  <xsl:param name="context" as="element()" />

  <xsl:attribute name="background-color" select="'orange'" />
</xsl:function>

<xsl:template match="table[@style eq 'orange']">
  <xsl:next-match>
    <xsl:with-param
        name="table-functions"
        as="map(xs:string, function(element()) as attribute()*)"
        select="map {
                  'table' := x3tb:orange-table#1
                }"
        tunnel="yes" />
  </xsl:next-match>
</xsl:template>
-----

The absence of an XPath-level computed attribute constructor made making
the function verbose compared to how you'd make an attribute in XQuery and
meant that it could not be an anonymous function.

That function for 'table' overrode the default:

-----
<xsl:template match="table">
  <xsl:param
      name="table-functions"
      as="map(xs:string, function(*))?"
      tunnel="yes" />

  <xsl:variable
      name="use-table-functions"
      select="map:new(($default-table-functions, $table-functions))"
      as="map(xs:string, function(*))" />

  <fo:table>
    <xsl:sequence select="$use-table-functions('table')(.)" />
    <xsl:apply-templates />
  </fo:table>
</xsl:template>
-----

Using a function that takes the context node as a parameter to define
attributes is not dissimilar to using xsl:attribute-set, since both can
evaluate expressions based on the context node and global variables only.

The way that attribute instructions defined with xsl:attribute-set can
combine across modules can work to your advantage, but there's no way to
'undefine' attribute instructions in attribute sets other than defining
attributes with the same name in a situation that has higher precedence.

Combining the functions that define attributes or the maps of functions
that define attributes across modules in a way that 'just works' when you
add a new module or override an existing template still requires thought.

However, the 'map of functions' approach could be extended to use
functions that take other, additional parameters to further control the
processing and/or other maps of other functions could be used in other
places to generate elements.  Indeed, the entire table processing could be
rewritten to use a map of functions that each 'do' the processing for the
context element, but by then you'll have just reimplemented 'typeswitch'
[18] in XSLT.

Regards,


Tony Graham                                   tgraham@xxxxxxxxxx
Consultant                                 http://www.mentea.net
Mentea       13 Kelly's Bay Beach, Skerries, Co. Dublin, Ireland
 --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --
    XML, XSL-FO and XSLT consulting, training and programming
       Chair, Print and Page Layout Community Group @ W3C

[1]
http://www.biglist.com/lists/lists.mulberrytech.com/xsl-list/archives/201310/msg00107.html
[2]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/table-test.xml
[3]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/table-test.xml#L30
[4]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/table-test.xml#L50
[5]
http://jats.nlm.nih.gov/articleauthoring/tag-library/1.0/index.html?attr=style
[6] http://jats.nlm.nih.gov/index.html
[7]
https://github.com/MenteaXML/xslt3testbed/blob/master/xsl/xhtml-tables-fo3.xsl
[8]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xsl/xhtml-tables-fo3.xsl
[9]
https://github.com/MenteaXML/xslt3testbed/blob/master/xml/table-test/red.xsl
[10]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/red.xsl
[11]
https://github.com/MenteaXML/xslt3testbed/blob/master/xml/table-test/blue.xsl
[12]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/blue.xsl
[13]
https://github.com/MenteaXML/xslt3testbed/blob/master/xml/table-test/red-blue.xsl
[14]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/red-blue.xsl
[15]
https://github.com/MenteaXML/xslt3testbed/blob/master/xml/table-test/orange.xsl
[16]
https://github.com/MenteaXML/xslt3testbed/blob/table-map/xml/table-test/orange.xsl
[17] http://www.w3.org/TR/xslt-30/#err-XTSE0710
[18] http://www.w3.org/TR/xquery/#id-typeswitch
[19] There's also attribute sets corresponding to the FOs that the table
elements become, but they have no effect at present.

Current Thread