Map of functions compared to xsl:attribute-set

Inasmuch as I’m trying out XSLT 3.0 techniques, 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 document that contains two <table-wrap> that each contain a <table>. For the sake of the exercise, the <table> in the second <table-wrap> has ‘style="orange"'.

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‘; ‘table-map‘)
  • red.xsl – Styles text is the table head as red (‘master‘; ‘table-map‘)
  • blue.xsl – Styles text is the table body as blue(‘master‘; ‘table-map‘)
  • red-blue.xsl – Imports both ‘red.xsl’ and ‘blue.xsl’ to achieve a combined effect (or try to) (‘master‘; ‘table-map‘)
  • orange.xsl – Styles table background as orange (‘master‘; ‘table-map‘)

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).

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

<xsl:template match="table[@style eq 'orange']">
  <xsl:next-match>
    <xsl:with-param
        name="table-attributes"
        as="attribute()*"
        tunnel="yes">
      <xsl:attribute name="background-color" select="'orange'" />
    </xsl:with-param>
  </xsl:next-match>
</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), 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 reference to the 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‘ in XSLT.

One thought on “Map of functions compared to xsl:attribute-set

Comments are closed.