XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

Template Languages in XSLT
by Jason Diamond | Pages: 1, 2, 3

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)

Comment on this articleShare your comments and questions about this article with the author and other readers in our forum.
Post your comments

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).

Pages: 1, 2, 3

Next Pagearrow