XQuery/XSL-FO Tables

Motivation edit

You want to be able to create high-quality tabular outputs suitable for book-publishing.

Method edit

To accomplish this we will convert our XML into XSL-FO tables. Unlike HTML, XML-FO allows you to create flows of text and you can set up rules on how objects span page boundaries.

Sample Input edit

Here is a sample XML file that contains a table with two columns.

<table heading="Department Phone Extensions">
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
   <Person>
      <Name>John Doe</Name>
      <Extension>1234</Extension>
   </Person>
   <Person>
      <Name>Sue Smith</Name>
      <Extension>5678</Extension>
   </Person>
</table>

We would like this XML file to be rendered with two columns, the first containing the person's name and the second their phone extension. It should look like the following.

Department Phone Extensions
Name Extension
John Doe 1234
Sue Smith 5678

Example FO File edit

The following is the core of the XML-FO layout that you will need to create the table (without control on the column widths).

<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format">
    <fo:block font-size="14pt" padding="10px" font-family="Verdana">Department Phone Extensions</fo:block>
    <fo:block font-size="10pt">
        <fo:table border="solid" border-collapse="collapse">
            <fo:table-header>
                <fo:table-row space-after="10px">
                    <fo:table-cell>
                        <fo:block font-weight="bold">Name</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block font-weight="bold">Extension</fo:block>
                    </fo:table-cell>
                </fo:table-row>
            </fo:table-header>
            <fo:table-body>
                <fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
<fo:table-row>
                    <fo:table-cell>
                        <fo:block>John Doe</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>1234</fo:block>
                    </fo:table-cell>
                </fo:table-row>
                <fo:table-row>
                    <fo:table-cell>
                        <fo:block>Sue Smith</fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block>5678</fo:block>
                    </fo:table-cell>
                </fo:table-row>
            </fo:table-body>

        </fo:table>
    </fo:block>
</fo:block>

Transform with XQuery edit

Transform with XSLT edit

NOTE: This example should be moved to a book on XSLT. XQuery typeswitch transforms should be used to do this.

We can transform the XML structure to XSL-FO using an XSLT script. This generic script only requires that the root of the XML table is called table with a heading attribute.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fo="http://www.w3.org/1999/XSL/Format"
    exclude-result-prefixes="xs" version="2.0">
    <xsl:template match="/table">
        <fo:block>
            <fo:block font-size="14pt" padding="10px" font-family="Verdana">
                <xsl:value-of select="@heading"/>
            </fo:block>
            <fo:block font-size="10pt">
              <fo:table border="solid" border-collapse="collapse" >
                <fo:table-header>
                    <fo:table-row>
                        <xsl:for-each select="*[1]/*">
                            <fo:table-cell>
                                <fo:block font-weight="bold">
                                    <xsl:value-of select="name(.)"/>
                                </fo:block>
                            </fo:table-cell>
                        </xsl:for-each>
                    </fo:table-row>
                </fo:table-header>
                <fo:table-body>
                    <xsl:apply-templates select="*"/>
                </fo:table-body>
              </fo:table>
            </fo:block>
        </fo:block>
    </xsl:template>
    <xsl:template match="*">
        <fo:table-row>
            <xsl:for-each select="*">
                <fo:table-cell>
                    <fo:block>
                        <xsl:value-of select="."/>
                    </fo:block>
                </fo:table-cell>
            </xsl:for-each>
        </fo:table-row>
    </xsl:template>
</xsl:stylesheet>

XQuery integration edit

Finally we can generate the full XSL-FO document and render as PDF with an XQuery script. We use the XSLT to transform the table, and then embed that XSL-FO fragment in the XSL-FO master before rendering as PDF and streaming the binary document. There are of course other ways to assemble the full XSLT-FO document.

xquery version "1.0";
import module namespace xslfo="http://exist-db.org/xquery/xslfo";
import module namespace transform="http://exist-db.org/xquery/transform";
declare namespace fo="http://www.w3.org/1999/XSL/Format";

let $table :=
<table heading="Department Phone Extensions">
    <Person>
        <Name>John Doe</Name>
        <Extension>1234</Extension>
    </Person>
    <Person>
        <Name>Doe John</Name>
        <Extension>4321</Extension>
    </Person>
    <Person>
        <Name><span style="color: #f00;">Sue Smith</span></Name>
        <Extension>5678</Extension>
    </Person>
</table>

let $table-fo := transform:transform($table,doc("/db/Wiki/eXist/xsl-fo/table2fo.xsl"),())
let $fo := 
    <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
            <fo:layout-master-set>
                <fo:simple-page-master master-name="my-page">
                    <fo:region-body margin="1in"/>
                </fo:simple-page-master>
            </fo:layout-master-set>
            <fo:page-sequence master-reference="my-page">
                <fo:flow flow-name="xsl-region-body">
                    <fo:block>
                        {$table-fo}
                    </fo:block>
                </fo:flow>
            </fo:page-sequence>
      </fo:root>
   
let $pdf := xslfo:render($fo, "application/pdf", ())
return 
  response:stream-binary($pdf, "application/pdf", "output.pdf")

Execute

Database data edit

As a further example, the following XQuery selects all employees and renders them in a PDF table:

xquery version "1.0";
import module namespace xslfo="http://exist-db.org/xquery/xslfo";
import module namespace transform="http://exist-db.org/xquery/transform";
declare namespace fo="http://www.w3.org/1999/XSL/Format";

let $table :=
<table heading="Employees">
   {//Emp}
</table>

let $table-fo := transform:transform($table,doc("/db/Wiki/eXist/xsl-fo/table2fo.xsl"),())
let $fo := 
    <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
            <fo:layout-master-set>
                <fo:simple-page-master master-name="my-page">
                    <fo:region-body margin="1in"/>
                </fo:simple-page-master>
            </fo:layout-master-set>
            <fo:page-sequence master-reference="my-page">
                <fo:flow flow-name="xsl-region-body">
                    <fo:block>
                        {$table-fo}
                    </fo:block>
                </fo:flow>
            </fo:page-sequence>
      </fo:root>
   
let $pdf := xslfo:render($fo, "application/pdf", ())
return 
  response:stream-binary($pdf, "application/pdf", "output.pdf")

Execute