Template Languages in XSLT
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.
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).
|
Related Reading
XSLT |
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.
|
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.
|
|
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.
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.
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).
|
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 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.
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.
XML.com Copyright © 1998-2006 O'Reilly Media, Inc.