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