Template Languages in XSLT
March 27, 2002
Introduction
Despite its simplicity and its original purpose, XSLT is an extremely rich and powerful programming language. Just about anything that can be done with XML can be implemented in XSLT -- all it really takes is a little bit of creativity and a whole lot of pointy brackets.
One of the most common uses of XSLT is to transform XML content into something more suitable for viewing. This separation between content and presentation seems to be the most often cited advantage for many XML advocates. XSLT was designed specifically for this task
It could be argued, however, that, XSLT fails miserably at separating these two layers.
Traversing source documents with any sort of XPath or XSLT instructions like
xsl:for-each
and xsl:apply-templates
in your style sheets is
like opening a connection to a database and performing a query in the middle of an
ASP or
JSP page. Good programmers don't do this because it breaks the separation between
the
presentation and data tiers in their applications.
Thinking about it from an altogether different perspective, having literal result elements interspersed with XSLT instructions in your transforms is like generating HTML by concatenating strings and then printing them to your output (as is often done when implementing servlets). Most designers can't work in an environment like that. Even if they can, they shouldn't have to concern themselves with all the logic of extracting and manipulating the data they're trying to present.
Table of Contents |
No matter how you look at, you've already lost the separation you've been trying so hard to retain. If your style sheets are coupled to the structure of your source documents, even slight modifications to your vocabulary could require updates to each and every style sheet that operates on those documents.
Eric van der Vlist introduced the concept of "Style-free XSLT Style Sheets" in November, 2000. That article served as the inspiration for implementing my own XSLT-like template language (in XSLT) designed specifically for transforming instances of a particular vocabulary into whatever output format a designer could dream up without requiring any knowledge of XPath, XSLT, or the XML vocabulary they were transforming. This language included all of the traditional control flow constructs necessary to generate the desired output.
This article will show you how to implement your own specialized template languages by building up a simple example capable of transforming a music database in XML into any form of HTML.
Example 1. A sample source document (collection.xml)<collection> <owner> <name><given>Jason</given> <family>Diamond</family></name> (<email>jason@injektilo.org</email>) </owner> <album> <artist>Radiohead</artist> <title>OK Computer</title> </album> <album> <artist>Sigur Rós</artist> <title>Ágætis Byrjun</title> </album> <album> <artist>Mogwai</artist> <title>Kicking a Dead Pig</title> </album> </collection>
All of the files referenced in this article can be found in the associated archive. Included in that archive is a slightly more practical example of using the same template to process two different versions of RSS.
Literal Result Elements
Like XSLT, templates in our template language will be well-formed XML documents containing both literal result elements and instructions. Handling literal result elements is easy -- just copy them to the result tree.
Example 2. Our simple identity transform (transform1.xslt)
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:param name="template-uri" /> <xsl:variable name="template" select="document($template-uri)" /> <xsl:variable name="source" select="/" /> <xsl:template match="/"> <xsl:apply-templates select="$template/node()" /> </xsl:template> <xsl:template match="*"> <xsl:element name="{name()}"> <xsl:apply-templates select="@* | node()" /> </xsl:element> </xsl:template> <xsl:template match="@*"> <xsl:attribute name="{name()}"> <xsl:value-of select="." /> </xsl:attribute> </xsl:template> <xsl:template match="text()"> <xsl:if test="normalize-space()"> <xsl:value-of select="." /> </xsl:if> </xsl:template> </xsl:transform>
There's a couple of things to note about this example. First, this transform requires
a
parameter named template-uri
. This URI is used as the input to the
document
function to retrieve the actual contents of our template. We start
the transform off by processing the nodes in the template document and not the source document (although we do save the root of
the source document in the source
variable so that we can access it later).
With just a few small variations, the transform almost looks like the identity transform.
We're using the xsl:element
and xsl:attribute
instructions instead
of xsl:copy
since xsl:copy
automatically copies the namespace
nodes of the current node which would include the template language namespace declaration.
We're also only copying text nodes over when they don't consist of just whitespace,
purely
for aesthetic reasons.
In XSLT templates, literal result elements are the elements that do not belong to
the XSLT
namespace. Assuming the template language namespace name is
http://xml.com/my-template-language
namespace, the following template will
produce an identical result document (minus the namespace declaration).
Example 3. A useless template (template1.xml)
<html xmlns:my="http://xml.com/my-template-language"> <body> <h1>My Collection</h1> </body> </html>
Our template language is, so far, worthless. That's about to change with the addition of our first instruction.
Instructions
As the transform that implements your template language processes each element in your template, it needs to interpret certain elements as instructions. These are like XSLT instructions in that they perform some sort of predefined action rather than just being copied directly to your output. XSLT instructions can all be found in the XSLT namespace. Your template language's instructions should be identified using their own namespace.
For example, let's personalize your music collection template by displaying the owner's name in the output.
Example 4. A slightly more useful template (template2.xml)
<html xmlns:my="http://xml.com/my-template-language"> <body> <h1><my:owner-name />'s Collection</h1> </body> </html>
Implementing this and other instructions is as simple as adding a new XSLT template to the transform.
Example 5. Our first instructions (transform2.xslt)
<xsl:template match="my:owner-name"> <xsl:value-of select="$source/collection/owner/name" /> </xsl:template> <xsl:template match="my:owner-email"> <xsl:value-of select="$source/collection/owner/email" /> </xsl:template>
These templates have a higher default priority than the template that matched all elements in the previous section and so are always evaluated in preference to those templates. Notice that the XPath expressions used to extract the owner's name and email address are encapsulated in the transform and don't appear anywhere in the template.
Simple Conditionals (if)
Instructions can (and should) offer more than just simple data extraction. Template designers will often need to ask questions about the data they're trying to present.
Suppose that the template designer wishes to output the collection owner's email address
using a standard HTML address
element but only if the owner actually specified
an email address. If the designer can't ask this question, the output could possibly
include
an empty address
element (or worse).
Example 6. A template with logic (template3.xml)
<html xmlns:my="http://xml.com/my-template-language"> <body> <h1><my:owner-name />'s Collection</h1> <my:if-owner-has-email> <address> <my:owner-email />< /address> </my:if-owner-has-email> </body> </html>
Implementing the my:if-owner-has-email
instruction is easily achieved using
XSLT's xsl:if
instruction.
Example 7. Our first conditional (transform3.xslt)
<xsl:template match="my:if-owner-has-email"> <xsl:if test="$source/collection/owner/email"> <xsl:apply-templates /> </xsl:if> </xsl:template>
Note that the implementation of this instruction could actually be enhanced to check
for a
text node containing a '@' character rather than just checking for the presence of
the
email
element. This kind of logic should not be a concern to the template
designer.
Loops (for-each)
Since the output of most templates will probably consist of data displayed in some sort of repeating structure like an HTML list or table, our template language needs some sort of mechanism to iterate over the information in the source document.
Example 8. A template with a loop (template4.xml)
<html xmlns:my="http://xml.com/my-template-language"> <body> <h1><my:owner-name />'s Collection</h1> <h2>Albums</h2> <ul> <my:for-each-album> <li> <my:album-artist /> / <my:album-title /> </li> </my:for-each-album> </ul> </body> </html>
Just like the simple conditional instruction we implemented using xsl:if
, our
loops will be implemented using xsl:for-each
.
Example 9. The loop instruction (transform4.xslt)
<xsl:template match="my:for-each-album"> <xsl:variable name="current" select="." /> <xsl:for-each select="$source/collection/album"> <xsl:apply-templates select="$current/node()"> <xsl:with-param name="current-album" select="." /> </xsl:apply-templates> </xsl:for-each> </xsl:template>
Capturing the current node is necessary because xsl:for-each
is one of the few
XSLT instructions that change the current node. If this wasn't done, we'd be processing
the
child nodes of each album node in the source document when we invoked
xsl:apply-templates
instead of the child nodes of the
my:for-each-album
element in the template document.
Notice that the XSLT template that implements the my:for-each-album
instruction is passing the node that represents the current album to the XSLT templates
that
get evaluated next (whatever they may be). This is necessary for instructions like
my:album-artist
and my:album-title
if they're to extract the
artist and title for the correct album. The next XSLT template to get evaluated after
my:for-each-album
's is not my:album-artist
's or
my:album-title
's, however. Look closely at the template above. The
my:for-each-album
element contains a lone li
element. It's the
XSLT template that matches all elements that receives
the current-album
parameter and so that template will need to pass the
current-album
on to further XSLT templates.
Example 10. The modified identity template (transform4.xslt)
<xsl:template match="*"> <xsl:param name="current-album" /> <xsl:element name="{name()}"> <xsl:apply-templates select="@* | node()"> <xsl:with-param name="current-album" select="$current-album" /> </xsl:apply-templates> </xsl:element> </xsl:template>
Unfortunately, this means that you have to update the XSLT template that matches all elements for each type of loop you allow. For example, let's now add an instruction that allows the template designer to iterate over all of the unique artists in a collection.
Example 11. Another loop instruction (transform5.xslt)
<xsl:template match="my:for-each-artist"> <xsl:variable name="current" select="." /> <xsl:for-each select="$source/ collection/ album[not(artist = preceding-sibling::album/artist)]/ artist"> <xsl:apply-templates select="$current/node()"> <xsl:with-param name="current-artist" select="." /> </xsl:apply-templates> </xsl:for-each> </xsl:template>
The XSLT template that matches all elements needs to
be modified to accept and pass on the new current-artist
parameter.
Example 12. The modified (again) identity template (transform5.xslt)
<xsl:template match="*"> <xsl:param name="current-album" /> <xsl:param name="current-artist" /> <xsl:element name="{name()}"> <xsl:apply-templates select="@* | node()"> <xsl:with-param name="current-album" select="$current-album" /> <xsl:with-param name="current-artist" select="$current-artist" /> </xsl:apply-templates> </xsl:element> </xsl:template>
Let's add one last loop instruction before moving on. Now that we can iterate over the unique artists in a collection, it would be nice if we could iterate over the albums released by each of those artists. This would become a nested loop in our templates.
Example 13. A nested loop instruction (transform6.xslt)
<xsl:template match="my:for-each-album-by-artist"> <xsl:param name="current-artist" /> <xsl:variable name="current" select="." /> <xsl:for-each select="$source/ collection/ album[artist = $current-artist]"> <xsl:apply-templates select="$current/node()"> <xsl:with-param name="current-artist" select="$current-artist" /> <xsl:with-param name="current-album" select="." /> </xsl:apply-templates> </xsl:for-each> </xsl:template>
We do not need to add another parameter to the XSLT
template that matches all elements in this case because
it's already handling a current-album
parameter. Thus, we can also reuse
the my:album-title
instruction within this nested loop as well.
Advanced Conditionals (choose/when/otherwise)
Sometimes simple conditionals aren't enough. The template designer might want to perform an action if a certain condition is true or else perform a different action when it's not. In traditional programming languages, we could virtually always use some sort of if/else construct. Unfortunately, XML doesn't lend itself to making that easy to markup. This is why XSLT and our template language uses the more verbose choose/when/otherwise set of instructions.
As yet another contrived example, let's assume that, for the sake of brevity, a
collection's owner decided not to include a title
element for self-titled
albums (that is, albums where the title is equal to the artist name). For those albums,
the
template should output the artist name in place of the missing album title.
We could add a pair of extra if instructions (my:if-album-has-title
and
my:if-album-has-no-title
) but that would be unintuitive and could even get
cumbersome (especially when we need to handle more than one binary condition). Instead,
we
want our template to look like the following.
Example 14. choose/when/otherwise in action (template7.xml)
<html xmlns:my="http://xml.com/my-template-language"> <body> <h1><my:owner-name />'s Collection</h1> <h2>Albums</h2> <ul> <my:for-each-album> <li> <my:album-artist /> <my:text> / </my:text> <my:choose> <my:when-album-has-title> <my:album-title /> </my:when-album-has-title> <my:otherwise> <my:album-artist /> </my:otherwise> </my:choose> </li> </my:for-each-album> </ul> </body> </html>
This template also shows the my:text
instruction which, like
xsl:text
, simply copies its content to the output document while preserving
whitespace. It's also very helpful in eliminating mixed content.
Getting XSLT to test each choice until one evaluates to true required the use of a recursive named template. Fortunately, it only has to be implemented once no matter how many choices your template language offers. Adding new choices is simpler than adding new if instructions.
Example 15. Implementing when is easy (transform7.xslt)
<xsl:template match="my:when-album-has-title"> <xsl:param name="current-album" /> <xsl:value-of select="boolean($current-album/title)" /> </xsl:template>
The choose
XSLT template will check each when instruction for output equal to
"true" (xsl:value-of
implicitly converts the result of its select
expression to a string) and will evaluate the child nodes of the first when instruction
that
does, in fact, return "true". If none of the choices pass this test, the choose
template then evaluates the child nodes of the my:otherwise
instruction, if
any.
As an aside, checking to see if a title was missing and returning the artist name
if it was
would be more appropriately implemented by the my:album-title
instruction.
However, by giving the template designers the my:when-album-has-title
instruction, they have the ability to output something other than the artist name
(like
"Self-Titled", perhaps).
Instruction Parameters (attributes)
So far, our instructions have required no parameters but there's nothing preventing us from implementing the instructions so that they can take advantage of them. Like XSLT, our instructions can be parameterized via attributes.
For example, imagine we want to give the template designer the ability to sort the
list of
albums she iterates over. Using a sort-by
attribute, she could specify whether
she'd like the albums sorted by artist name or album title with the values "artist"
or
"title" (which are, conveniently, the names of the elements that hold that information).
Example 16. A parameterized sort instruction (transform8.xslt)
<xsl:template match="my:for-each-album[@sort-by]"> <xsl:variable name="sort-by" select="@sort-by" /> <xsl:variable name="current" select="." /> <xsl:for-each select="$source/collection/album"> <xsl:sort select="*[local-name() = $sort-by]" /> <xsl:apply-templates select="$current/node()"> <xsl:with-param name="current-album" select="." /> </xsl:apply-templates> </xsl:for-each> </xsl:template>
Unfortunately, XSLT doesn't allow the select
attribute to contain any sort of
attribute value template. Hence, we've been forced to duplicate the existing XSLT
template
that implemented my:for-each-album
instruction so that we can offer both the
default sort and the customized one. The default priority rules state that match patterns
containing a predicate have a higher priority than match patterns containing nothing
but a
QName so the correct XSLT template will be chosen automatically by the processor.
Attribute Value Templates
Attribute value templates in XSLT are the XPath expressions that appear in curly braces
in
attribute values. Without this extremely convenient shortcut, we'd be forced to use
the
xsl:attribute
instruction whenever we needed to dynamically compute an
attribute's value.
It would be nice if we could offer the same shortcut for our template language. Consider
the case of outputting a link to a collection owner's email address. We could do this
by
implementing an instruction that outputs an a
element with an href
attribute containing the owner's email address but that is problematic in several
ways.
First, it assumes that the template designer is outputting HTML. Second, it makes
it
impossible for the template designer to add other attributes to the a
element
(like id, class, or title). What we really need are attribute value templates.
Example 17. Attribute value templates (template9.xml)
<html xmlns:my="http://xml.com/my-template-language"> <body> <h1><my:owner-name />'s Collection</h1> <my:if-owner-has-email> <address> <a href="mailto:{$owner-email}"> <my:owner-email /> </a> </address> </my:if-owner-has-email> </body> </html>
We've borrowed the syntax from XSLT but that's not required since we can't really get an XSLT compliant processor to evaluate arbitrary XPath expressions. The only form of attribute value templates that our template language supports are simple variable substitutions like the above.
In order to perform the substitutions, we have to modify the XSLT template that matches all attributes to call a recursive named template. That template will scan the attribute value for "{$" and attempt to invoke the appropriate instruction based on the string found after "{$" and before "}".
Example 18. Implementing attribute value templates (transform9.xslt)
<xsl:template match="@*"> <xsl:attribute name="{name()}"> <xsl:call-template name="attribute"> <xsl:with-param name="value" select="string(.)" /> </xsl:call-template> </xsl:attribute> </xsl:template> <xsl:template name="attribute"> <xsl:param name="value" /> <xsl:choose> <xsl:when test="contains($value, '{$')"> <xsl:value-of select="substring-before($value, '{$')" /> <xsl:variable name="name" select="substring-before( substring-after($value, '{$'), '}')" /> <xsl:variable name="node" select="document('')/ xsl:transform/ my:*[local-name() = $name]" /> <xsl:choose> <xsl:when test="$node"> <xsl:apply-templates select="$node" /> </xsl:when> <xsl:otherwise> <xsl:value-of select="concat('{$', $name, '}')" /> </xsl:otherwise> </xsl:choose> <xsl:call-template name="attribute"> <xsl:with-param name="value" select="substring-after( substring-after( $value, '{$'), '}')" /> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$value" /> </xsl:otherwise> </xsl:choose> </xsl:template> <my:owner-email />
In order to try and make adding new attribute value template variables as easy as
possible,
the attribute
XSLT template looks for a node with the correct namespace name
and local name in the XSLT document itself. It only uses this node to get
xsl:apply-templates
to evaluate the correct XSLT template for our desired
variable/instruction.
For example, to add an $owner-name
attribute value template variable, all we
would need to do is add an empty my:owner-name
element to the transform. The
attribute
XSLT template would then use that to invoke the
my:owner-name
instruction without any requiring any other modifications.
Conclusion
Getting XSLT to process your custom templates isn't as easy as I would like it to be, but once the initial framework is created, adding new instructions and variables is relatively painless. Creating a prototype with XSLT is certainly the quickest way to go as you can easily add new instructions when your template designer needs them. I've personally used the techniques described in this article to prototype a template language with close to 200 instructions. The templates that utilized those instructions were still preferable to hardcoded XPath/XSLT, and it was possible to re-implement the template language processor in a more efficient language (a subject for another article) once the design was finalized without requiring any changes to the templates themselves.