<?xml version="1.0" encoding="UTF-8"?>
<essay xml:lang="en" version="pto" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:gal="http://norman.walsh.name/rdf/gallery#">
<?force-one-column?>
<info>
    
    
    
    
    
    
    
    
<title>DocBook Title Pages with XSLT 2.0</title><biblioid class="uri">http://norman.walsh.name/2004/07/27/titlepages</biblioid>
<volumenum>7</volumenum>
<issuenum>135</issuenum>
<pubdate>2004-07-27T07:01:38-04:00</pubdate>
<date>$Date$</date>
<author>
      <personname>
<firstname>Norman</firstname>
	<surname>Walsh</surname>
</personname>
    </author>
<copyright>
      <year>2004</year>
      <holder>Norman Walsh</holder>
    </copyright>
<abstract>
<para>XSLT 2.0 is around the corner and I’ve started to think about
how the DocBook XSL Stylesheets might be improved in an XSLT 2.0
version. One of the things that I’d like to address is the clumsy way
title pages are currently handled.
</para>
</abstract>
<dc:subject rdf:resource="http://norman.walsh.name/knows/taxonomy#DocBook"/>
<dc:subject rdf:resource="http://norman.walsh.name/knows/taxonomy#XSLT2"/>
</info>

<note>
<para xml:id="p1">This essay is a bit of a deep dive on XSLT 2.0 and handling a
hard customization problem in the DocBook stylesheets.
If your interests don’t run along those lines, well, you may get
a little bored (more than usual, I mean :-)).
</para>
</note>

<para xml:id="p2"><phrase xlink:href="http://www.w3.org/TR/xslt20/">XSLT 2.0</phrase> is
around the corner (it’s probably a little too early to describe it as
“just around the corner”) and I’ve started to think about how the
<phrase xlink:href="http://docbook.sourceforge.net/">DocBook XSL
Stylesheets</phrase> might be improved in an XSLT 2.0 version. (No, the
XSLT 1.0 version isn’t going away any time soon, have no fear.)</para>

<para xml:id="p3">One of the things that I’d like to address is the clumsy way
that title pages are currently handled. One goal of the stylesheets is
to allow end users to customize them without becoming XSLT
template-writing wizards. There’s no question that if you’re an XSLT
hack, you can do almost anything: the trick is arranging things so that
the average mortal can do stuff too.</para>

<para xml:id="p4">Lots of customizations: double sided printing,
admonition graphics, section numbering, etc., can be achieved with
simple parameters, but title pages don’t fall into that category.
There’s a lot of variation possible in title pages and no easy way to
describe it in terms of scalar parameters.</para>

<para xml:id="p5">The current solution is to write an XML document that contains a template
that describes the title page you’d like, then process that template
with a stylesheet to <emphasis>produce a stylesheet</emphasis> that you actually
incorporate into your customization (yet another stylesheet)
which you use to produce
your output. That’s way too many steps, way to much complexity. The stylesheet
produced from the template, by the way, is a horrifying thing.</para>

<para xml:id="p6">I think we can do better now. Let’s begin by making three simplifying
assumptions:</para>

<orderedlist>
<listitem>
<para xml:id="p7">We’re using XSLT 2.0.
</para>
</listitem>
<listitem>
<para xml:id="p8">We’re processing DocBook NG (aka 5.0, we think, eventually). Specifically,
we’re processing DocBook elements that can be identified by the fact that they’re
in a DocBook namespace.
</para>
</listitem>
<listitem>
<para xml:id="p9">The customizer is going to have to have write some templates, we
can’t do this with scalar parameters. The
goal is to keep things as simple as possible.
</para>
</listitem>
</orderedlist>

<para xml:id="p10">Let’s start with a parameter to describe the title
page. How does this look?</para>

<programlisting>&lt;xsl:param name="preface.titlepage.recto"&gt;
  &lt;div class="titlepage"&gt;
    &lt;db:title/&gt;
    &lt;db:subtitle/&gt;
    &lt;db:pubdate/&gt;
    &lt;db:author/&gt;
    &lt;db:releaseinfo role="cvs"/&gt;
    &lt;db:copyright/&gt;
    &lt;db:abstract/&gt;
  &lt;/div&gt;
  &lt;hr/&gt;
&lt;/xsl:param&gt;</programlisting>

<para xml:id="p11">That says that the title page for a Preface should be wrapped in
a <tag class="starttag">div class="titlepage"</tag> tag,
should include title, subtitle, publication
date, author, release info
identified as “cvs” with the <tag class="attribute">role</tag> attribute,
copyright, and
abstract elements, and should be separated from the rest of the document with
an <tag class="emptytag">hr</tag>.</para>

