Menu

Template Languages in XSLT

March 27, 2002

Jason Diamond

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

Introduction

Literal Result Elements

Instructions

Simple Conditionals (if)

Loops (for-each)

Advanced Conditionals (choose/when/otherwise)

Instruction Parameters (attributes)

Attribute Value Templates

Conclusion

Bibliography

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&#243;s</artist>

                <title>&#193;g&#230;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.