Maintained by: David J. Birnbaum (djbpitt@gmail.com)
Last modified:
2022-03-13T21:51:33+0000
One advantage that XSLT provides over CSS is that while CSS can “decorate the tree,” XSLT
can rearrange it, fetching nodes from one place in the input tree and writing them
somewhere else in the output tree. A subtle side-benefit is that not only can XSLT move
a node to a new location, but it can output the same node in multiple locations and
treat it differently each time. For example, if your input document has chapters with
titles, your XSLT can create output that formats the titles and the chapters as they
might appear in a book, but it can also use the chapter titles again, differently, to
create a table of contents at the beginning of the output document. It does this by
using <xsl:apply-templates/>
more than once; in
the table of contents it applies templates just to the titles, while when it is
formatting the body it might apply templates to each chapter, and then, within each
chapter, apply templates first to the chapter title and then to the chapter body
contents.
Reusing the same input nodes more than once in the output raises the question of how you
might treat a node in different ways when you reuse it. For example, when you output the
actual chapters you might want to render the chapter titles as HTML
<h3>
elements before the chapter contents, so
that they look like large, bold chapter titles. In the table of contents, though, you
might want to create a bulleted list with an HTML
<ul>
element, where each chapter title gets
created inside the list as an HTML <li>
element.
How do you write template rules that can create an
<h3>
element when needed for a chapter title in
the body, and that can also create a <li>
element when needed for the same chapter title in a table of contents at the front of
the output document?
There are several possible solutions to the question of how to reuse parts of the input
tree to output them differently in different locations in the output tree, but the one
that is often most convenient is the XSLT @mode
attribute.
XSLT has a @mode
attribute that can appear on
<xsl:template>
and
<xsl:apply-templates>
elements. The
@mode
attribute can be used to create a separate
set of rules for processing titles in the table of contents that can operate alongside
the regular template rules that you might use to output the main text. For example,
suppose your input document is something like:
<report>
<chapter>
<title>This is the title of the first chapter</title>
<paragraph>This paragraph is the content of the first chapter. In
real life a chapter would probably have a lot of paragraphs.</paragraph>
</chapter>
<chapter>
<title>This is the title of the second chapter</title>
<paragraph>This paragraph is the content of the second chapter.</paragraph>
</chapter>
<chapter>
<title>This is the title of the third chapter</title>
<paragraph>This paragraph is the content of the third chapter.</paragraph>
</chapter>
<chapter>
<title>This is the title of the fourth chapter</title>
<paragraph>This paragraph is the content of the fourth chapter.</paragraph>
</chapter>
</report>
You already know how to generate HTML output from this input:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/1999/xhtml"
xmlns:math="http://www.w3.org/2005/xpath-functions/math" exclude-result-prefixes="#all"
version="3.0">
<xsl:output method="xhtml" html-version="5" omit-xml-declaration="no" include-content-type="no"
indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title>My output</title>
</head>
<body>
<xsl:apply-templates select="//chapter"/>
</body>
</html>
</xsl:template>
<xsl:template match="chapter">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="title">
<h3>
<xsl:apply-templates/>
</h3>
</xsl:template>
<xsl:template match="paragraph">
<p>
<xsl:apply-templates/>
</p>
</xsl:template>
</xsl:stylesheet>
The <xsl:template>
and
<xsl:apply-templates>
elements in this
stylesheet have no @mode
attribute. You can now
augment the stylesheet by adding additional rules that specify a
@mode
attribute; you can name the mode anything you
want, and I normally use “toc” for tables of contents.
The rules you add to your stylesheet are highlighted below:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/1999/xhtml"
xmlns:math="http://www.w3.org/2005/xpath-functions/math" exclude-result-prefixes="#all"
version="3.0">
<xsl:output method="xhtml" html-version="5" omit-xml-declaration="no" include-content-type="no"
indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title>My output</title>
</head>
<body>
<h2>Contents</h2>
<ul>
<xsl:apply-templates select="//title" mode="toc"/>
</ul>
<hr/>
<xsl:apply-templates select="//chapter"/>
</body>
</html>
</xsl:template>
<xsl:template match="chapter">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="title">
<h2>
<xsl:apply-templates/>
</h2>
</xsl:template>
<xsl:template match="paragraph">
<p>
<xsl:apply-templates/>
</p>
</xsl:template>
<xsl:template match="title" mode="toc">
<li>
<xsl:apply-templates/>
</li>
</xsl:template>
</xsl:stylesheet>
Where we want to generate the table of contents (in this case, before the actual
contents), we create the <ul>
container that
will hold the list of chapter titles. Inside it we apply templates to all of the titles
as a way of rounding them up for processing, but when we apply templates there, we
specify mode="toc"
. The presence of the
@mode
attribute on the
<xsl:apply-templates>
element tells the system
to ignore any template rule for <title>
nodes
that does not specify the same value for the @mode
attribute. The system therefore ignores the regular (modeless) rule that we wrote for
titles, and chooses instead the new one (with
mode="toc"
), which specifies that titles should be
wrapped <li>
tags.
Meanwhile, when we later process titles a second time, to render each title as an HTML
<h2>
before the chapter contents in the body of
the output, the old rule applies. The new, modal rules don’t affect the logic of the
original stylesheet; our old, modeless rules continue to work as before, but they are
now augmented by modal rules that let us also generate a table of contents that treats
titles differently from the way they’re treated in the main output.
You can have as many modes as you need in a stylesheet and you can call them anything you want. Note also that you can call a template that is in one mode from inside a template that is in a different mode. In the example above, the template rule for titles that is in the “toc” mode applies templates to the contents of each title, that is, to the text nodes that contain the actual title text, and it doesn’t specify a mode. We can use the regular (modeless and built-in) template rule in this case because we don’t need to process the text any differently than we do in the body of the output document. The point is that there is no prohibition against calling a modeless template from inside a modal one or vice versa; whether you specify a mode depends only on the processing you need at that point in the transformation.
The most common mistake people make with modal XSLT is forgetting to specify the mode
value when needed. We have mentioned before that we often think of XSLT as involving
throwing and catching, where
<xsl:apply-templates>
throws nodes out into the
ether and template rules sit around waiting to catch them as they come by. If you have a
rule like
<xsl:apply-templates select="//title" mode="toc"/>
,
it throws the <title>
elements out, but it
specifies that they can be caught only by a template rule that not only matches the
correct GI (GI
is the standard abbreviation for generic identifier,
which
is XML-speak for what humans would call an element name
) with
match="title"
, but also invokes the correct mode
with mode="toc"
.