<para xml:id="p12">The idea is that the structure of the title page (the
<tag>div</tag> and <tag class="emptytag">hr</tag> in this case) is written
just the way the customizer wants it. When the stylesheet processes this
parameter, it will replace each of the DocBook elements with the result
of processing the matching element
from the source document.</para>

<para xml:id="p13">Unfortunately, it isn’t <emphasis>quite</emphasis> that simple.
Some elements, like <tag>bibliography</tag>, have an optional title. If it
isn’t specified in the source document, we need to provide a default.
Unfortunately, if it isn’t in the source document, there isn’t going to be
any element to match, so we’re going to have to be able call named templates.
A couple of attributes from another namespace will provide that functionality:
</para>

<programlistingco>
<areaspec>
<area xml:id="co-t-nt" coords="3 31"/>
<area xml:id="co-t-force" coords="4 22"/>
</areaspec>
<programlisting>&lt;xsl:param name="bibliography.titlepage.recto"&gt;
  &lt;div class="titlepage"&gt;
    &lt;db:title t:named-template="component.title"
              t:force="1"/&gt;
    &lt;db:subtitle/&gt;
  &lt;/div&gt;
  &lt;hr/&gt;
&lt;/xsl:param&gt;</programlisting>

<calloutlist>
<callout arearefs="co-t-nt">
<simpara xml:id="p42">The <tag class="attribute">t:named-template</tag> attribute tells
the process to call a named template to handle the title, instead of just
processing the <tag>db:title</tag> in a mode.</simpara>
</callout>
<callout arearefs="co-t-force">
<simpara xml:id="p43">The <tag class="attribute">t:force</tag> attribute tells
the process to call the named template even if the <tag>db:title</tag>
element isn’t present in the source document.</simpara>
</callout>
</calloutlist>

</programlistingco>

<para xml:id="p14">That’s not too bad.</para>

<para xml:id="p15">Ideally, the only thing a customizer should have to do is redefine
the appropriate title page template.</para>

<para xml:id="p16">Now, before we get started, you might be wondering why we don’t just
put <emphasis>all</emphasis> the markup in this template. Like this, perhaps:</para>

<programlisting>&lt;xsl:param name="preface.titlepage.recto"&gt;
  &lt;div class="titlepage"&gt;
    &lt;h1&gt;&lt;db:title/&gt;&lt;/h1&gt;
    &lt;h2&gt;&lt;db:subtitle/&gt;&lt;/h2&gt;
    &lt;h3&gt;Published on: &lt;db:pubdate/&gt;&lt;/h3&gt;
    &lt;h3&gt;By &lt;db:author/&gt;&lt;/h3&gt;
    &lt;h4&gt;&lt;db:releaseinfo role="cvs"/&gt;&lt;/h4&gt;
    &lt;h4&gt;&lt;db:copyright/&gt;&lt;/h4&gt;
    &lt;div class="abstract"&gt;
      &lt;db:abstract/&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;hr/&gt;
&lt;/xsl:param&gt;</programlisting>

<para xml:id="p17">The problem is that some elements can be repeated, and sometimes at different
levels (<foreignphrase>e.g.</foreignphrase> <tag>authorgroup</tag> vs.
<tag>author</tag>).
Consider a document with multiple authors, you probably wouldn’t want to
repeat the generated text “By ” in front of each of them. Any system rich enough
to handle that much complexity would, I fear, become as complex as writing the
XSLT by hand. So instead, we rely on XSLT templates for each of the individual elements
to generate the markup for that element, and we hope that the supplied defaults
work most of the time.</para>

<para xml:id="p18">Ok, back to work. How are we going to process our template parameter?</para>

<para xml:id="p19">First, we have to make sure something gets called to do the processing,
so at the beginning of each XSLT template for an element that can have a title
page (<tag>book</tag>, <tag>preface</tag>, …, <tag>bibliography</tag>, etc.)
we call a <code>titlepage</code> template. Here’s what a template for
<tag>preface</tag> might look like:</para>

<programlistingco>
<areaspec>
<areaset xml:id="co-call-templates">
  <area xml:id="co-tpr" coords="3 41"/>
  <area xml:id="co-tpv" coords="9 41"/>
