Skip to content

Instantly share code, notes, and snippets.

@mkaatman
Last active September 6, 2018 15:44
Show Gist options
  • Save mkaatman/6734900 to your computer and use it in GitHub Desktop.
Save mkaatman/6734900 to your computer and use it in GitHub Desktop.
XSL templates to transform a DITA list to a dynamic FO table via outputclass.
<!--
It works with both simple list and unordered list. The output can be sorted across rows or columns.
The outputclass supports the number of columns and you can specify if you want it to be ordered vertically across rows or horizontally across columns.
Format: <number of columns>column(optional: _vert)
Examples:
3 column vertical sort – <sl outputclass=”3column_vert”>
2 column horizontal sort - <sl outputclass=”2column”>
You will need to define a "my" namespace in your stylesheet:
<xsl:stylesheet xmlns:my="http://www.example.com" exclude-result-prefixes="my" version="2.0">
Thanks to Steven Calderwood for putting 99.9% of this together and continually helping me enhance it. Feedback welcomed.
-->
<xsl:template match="*[local-name() = 'sl' or local-name() = 'ul' or local-name() = 'ol'][number(substring-before(@outputclass,'column')) = number(substring-before(@outputclass,'column'))]"> <!-- Process only sl whose outputclass contains the word column and before that word there is a number; number(x) = number(x) is only true where x is a number or boolean value; if x is neither then number() returns NaN and NaN is not equal to itself -->
<xsl:variable name="childrenListItems" select="if (local-name()='sl') then sli else li" as="element()+"/>
<xsl:variable name="numberColumns" select="xs:integer(number(substring-before(@outputclass,'column')))" as="xs:integer" />
<xsl:variable name="numberRowsNeeded" select="xs:integer(ceiling(count($childrenListItems) div $numberColumns))" as="xs:integer"/>
<xsl:variable name="numberCellsNeeded" select="$numberColumns * $numberRowsNeeded" as="xs:integer" />
<xsl:variable name="sortedChildrenListItems" select="my:sort($childrenListItems)"/>
<xsl:variable name="cells" as="element()+"> <!-- we create our cells first, including the empty ones we need to make a rectangular matrix -->
<xsl:apply-templates select="$sortedChildrenListItems" mode="createCells"> <!-- I could have used a named template but being reminded of the power of modes is a good thing -->
<xsl:with-param name="numberCellsNeeded" select="$numberCellsNeeded" as="xs:integer"/>
</xsl:apply-templates>
</xsl:variable>
<xsl:variable name="abc" select="
if (contains(@outputclass,'_vert')) then 'vert'
else 'horiz'
"
/> <!-- Note the if then we do inside this xpath expression, this is very powerful; also note that the whitespace I added doesn't affect the processing, only the readability -->
<!-- Switched to contains in case of multiple outputclasses -->
<xsl:variable name="rows" as="element()+"> <!-- create the rows before assigning items to cells, with the rows created and our knowledge of the number of columns needed we have a matrix -->
<xsl:call-template name="createRows">
<xsl:with-param name="numberRowsNeeded" select="$numberRowsNeeded" as="xs:integer" />
</xsl:call-template>
</xsl:variable>
<fo:table width="100%">
<fo:table-body>
<xsl:call-template name="fillRows"> <!-- with this template we will fill the cells in the matrix -->
<xsl:with-param name="rows" select="$rows" as="element()+" tunnel="yes" />
<xsl:with-param name="numberColumns" select="$numberColumns" as="xs:integer" tunnel="yes" />
<xsl:with-param name="cells" select="$cells" as="element()+" tunnel="yes" />
<xsl:with-param name="abc" select="$abc" as="xs:string" tunnel="yes" />
</xsl:call-template>
</fo:table-body>
</fo:table>
</xsl:template>
<xsl:template name="fillRows">
<xsl:param name="rows" required="yes" as="element()+" tunnel="yes" />
<xsl:param name="cells" required="yes" as="element()+" tunnel="yes" />
<xsl:param name="numberColumns" required="yes" as="xs:integer" tunnel="yes" />
<xsl:param name="abc" required="yes" as="xs:string" tunnel="yes" />
<xsl:variable name="newRows"> <!-- I like to capture my content in variables for post-processing in case needed -->
<xsl:for-each select="$rows">
<xsl:variable name="curRow" select="position()" as="xs:integer" /> <!-- the context item inside a for-each is the current item of the for-each, so we capture its position because the context item is about to change -->
<fo:table-row keep-together.within-column="1">
<xsl:choose>
<xsl:when test="$abc eq 'horiz'">
<!--<xsl:message>HORIZ</xsl:message>-->
<xsl:for-each select="1 to $numberColumns"> <!-- the context item has changed, it is now the current item in $numberColumns -->
<xsl:variable name="curColumn" select="position()" as="xs:integer" />
<xsl:sequence select="$cells[$curColumn + (($curRow - 1) * $numberColumns)]" /> <!-- select the table-cell using a formula that selects the right cell when we want to alphabetize horizontally -->
</xsl:for-each>
</xsl:when>
<xsl:when test="$abc eq 'vert'">
<!--<xsl:message>VERT</xsl:message>-->
<xsl:for-each select="1 to $numberColumns">
<xsl:variable name="curColumn" select="position()" as="xs:integer" />
<xsl:sequence select="$cells[$curRow + (($curColumn - 1) * count($rows))]"/> <!-- select the table-cell using a formula that selects the right cell when we want to alphabetize vertically-->
</xsl:for-each>
</xsl:when>
</xsl:choose>
</fo:table-row>
</xsl:for-each>
</xsl:variable>
<xsl:sequence select="$newRows" /> <!-- output to the result tree our new rows -->
</xsl:template>
<xsl:template name="createRows">
<xsl:param name="numberRowsNeeded" as="xs:integer" required="yes" />
<xsl:for-each select="1 to $numberRowsNeeded"> <!-- create the number of empty rows we need -->
<fo:table-row>
</fo:table-row>
</xsl:for-each>
</xsl:template>
<xsl:template match="sli|li" mode="createCells">
<xsl:param name="numberCellsNeeded" required="yes" as="xs:integer" />
<fo:table-cell text-align="from-table-column()">
<fo:block start-indent="3pt" end-indent="3pt"><xsl:apply-templates /></fo:block> <!-- again, as before, don't want to use copy-of, which makes a deep copy; note, the lack of a mode declaration on this apply-templates; that means we process the nodes in the #default mode, which is what we want, because that's where the templates for image, p, etc. are -->
</fo:table-cell>
<xsl:choose>
<xsl:when test="(position() = last()) and count(../*[sli or li]) &lt; $numberCellsNeeded"> <!-- only create empty cells if we need em -->
<xsl:call-template name="emptycell">
<xsl:with-param name="cellcounter" select="1" as="xs:integer" />
<xsl:with-param name="numberMoreCellsNeeded" select="$numberCellsNeeded - count(../*[sli or li])" as="xs:integer" tunnel="yes" />
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="emptycell">
<xsl:param name="cellcounter" required="yes" as="xs:integer" />
<xsl:param name="numberMoreCellsNeeded" required="yes" as="xs:integer" tunnel="yes" />
<xsl:if test="$cellcounter &lt;= $numberMoreCellsNeeded"> <!-- create only the number of empty cells as we need to fill out the matrix -->
<fo:table-cell></fo:table-cell>
<xsl:call-template name="emptycell">
<xsl:with-param name="cellcounter" select="$cellcounter + 1" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:function name="my:sort"> <!-- Not sure why we have to do this in a function but we do otherwise we just get the text nodes -->
<xsl:param name="in"/>
<xsl:perform-sort select="$in">
<xsl:sort select="text()[1]"/>
</xsl:perform-sort>
</xsl:function>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment