Newer
Older
XML / modules / tabular.xml
<?xml version="1.0" encoding="utf-8"?>

<!--
	Tabular structures (LaTeX {tabular}, HTML <table>). Note that this is distinct from "tables" in general, which correspond to the {table} environment in LaTeX (i.e., floating).
-->

<stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">


	<!--
		Tabular structures (LaTeX {tabular}, HTML <table>).
		
		@align: The alignment of the table as a whole.
			'left' [default]
			'center' | 'centre'
			'right'
		
		@valign (LaTeX only): Vertical alignment of the tabular within the paragraph. 
			'top'
			'center' | 'centre' [default]
			'bottom'
		
		@border (HTML only): Width of cell border for HTML tables.
		
		@scale (LaTeX only): Scaling factor for the tabular.
		
		@rotate (LaTeX only): Rotation angle of tabular in degrees anti-clockwise.
		
		@long-table (LaTeX only): Use the longtable environment instead of tabular so it can span multiple pages.
		    'no' [default]
		    'yes'
		
		If @long-table is 'yes', the following changes occur:
		    tabular-header is repeated on every page of the longtable (\endhead)
		    tabular-header with @first = 'yes' appears on the first page of the longtable (\endfirsthead)
		    tabular-footer is repeated on every page of the longtable (\endfoot)
		    tabular-footer with @last = 'yes' appears on the last page of the longtable (\endlastfoot)
		    caption is available as a sub-element
	-->
	<template name="tabular" match="tabular">
		<common formats="/latex/xelatex/">
			<!-- spacing -->
			<xsl:call-template name="newline-internal" />
			<xsl:call-template name="newline-internal" />
			
			<!-- Overall tabular alignment. -->
			<xsl:if test="@align">
				<xsl:text>\begin{</xsl:text>
				<xsl:choose>
					<xsl:when test="@align = ('left', 'right')">
						<xsl:text>flush</xsl:text><xsl:value-of select="@align" />
					</xsl:when>
					<xsl:when test="@align = ('center', 'centre')">
						<xsl:text>center</xsl:text>
					</xsl:when>
				</xsl:choose>
				<xsl:text>}</xsl:text>
			</xsl:if>

			<!-- tabular rotation -->
			<xsl:if test="@rotate">
				<xsl:text>\rotatebox{</xsl:text>
				<xsl:value-of select="@rotate" />
				<xsl:text>}{</xsl:text>
			</xsl:if>

			<!-- tabular scaling -->
			<xsl:if test="@scale">
				<xsl:text>\scalebox{</xsl:text>
				<xsl:value-of select="@scale" />
				<xsl:text>}{</xsl:text>
			</xsl:if>

			<xsl:text>\begin{</xsl:text>
			<xsl:value-of select="
			    if (@long-table = ('yes', 'y', 'true', 't', '1')) then 'longtable'
			    else 'tabular'" />
			<xsl:text>}</xsl:text>
			<!-- vertical alignment -->
			<xsl:if test="@valign = ('top', 'bottom')">
				<xsl:text>[</xsl:text>
				<xsl:value-of select="substring(@valign, 1, 1)" />
				<xsl:text>]</xsl:text>
			</xsl:if>
			<xsl:text>{</xsl:text>
			<xsl:apply-templates select="tabular-columns" />
			<xsl:text>}</xsl:text>
            <xsl:apply-templates select="tabular-header" />
            <!-- For long tables, we need to do the headers and footers first. -->
			<xsl:choose>
    		    <xsl:when test="@long-table = ('yes', 'y', 'true', 't', '1')">
                    <xsl:apply-templates select="tabular-footer" />
                    <xsl:apply-templates select="tabular-body" />
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="tabular-body" />
                    <xsl:apply-templates select="tabular-footer" />
                </xsl:otherwise>
            </xsl:choose>
            <xsl:if test="@long-table = ('yes', 'y', 'true', 't', '1') and count(caption) ne 0">
                <xsl:text>\caption{</xsl:text>
                <xsl:apply-templates select="caption" />
                <xsl:text>}</xsl:text>
                <xsl:call-template name="newline-internal" />
                <xsl:if test="@label">
                    <xsl:text>\label{</xsl:text>
                    <xsl:value-of select="@label" />
                    <xsl:text>}</xsl:text>
                    <xsl:call-template name="newline-internal" />
                </xsl:if>
            </xsl:if>
			<xsl:text>\end{</xsl:text>
			<xsl:value-of select="
			    if (@long-table = ('yes', 'y', 'true', 't', '1')) then 'longtable'
			    else 'tabular'" />
			<xsl:text>}</xsl:text>

			<xsl:if test="@scale"><xsl:text>}</xsl:text></xsl:if>

			<xsl:if test="@rotate"><xsl:text>}</xsl:text></xsl:if>

			<xsl:if test="@align">
				<xsl:text>\end{</xsl:text>
				<xsl:choose>
					<xsl:when test="@align = ('left', 'right')">
						<xsl:text>flush</xsl:text>
						<xsl:value-of select="@align" />
					</xsl:when>
					<xsl:when test="@align = ('center', 'centre')">
						<xsl:text>center</xsl:text>
					</xsl:when>
				</xsl:choose>
				<xsl:text>}</xsl:text>
			</xsl:if>

			<!-- spacing -->
			<xsl:call-template name="newline-internal" />
			<xsl:call-template name="newline-internal" />
		</common>
		<common formats="/html/xhtml/">
			<table>
				<xsl:attribute name="border">
					<xsl:value-of select="@border" />
					<xsl:if test="not(@border)">0</xsl:if>
				</xsl:attribute>
				<xsl:attribute name="cellspacing">0</xsl:attribute>
				<xsl:attribute name="style">
					<xsl:text>border-collapse: collapse; </xsl:text>
					<xsl:choose>
						<xsl:when test="@align = ('center', 'centre')">
							<xsl:text>margin-left:auto; margin-right: auto; </xsl:text>
						</xsl:when>
						<xsl:when test="@align eq 'right'">
							<xsl:text>margin-left:auto; </xsl:text>
						</xsl:when>
						<xsl:otherwise />
					</xsl:choose>
				</xsl:attribute>
				<!--
					Note different ordering of tabular components: HTML requires THEAD and TFOOT to precede TBODY.
				-->
				<xsl:apply-templates select="tabular-header" />
				<xsl:apply-templates select="tabular-footer" />
				<xsl:apply-templates select="tabular-body" />
			</table>
		</common>
	</template>
	
	
	<!--
		Specify column formatting, mainly for LaTeX, although HTML does get <td> ALIGN values from here.
		
		@align: The alignment of this particular column.
			'left' [default]
			'center' | 'centre'
			'right'
		
		@left-border: Set to '|' to include a column separator to the left of this column.

		@right-border: Set to '|' to include a column separator to the right of this column.
		
		Careful: including a right-border on a cell and a left-border on the next cell will produce '||', not '|'. BUT, when doing multi-column or multi-row cells, always include both borders if required, because \multicolumn overrides the default border specification.
	-->
	<template name="aligned-tabular-column" match="tabular-columns/column[@align]">
		<common formats="/latex/xelatex/">
			<xsl:value-of select="@left-border" />
			<xsl:value-of select="substring(@align, 1, 1)" />
			<xsl:value-of select="@right-border" />
		</common>
	</template>

	<template name="unaligned-tabular-column" match="tabular-columns/column[not(@align)]">
		<common formats="/latex/xelatex/">
			<xsl:value-of select="@left-border" />
			<xsl:text>l</xsl:text>
			<xsl:value-of select="@right-border" />
		</common>
	</template>
	
	
	<!-- Tabular header. -->
	<template name="tabular-header" match="tabular-header">
		<common formats="/latex/xelatex/">
		    <xsl:apply-templates />
		    <xsl:value-of select="
		        if (../@long-table = ('yes', 'y', 'true', 't', '1')) then
		            if (@first = ('yes', 'y', 'true', 't', '1')) then '\endfirsthead'
		            else '\endhead'
		        else ''" />
		</common>
		<common formats="/html/xhtml/">
			<thead>
				<xsl:apply-templates />
			</thead>
		</common>
	</template>
	
	
	<!-- Tabular footer. -->
	<template name="tabular-footer" match="tabular-footer">
		<common formats="/latex/xelatex/">
		    <xsl:apply-templates />
		    <xsl:value-of select="
		        if (../@long-table = ('yes', 'y', 'true', 't', '1')) then
		            if (@last = ('yes', 'y', 'true', 't', '1')) then '\endlastfoot'
		            else '\endfoot'
		        else ''" />
		</common>
		<common formats="/html/xhtml/">
			<tfoot>
				<xsl:apply-templates />
			</tfoot>
		</common>
	</template>
	
	
	<!-- Tabular main body. -->
	<template name="tabular-body" match="tabular-body">
		<common formats="/latex/xelatex/"><xsl:apply-templates /></common>
		<common formats="/html/xhtml/">
			<tbody>
				<xsl:apply-templates />
			</tbody>
		</common>
	</template>
	
	
	<!--
		A single row of a tabular.
		
		@page-break: Inhibit page breaks after this row (LaTeX only).
	-->
	<template name="row" match="row">
		<common formats="/latex/xelatex/">
			<xsl:apply-templates />
			<xsl:text> \\</xsl:text>
			<xsl:if test="@page-break = ('no', 'n', 'false', 'f', '0')"><xsl:text>*</xsl:text></xsl:if>
			<xsl:text> </xsl:text>
		</common>
		<common formats="/html/xhtml/">
			<tr>
				<xsl:if test="@valign">
					<xsl:attribute name="valign"><xsl:value-of select="@valign" /></xsl:attribute>
				</xsl:if>
				<xsl:apply-templates />
			</tr>
		</common>
	</template>
	
	<!--
		This template handles the following cases:
		
		The last row of a tabular in (Xe)LaTeX shouldn't have a \\ at the end, /unless/ you want a horizontal rule (\hline) under the last row (Lamport, p. 62). This template handles the following cases:

		Case 1: The last row of a tabular-body, without a row-rule following it, and no tabular-footer.
		Case 2: The last row of a tabular-footer, without a row-rule following it.
	-->
	<template name="last-row-body" match="tabular-body/row[( position() = last() ) and not( following-sibling::row-rule ) and not( ancestor::tabular/tabular-footer )]|tabular-footer/row[position() = last() and not( following-sibling::row-rule )]">
		<common formats="/latex/xelatex/">
			<xsl:apply-templates />
			<xsl:text> </xsl:text>
		</common>
		<common formats="/html/xhtml/">
			<xsl:call-template name="row" />
		</common>
	</template>
	
	<!--
		Horizontal rules (LaTeX only).
		
		@columns: The column range to draw the rule across. Specify as you would for a \cline in LaTeX, e.g., '3-5'. If omitted, the rule is drawn across all columns.
		
		@weight: The weight of the rule. Note that double weight isn't available for partial rules in LaTeX, because repeated \cline macros simply draw over the top of each other.
			'single' [default]
			'double' = a double rule
	-->
	<template name="row-rule-full" match="row-rule[not(@columns)]">
		<common formats="/latex/xelatex/">
			<xsl:text>\hline</xsl:text>
			<xsl:if test="@weight eq 'double'"><xsl:text>\hline</xsl:text></xsl:if>
			<xsl:text> </xsl:text>
		</common>
		<common formats="/html/xhtml/">
			<tr>
				<td>
					<xsl:attribute name="style">
						<xsl:text>border-bottom: 1px solid black; </xsl:text>
						<!--
							This is a bit of a hack to get a double border,
							as some browsers don't support the "double" border
							style yet. It looks slightly ugly, but works.
						-->
						<xsl:if test="@weight eq 'double'">
							<xsl:text>border-top: 1px solid black; </xsl:text>
						</xsl:if>
					</xsl:attribute>
					<xsl:attribute name="colspan">
						<xsl:value-of select="count(ancestor::tabular/tabular-columns/column)" />
					</xsl:attribute>
				</td>
			</tr>
		</common>
	</template>

	<template name="row-rule-partial" match="row-rule[@columns]">
		<common formats="/latex/xelatex/">
			<!-- Tokenise the column specifications for later reference. -->
			<xsl:variable name="column-specs" select="tokenize(@columns, ',')" />
			<xsl:for-each select="$column-specs">
				<xsl:text>\cline{</xsl:text>
				<xsl:value-of select="." />
				<!-- Normalise the value to a range if necessary. -->
				<xsl:if test="not(contains(., '-'))">
					<xsl:text>-</xsl:text>
					<xsl:value-of select="." />
				</xsl:if>
				<xsl:text>}</xsl:text>
			</xsl:for-each>
		</common>
		<common formats="/html/xhtml/">
			<!-- Tokenise the column specifications for later reference. -->
			<xsl:variable name="column-specs" select="tokenize(@columns, '[-,]')" />
			
			<!--
				We need to store the value of @weight, because the context will be changed by the for-each loop below. Default to "single" in any case.
			-->
			<xsl:variable name="weight">
				<xsl:value-of select="@weight" />
				<xsl:if test="not(@weight)">
					<xsl:text>single</xsl:text>
				</xsl:if>
			</xsl:variable>
			
			<tr>
				<!--
					Loop through all of the columns in the table, testing to see whether the column index exists in $column-specs. If so, add a border to the cell. Note that this loop changes the current context!
				-->
				<xsl:for-each select="1 to count(ancestor::tabular/tabular-columns/column)">
					<td>
						<xsl:if test="exists(index-of($column-specs, . cast as xs:string))">
							<xsl:attribute name="style">
								<xsl:text>border-bottom: 1px solid black; </xsl:text>
								<xsl:if test="$weight eq 'double'">
									<xsl:text>border-top: 1px solid black; </xsl:text>
								</xsl:if>
							</xsl:attribute>
						</xsl:if>
					</td>
				</xsl:for-each>
			</tr>
		</common>
	</template>
	
	
	<!--
		Multi-row cells (LaTeX only, as this is pretty trivial to achieve in HTML).
		
		@rows: The number of rows this cell spans.
		
		@header: Is this a header cell? [yes, NO]
	-->
	<!--
		Hmm, the multi-row stuff is somewhat broken in LaTeX, oops. Need to insert missing columns (as was done in the calendar XSL) when a multi-row cell is encountered.
		
		Also need to sort out \hlines in the presence of multi-row cells and also what happens to vertical cell borders :(.
		
		This probably needs reworking.
	-->
	<template name="multirow-cell" match="cell" mode="latex-multi-row">
		<common formats="/latex/xelatex/">
			<xsl:text>\multirow{</xsl:text>
			<xsl:value-of select="@rows" />
			<xsl:text>}{*}{</xsl:text>
			<xsl:if test="@header = ('yes', 'y', 'true', 't', '1')"><xsl:text>\textbf{</xsl:text></xsl:if>

			<!-- check for embedded line breaks, if so, embed another tabular inside this one -->
			<xsl:if test="count(child::br) ne 0">
				<xsl:text>\begin{tabular}{@{}</xsl:text>
				<xsl:choose>
					<xsl:when test="@align">
						<xsl:value-of select="substring(@align, 1, 1)" />
					</xsl:when>
					<xsl:otherwise>
						<xsl:text>l</xsl:text>
					</xsl:otherwise>
				</xsl:choose>
				<xsl:text>@{}}</xsl:text>
			</xsl:if>

			<xsl:apply-templates />

			<xsl:if test="count(child::br) ne 0">
				<xsl:text>\end{tabular}</xsl:text>
				<xsl:call-template name="newline-internal" />
			</xsl:if>
			<xsl:if test="@header = ('yes', 'y', 'true', 't', '1')"><xsl:text>}</xsl:text></xsl:if>
			<xsl:text>}</xsl:text>
		</common>
	</template>
	
	
	<!--
		Multi-column cells (LaTeX only, as this is pretty trivial to achieve in HTML).
		
		$num-columns: The number of columns this cell spans.
		
		@header: Is this a header cell? [yes, NO]
	-->
	<template name="multicolumn-cell" match="cell" mode="latex-multi-column">
		<common formats="/latex/xelatex/">
			<!--
				TODO: Not quite sure why the number of columns is a parameter rather than just using the @columns attribute? Something to do with defaults perhaps?
			-->
			<xsl:param name="num-columns">1</xsl:param>
			<xsl:text>\multicolumn{</xsl:text>
			<xsl:value-of select="$num-columns" />
			<xsl:text>}{</xsl:text>
			<xsl:value-of select="@left-border" />
			<xsl:choose>
				<xsl:when test="@align">
					<xsl:value-of select="substring(@align, 1, 1)" />
				</xsl:when>
				<xsl:otherwise>
					<xsl:text>l</xsl:text>
				</xsl:otherwise>
			</xsl:choose>
			<xsl:value-of select="@right-border" />
			<xsl:text>}{</xsl:text>
			<xsl:choose>
				<xsl:when test="@rows">
					<xsl:apply-templates select="." mode="multi-row" />
				</xsl:when>
				<xsl:otherwise>
					<xsl:if test="@header = ('yes', 'y', 'true', 't', '1')"><xsl:text>\textbf{</xsl:text></xsl:if>

					<!-- check for embedded line breaks, if so, embed another tabular inside this one -->
					<xsl:if test="count(child::br) ne 0">
						<xsl:text>\begin{tabular}{@{}</xsl:text>
						<xsl:choose>
							<xsl:when test="@align">
								<xsl:value-of select="substring(@align, 1, 1)" />
							</xsl:when>
							<xsl:otherwise>
								<xsl:text>l</xsl:text>
							</xsl:otherwise>
						</xsl:choose>
						<xsl:text>@{}}</xsl:text>
					</xsl:if>

					<xsl:apply-templates />

					<xsl:if test="count(child::br) ne 0">
						<xsl:text>\end{tabular}</xsl:text>
						<xsl:call-template name="newline-internal" />
					</xsl:if>
					<xsl:if test="@header = ('yes', 'y', 'true', 't', '1')"><xsl:text>}</xsl:text></xsl:if>
				</xsl:otherwise>
			</xsl:choose>
			<xsl:text>}</xsl:text>
		</common>
	</template>
	
	
	<!--
		Top-level template for generating cells.
		
		@columns: The number of columns this cell spans.
		
		@rows: The number of rows this cell spans.
		
		@align: The alignment of this cell. [LEFT, center, right]
		
		@header: Is this a header cell? [yes, NO]
	-->
	<template match="cell">
		<common>
			<!-- position() doesn't seem to work very well in this context. -->
			<xsl:variable name="column-no"><xsl:number /></xsl:variable>
		</common>
		<!--
			Doing this sensibly for LaTeX is actually pretty ugly, because different attribute combinations produce different code :(.
			
			This is the sort of algorithm that can only really be clearly described by a flow chart :).
		-->
		<common formats="/latex/xelatex/">
			<xsl:choose>
				<xsl:when test="@columns">
					<xsl:apply-templates select="." mode="latex-multi-column">
						<xsl:with-param name="num-columns" select="@columns" />
					</xsl:apply-templates>
				</xsl:when>
				<xsl:when test="@align">
					<xsl:apply-templates select="." mode="latex-multi-column" />
				</xsl:when>
				<xsl:when test="@rows">
					<xsl:apply-templates select="." mode="latex-multi-row" />
				</xsl:when>
				<xsl:otherwise>
					<xsl:if test="@header = ('yes', 'y', 'true', 't', '1')"><xsl:text>\textbf{</xsl:text></xsl:if>

					<!-- check for embedded line breaks, if so, embed another tabular inside this one -->
					<xsl:if test="count(child::br) ne 0">
						<xsl:text>\begin{tabular}{@{}</xsl:text>
						<xsl:choose>
							<xsl:when test="@align">
								<xsl:value-of select="substring(@align, 1, 1)" />
							</xsl:when>
							<xsl:otherwise>
								<xsl:text>l</xsl:text>
							</xsl:otherwise>
						</xsl:choose>
						<xsl:text>@{}}</xsl:text>
					</xsl:if>

					<xsl:apply-templates />

					<xsl:if test="count(child::br) ne 0">
						<xsl:text>\end{tabular}</xsl:text>
						<xsl:call-template name="newline-internal" />
					</xsl:if>
					<xsl:if test="@header = ('yes', 'y', 'true', 't', '1')"><xsl:text>}</xsl:text></xsl:if>
				</xsl:otherwise>
			</xsl:choose>
			<xsl:if test="$column-no != last()">
				<xsl:call-template name="tabular-column-separator" />
			</xsl:if>
		</common>
		<!--
			It's much easier in HTML, because a <td> is a <td> is a <td>, regardless of the attributes supplied.
		-->
		<common formats="/html/xhtml/">
			<!--
				Hmm, how to generate either a TD or TH as required, including attributes, without repeating code?
				
				Aha, the answer is attribute value templates!
			-->
			<xsl:variable name="celltype">
				<xsl:choose>
					<xsl:when test="@header = ('yes', 'y', 'true', 't', '1')">th</xsl:when>
					<xsl:otherwise>td</xsl:otherwise>
				</xsl:choose>
			</xsl:variable>
			<xsl:element name="{$celltype}">
				<xsl:attribute name="align">
					<xsl:value-of select="@align" />
					<xsl:if test="not(@align)">
						<xsl:value-of select="ancestor::tabular/tabular-columns/column[position() = $column-no]/@align" />
						<xsl:if test="not(ancestor::tabular/tabular-columns/column[position() = $column-no]/@align)">
							<xsl:text>left</xsl:text>
						</xsl:if>
					</xsl:if>
				</xsl:attribute>
				<xsl:attribute name="valign">
					<xsl:value-of select="@valign" />
					<xsl:if test="not(@valign)"><xsl:text>middle</xsl:text></xsl:if>
				</xsl:attribute>
				<xsl:attribute name="colspan">
					<xsl:value-of select="@columns" />
					<xsl:if test="not(@columns)"><xsl:text>1</xsl:text></xsl:if>
				</xsl:attribute>
				<xsl:attribute name="rowspan">
					<xsl:value-of select="@rows" />
					<xsl:if test="not(@rows)"><xsl:text>1</xsl:text></xsl:if>
				</xsl:attribute>
				<xsl:apply-templates />
			</xsl:element>
		</common>
	</template>


	<!-- Long table captions are only relevant in (Xe)LaTeX. -->
	<template name="long-table-caption" match="tabular[@long-table = ('yes', 'y', 'true', 't', '1')]/caption">
		<common formats="/latex/xelatex/">
			<xsl:apply-templates />
		</common>
	</template>
	
	
</stylesheet>