</areaset>
<area xml:id="co-apply-templates" coords="16 27"/>
</areaspec>
<programlisting>&lt;xsl:template match="db:preface"&gt;
  &lt;div class="preface"&gt;
    &lt;xsl:call-template name="titlepage"&gt;
      &lt;xsl:with-param name="info"
                      select="db:title|db:subtitle|db:titleabbrev|db:info/*"/&gt;
      &lt;xsl:with-param name="content" select="$preface.titlepage.recto"/&gt;
    &lt;/xsl:call-template&gt;

    &lt;xsl:call-template name="titlepage"&gt;
      &lt;xsl:with-param name="side" select="'verso'"/&gt;
      &lt;xsl:with-param name="info"
                      select="db:title|db:subtitle|db:titleabbrev|db:info/*"/&gt;
      &lt;xsl:with-param name="content" select="$preface.titlepage.verso"/&gt;
    &lt;/xsl:call-template&gt;

    &lt;xsl:apply-templates/&gt;
  &lt;/div&gt;
&lt;/xsl:template&gt;</programlisting>
</programlistingco>

<calloutlist>
<callout arearefs="co-call-templates">
<simpara xml:id="p20">The important part here is the call to <code>titlepage</code>
template, once
for the recto titlepage and once for the verso (for XHTML output, the verso
one is probably almost always empty, but it’s needed in the FO output).
We pass in the list of “info elements” from the
source document and the
appropriate titlepage parameter.</simpara>
</callout>
<callout arearefs="co-apply-templates">
<simpara xml:id="p21">After processing the title page, of course, we have to go on and process
the rest of the elements in the <tag>preface</tag>.</simpara>
</callout>
</calloutlist>

<para xml:id="p22">The <code>titlepage</code> named-template
looks like this:</para>

<programlistingco>
<areaspec>
<areaset xml:id="tc1-2">
  <area xml:id="tc1" coords="2 56"/>
  <area xml:id="tc2" coords="3 40"/>
</areaset>
<area xml:id="tc3" coords="6 53"/>
<area xml:id="co-tunnel" coords="8 62"/>
<area xml:id="co-node" coords="9 45"/>
</areaspec>
<programlisting>&lt;xsl:template name="titlepage"&gt;
  &lt;xsl:param name="side" select="'recto'" as="xs:string"/&gt;
  &lt;xsl:param name="info" as="element()*"/&gt;
  &lt;xsl:param name="content"/&gt;

  &lt;xsl:if test="$content instance of document-node()"&gt;
    &lt;xsl:apply-templates select="$content" mode="titlepage-templates"&gt;
      &lt;xsl:with-param name="side" select="$side" tunnel="yes"/&gt;
      &lt;xsl:with-param name="node" select="." tunnel="yes"/&gt;
      &lt;xsl:with-param name="info" select="$info" tunnel="yes"/&gt;
    &lt;/xsl:apply-templates&gt;
  &lt;/xsl:if&gt;
&lt;/xsl:template&gt;</programlisting>

<calloutlist>
<para xml:id="p23">There are a few XSLT 2.0 features to consider here:</para>
<callout arearefs="tc1-2">
<simpara xml:id="p24">We specify the argument types where it will help catch errors. In this 
case the “<parameter>side</parameter>”, which defaults to “<literal>recto</literal>”,
must be a string and “<parameter>info</parameter>”, the info elements from the
source document, must be a sequence of elements.</simpara>
</callout>
<callout arearefs="tc3">
<simpara xml:id="p25">We only do further processing if the
“<parameter>content</parameter>” passed in is a document node
(<foreignphrase>i.e.</foreignphrase>, if the
parameter is an empty sequence, we do nothing).</simpara>
</callout>
<callout arearefs="co-tunnel">
<simpara xml:id="p26">We’re taking advantage of
<phrase xlink:href="http://www.w3.org/TR/xslt20/#tunnel-params">tunnel parameters</phrase>
to make argument passing simpler.
</simpara>
</callout>
<callout arearefs="co-node">
<simpara xml:id="p27">From this point on, we’ll be processing the elements in the titlepage
parameter, so we pass along the important context (the <tag>preface</tag>,
for example) in <parameter>node</parameter>.</simpara>
</callout>
</calloutlist>
</programlistingco>

<para xml:id="p28">XSLT 2.0 features aside, the practical upshot of <code>titlepage</code>
is that it processes each of the elements of the title page parameter
in “<literal>titlepage-templates</literal>” mode.</para>

<para xml:id="p29">In <literal>titlepage-templates</literal> mode, anything not in the
DocBook namespace is just passed straight through. But we need to do some
extra work for DocBook elements:</para>

