Friday, October 30, 2009

XSL-FO number-columns-spanned="remainder"

Every so often when developing some XSL-FO I get the dreaded...

column-number or number of cells in the row overflows the number of fo:table-column specified for the table

...these tend to be a nightmare to track down, and very fiddly to fix, because often you need to add a number-columns-spanned attribute to one of the fo:table-cell elements, and in a dynamic world you have to figure out exactly how many cells to span. Agh!

It'd be much easier if XSL-FO let you just say number-columns-spanned="remainder". So today I wrote a little XSLT that you can put after your XSL-FO and before you try to serialize it (with something like Apache FOP).

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet xmlns:xsl="" version="2.0"

   <!-- number-columns-spanned: remainder -->
   <xsl:template match="fo:table-cell[@number-columns-spanned = 'remainder']" mode="#all">
         <xsl:attribute name="number-columns-spanned">
            <xsl:variable name="number-columns-in-table" as="xs:integer">
               <xsl:value-of select="count(ancestor::fo:table[1]/fo:table-column)"/>
            <xsl:variable name="number-columns-in-row" as="xs:integer">
               <xsl:value-of select="count(preceding-sibling::fo:table-cell[not(@number-columns-spanned)]) + count(following-sibling::fo:table-cell[not(@number-columns-spanned)])"/>
            <xsl:variable name="number-spanned-columns-in-row" as="xs:integer">
               <xsl:value-of select="sum(preceding-sibling::fo:table-cell[@number-columns-spanned ne 'remainder']/@number-columns-spanned) + sum(following-sibling::fo:table-cell[@number-columns-spanned ne 'remainder']/@number-columns-spanned)"/>
            <xsl:value-of select="$number-columns-in-table - $number-columns-in-row - $number-spanned-columns-in-row"/>
         <xsl:apply-templates select="@*[not(local-name() = 'number-columns-spanned')]|node()"/>

   <!-- identity transform -->
   <xsl:template match="@*|node()">
         <xsl:apply-templates select="@*|node()"/>


I should've done it years ago. Note unfortunately it won't work if you've got other rows that are using number-rows-spanned. Improvements welcome!