<programlistingco>
<areaspec>
<area xml:id="co-matches" coords="7 62"/>
<area xml:id="co-named" coords="10 39"/>
<area xml:id="co-force" coords="11 62"/>
<area xml:id="co-tp-templates" coords="12 54"/>
<area xml:id="co-this" coords="15 66"/>
<area xml:id="co-tp-mode" coords="21 67"/>
</areaspec>
<programlisting>&lt;xsl:template match="db:*" mode="titlepage-templates" priority="1000"&gt;
  &lt;xsl:param name="side" tunnel="yes"/&gt;
  &lt;xsl:param name="node" tunnel="yes"/&gt;
  &lt;xsl:param name="info" tunnel="yes"/&gt;

  &lt;xsl:variable name="this" select="."/&gt;
  &lt;xsl:variable name="content" select="$info[dbf:node-matches($this,.)]"/&gt;

  &lt;xsl:choose&gt;
    &lt;xsl:when test="@t:named-template"&gt;
      &lt;xsl:if test="$content or (@t:force and @t:force != 0)"&gt;
        &lt;xsl:call-template name="titlepage-templates"&gt;
          &lt;xsl:with-param name="template" select="@t:named-template" tunnel="yes"/&gt;
          &lt;xsl:with-param name="content" select="$content" tunnel="yes"/&gt;
          &lt;xsl:with-param name="this" select="$this" tunnel="yes"/&gt;
        &lt;/xsl:call-template&gt;
      &lt;/xsl:if&gt;
    &lt;/xsl:when&gt;
    &lt;xsl:otherwise&gt;
      &lt;!-- $content may be empty --&gt;
      &lt;xsl:apply-templates select="$content" mode="titlepage.mode"/&gt;
    &lt;/xsl:otherwise&gt;
  &lt;/xsl:choose&gt;
&lt;/xsl:template&gt;</programlisting>
</programlistingco>

<calloutlist>
<callout arearefs="co-matches">
<simpara xml:id="p30">The <function>node-matches</function> function is an XSLT 2.0
<phrase xlink:href="http://www.w3.org/TR/xslt20/#stylesheet-functions">stylesheet
function</phrase>,
we’ll get to that in a minute. For any given DocBook element in the
titlepage parameter, we select the info elements from the source document that match it.
</simpara>
</callout>
<callout arearefs="co-named">
<simpara xml:id="p31">If the titlepage parameter element has a
<tag class="attribute">t:named-template</tag> attribute, then…
</simpara>
</callout>
<callout arearefs="co-force">
<simpara xml:id="p32">If some elements were selected, or it also has a non-zero
<tag class="attribute">t:force</tag> attribute, then…
</simpara>
</callout>
<callout arearefs="co-tp-templates">
<simpara xml:id="p33">We call the <code>titlepage-templates</code> template to process
it, passing
</simpara>
</callout>
<callout arearefs="co-this">
<simpara xml:id="p34">the current template node as a parameter.
</simpara>
</callout>
<callout arearefs="co-tp-mode">
<simpara xml:id="p35">Otherwise, we just process the <emphasis>source document</emphasis>’s
matching elements in <literal>titlepage.mode</literal>.
</simpara>
</callout>
</calloutlist>

<para xml:id="p36">The <function>node-matches</function> function looks like this:</para>

<programlistingco>
<areaspec>
<area xml:id="co-qname-matches" coords="6 75"/>
<area xml:id="co-for-each" coords="8 72"/>
<area xml:id="co-attr-match" coords="12 52"/>
<area xml:id="co-attr-not-match" coords="13 45"/>
<area xml:id="co-all-match" coords="19 56"/>
</areaspec>
<programlisting>&lt;xsl:function name="dbf:node-matches" as="xs:boolean"&gt;
  &lt;xsl:param name="template-node" as="element()"/&gt;
  &lt;xsl:param name="document-node" as="element()"/&gt;

  &lt;xsl:choose&gt;
    &lt;xsl:when test="node-name($template-node) = node-name($document-node)"&gt;
      &lt;xsl:variable name="attrMatch"&gt;
        &lt;xsl:for-each select="$template-node/@*[namespace-uri(.) = '']"&gt;
          &lt;xsl:variable name="aname" select="local-name(.)"/&gt;
          &lt;xsl:variable name="attr" select="$document-node/@*[local-name(.) = $aname]"/&gt;
          &lt;xsl:choose&gt;
            &lt;xsl:when test="$attr = ."&gt;1&lt;/xsl:when&gt;
            &lt;xsl:otherwise&gt;0&lt;/xsl:otherwise&gt;
          &lt;/xsl:choose&gt;
        &lt;/xsl:for-each&gt;
      &lt;/xsl:variable&gt;

      &lt;xsl:choose&gt;
        &lt;xsl:when test="not(contains($attrMatch, '0'))"&gt;
          &lt;xsl:value-of select="dbf:user-node-matches($template-node, $document-node)"/&gt;
        &lt;/xsl:when&gt;
        &lt;xsl:otherwise&gt;
          &lt;xsl:value-of select="false()"/&gt;
        &lt;/xsl:otherwise&gt;
      &lt;/xsl:choose&gt;
    &lt;/xsl:when&gt;
    &lt;xsl:otherwise&gt;
      &lt;xsl:value-of select="false()"/&gt;
    &lt;/xsl:otherwise&gt;
  &lt;/xsl:choose&gt;
&lt;/xsl:function&gt;</programlisting>

<calloutlist>
<callout arearefs="co-qname-matches">
<simpara xml:id="p46">Two nodes can only match if they have the same QName.
</simpara>
</callout>
<callout arearefs="co-for-each">
<simpara xml:id="p47">If they do, we have to loop through all the attributes on the template
node (that aren’t namespace qualified).
</simpara>
</callout>
<callout arearefs="co-attr-match">
<simpara xml:id="p48">If the document element has a corresponding attribute with the same
value, it matches.
</simpara>
</callout>
<callout arearefs="co-attr-not-match">
<simpara xml:id="p49">Otherwise, it doesn’t match.
</simpara>
</callout>
<callout arearefs="co-all-match">
<simpara xml:id="p50">If all the attributes that were specified did match, then we have
a matching node. Otherwise we don’t.
</simpara>
</callout>
</calloutlist>
</programlistingco>

<para xml:id="p38">If the node matches, we call <function>user-node-matches</function>,
which gives the customizer an opportunity to add additional selection criteria.
The default implementation of <function>user-node-matches</function> always
returns <code>true()</code>.</para>

<para xml:id="p44">The <code>titlepage-templates</code> template looks like this:</para>

<programlisting>&lt;xsl:template name="titlepage-templates"&gt;
  &lt;xsl:param name="side" tunnel="yes"/&gt;
  &lt;xsl:param name="node" tunnel="yes"/&gt;
  &lt;xsl:param name="info" tunnel="yes"/&gt;
  &lt;xsl:param name="template" tunnel="yes"/&gt;
  &lt;xsl:param name="content" tunnel="yes"/&gt;
  &lt;xsl:param name="this" tunnel="yes"/&gt;

  &lt;xsl:choose&gt;
    &lt;xsl:when test="$template = 'component.title'"&gt;
      &lt;xsl:call-template name="component.title"/&gt;
    &lt;/xsl:when&gt;
    &lt;xsl:otherwise&gt;
      &lt;xsl:call-template name="user-titlepage-templates"/&gt;
    &lt;/xsl:otherwise&gt;
  &lt;/xsl:choose&gt;
&lt;/xsl:template&gt;
</programlisting>

<para xml:id="p45">XSLT doesn’t allow template names to be dynamically constructed, so
our mechanism for calling the named template identified in
<tag class="attribute">t:named-template</tag> devolves into a big
<tag>xsl:choose</tag> statement. The
<code>user-titlepage-templates</code> is an extension point for
customizers that add a new name. The default implementation
of <code>user-titlepage-templates</code> terminates the
stylesheet.</para>

<para xml:id="p39">That’s the bulk of the work. Each template for a DocBook element in
“<literal>titlepage.mode</literal>” is passed three parameters: the
<parameter>side</parameter>, the <parameter>node</parameter>, and the
set of <parameter>info</parameter> elements.</para>

<para xml:id="p40">To see all these bits in action, run
<phrase xlink:href="examples/tp-test.xsl">this example</phrase> through
your favorite XSLT 2.0 processor. The input document is irrelevant,
it always uses an internal document as its test data.</para>

<para xml:id="p41">Disclaimer: the stylesheets, like DocBook itself, are very much a group
effort and these scribblings are just my initial thoughts; they are subject
to change. Though if they do change, I’ll try to write about that too.</para>

<para xml:id="p51">In terms of XSLT 2.0, I don’t know which is more interesting, the fact
that I used so few type-related features, or the fact that I used any at all.
The few that I did use could easily be removed.</para>

<para xml:id="p52">Could it be done in XSLT 1.0? Yes, probably, with the
<function>node-set</function> extension function and a fair bit of cleverness
to replace the <function>node-matches</function> stylesheet function.</para>

</essay>

