Written by Chriztian Steinmeier.
Got comments? I’m @greystate on Twitter.
If you’re creating an Umbraco package, and you’d like it to work with both the “legacy” XML schema and the new 4.5 schema, this is the article you want to read. I’ll show you how to write your XSLT file to support both formats within the same file.
If you’re not familiar with the “match templates” approach to XSLT (as opposed to “for-each”) I suggest you swing by that article first, to fully understand what I’m doing here (and why).
Once again, I’ll be refactoring an XSLT file (this time from a real project I worked on) that renders a list of News items:
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:umb="urn:umbraco.library"
exclude-result-prefixes="umb"
>
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:param name="currentPage" />
<xsl:variable name="root" select="$currentPage/ancestor-or-self::node[@level = 1]" />
<xsl:variable name="newsRoot" select="$root/node[@nodeTypeAlias = 'Nyheder']" />
<xsl:template match="/">
<ul>
<xsl:apply-templates select="$newsRoot/node" />
</ul>
</xsl:template>
<xsl:template match="node">
<li>
<h4>
<xsl:value-of select="data[@alias = 'propertyNewsHeader']" />
</h4>
<p>
<xsl:value-of select="data[@alias = 'propertyNewsSummary']" />
</p>
<p>
<a href="{umb:NiceUrl(data[@alias = 'propertyNewsLink'])}">Læs mere</a>
</p>
</li>
</xsl:template>
</xsl:stylesheet>
To recap — going from 4.0 to 4.5, all references to node
should be changed to the value of the @nodeTypeAlias
attribute
or, if it needs to be more generic, the selector *[@isDoc]
. Also, data[alias = 'propertyName']
should be changed to just propertyName
to
work in the new schema.
So we need to address the following:
$root
and $newsRoot
variables use “node”match="/"
) template uses “node”data[@alias = '...']
Because the two XML formats have virtually no overlap, we can be pretty smart about this—e.g., take a statement like this in a legacy-format XSLT file:
<xsl:apply-templates select="$currentPage/node[@nodeTypeAlias = 'GuitarTAB']" />
In the new format, this would become:
<xsl:apply-templates select="$currentPage/GuitarTAB" />
If we want to support both formats in the same file, we could actually just apply both in succession:
<xsl:apply-templates select="$currentPage/node[@nodeTypeAlias = 'GuitarTAB']" />
<xsl:apply-templates select="$currentPage/GuitarTAB" />
(Technically, they will both be executed but only the one that works with the format being used will output anything)
We can collapse the two into one line if we like:
<xsl:apply-templates select="$currentPage/GuitarTAB | $currentPage/node[@nodeTypeAlias = 'GuitarTAB']" />
OR, we can do another little XPath thingy-ma-jiggery:
<xsl:apply-templates select="$currentPage/*[@nodeTypeAlias = 'GuitarTAB' or self::GuitarTAB]" />
Pick whichever technique that looks good to you - my reasoning goes something like this:
select="GuitarTAB"
, I’ll go with combining sets:select="node[@nodeTypeAlias = 'GuitarTAB'] | GuitarTAB"
select="$currentPage/*[@nodeTypeAlias = 'GuitarTAB' or self::GuitarTAB]"
So here we go, rewriting the patterns and expressions of the file:
$root
variable is very easy to convert: we’ll just replace “node” with an asterisk, since only Documents have the level attribute
(and we’re searching the ancestor tree of $currentPage
which, can only contain Documents)newsRoot
variable can be converted by adding or self::Nyheder
to the predicate<xsl:variable name="root" select="$currentPage/ancestor-or-self::*[@level = 1]" />
<xsl:variable name="newsRoot" select="$root/*[@nodeTypeAlias = 'Nyheder' or self::Nyheder]" />
The original root template just creates an unordered list and then applies templates to the childnodes of the $newsRoot page—again, we’ll substitute an asterisk for “node” and call it a day:
<xsl:template match="/">
<ul>
<xsl:apply-templates select="$newsRoot/*" />
</ul>
</xsl:template>
(Of course, this works closely in tandem with the fact that ‘Nyheder’ is set up to only allow one specific type of children)
node
templateFirst of all, we need to change the match attribute, as only the old format contains “node” elements, so let’s add its nodeTypeAlias to the match pattern:
<xsl:template match="node | NewsItem">
...
</xsl:template>
The template will now work for either format, so let’s look inside and see if we can get the final bits to comply.
Getting to a property in a cross-format way is done by combining the old syntax with the new one in a set, so:
<xsl:value-of select="data[@alias = 'numberOfTappingFingersAvailable']" />
- becomes:
<xsl:value-of select="data[@alias = 'numberOfTappingFingersAvailable'] | numberOfTappingFingersAvailable" />
Again, this works because the new format won’t have an element named “data”, and the legacy-format doesn’t have custom-named elements, so only one of them will actually return a node to take the value of.
Updating the contents of the template gives us this:
<li>
<h4>
<xsl:value-of select="data[@alias = 'propertyNewsHeader'] | propertyNewsHeader" />
</h4>
<p>
<xsl:value-of select="data[@alias = 'propertyNewsSummary'] | propertyNewsSummary" />
</p>
<p>
<a href="{umb:NiceUrl(data[@alias = 'propertyNewsLink'] | propertyNewsLink)}">Læs mere</a>
</p>
</li>
And then the complete file for reference:
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:umb="urn:umbraco.library"
exclude-result-prefixes="umb"
>
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:param name="currentPage" />
<xsl:variable name="root" select="$currentPage/ancestor-or-self::*[@level = 1]" />
<xsl:variable name="newsRoot" select="$root/*[@nodeTypeAlias = 'Nyheder' or self::Nyheder]" />
<xsl:template match="/">
<ul>
<xsl:apply-templates select="$newsRoot/*" />
</ul>
</xsl:template>
<xsl:template match="node | NewsItem">
<li>
<h4>
<xsl:value-of select="data[@alias = 'propertyNewsHeader'] | propertyNewsHeader" />
</h4>
<p>
<xsl:value-of select="data[@alias = 'propertyNewsSummary'] | propertyNewsSummary" />
</p>
<p>
<a href="{umb:NiceUrl(data[@alias = 'propertyNewsLink'] | propertyNewsLink)}">Læs mere</a>
</p>
</li>
</xsl:template>
</xsl:stylesheet>
That’s it! We’re done. Oh wait …
I thought I’d back this up with a little extra for the die-hard XSLT fans out there (looking at @leekelleher, @warrenbuckley, @drobar, @cultiv & countless others, who’ve been flooding me with great comments, feedback, praise and the general feel-good-ness of The Umbraco Spirit that I’ve come to love so much), so here’s how I Unit-Tested my way through this stylesheet.
Did you just mention “Unit Test” in the same sentence as “XSLT”?
Yes :-)
OK, I always knew I’d want to create two test documents; one in the legacy XML format and another one in the new format.
I would run the stylesheet on each file to make sure that what I wrote was actually working. Automation is King,
so I dug out my copy of Jeni Tennison’s excellent XSpec project,
ran svn update
to get the latest, and checked the shortcut I’d assigned to the TextMate command
I’d created long ago… ready, set, go.
No boring setup steps or configuration recipes here—e-mail me if you’re trying it out and you’re having trouble—here’s the XSpec file (don’t mind the danish stuff—think of it as Lorem Ipsum :-)
<?xml version="1.0" encoding="utf-8"?>
<x:description
stylesheet="../RenderNewsList.xslt"
xslt-version="1.0"
xmlns:x="http://www.jenitennison.com/xslt/xspec"
>
<x:scenario label="when processing the fixture file for 4.0">
<x:context href="file:///Users/chriz/Development/XSLT/RenderNewsList-conversion/test/fixture_newslist_4-0.xml" select="/">
<x:param name="newsRoot" select="document('file:///Users/chriz/Development/XSLT/RenderNewsList-conversion/test/fixture_newslist_4-0.xml')/root/node/node[@nodeTypeAlias = 'Nyheder']" />
</x:context>
<x:expect label="it should produce a list of three NewsItems">
<ul>
<li>
<h4>Dette er nyhed #1</h4>
<p>Der er et par ting vi skal vide om...</p>
<p>
<a href="1201">Læs mere</a>
</p>
</li>
<li>
<h4>Dette er nyhed #2</h4>
<p>Der er et par ting vi skal vide om...</p>
<p>
<a href="1202">Læs mere</a>
</p>
</li>
<li>
<h4>Dette er nyhed #3</h4>
<p>Der er et par ting vi skal vide om...</p>
<p>
<a href="1203">Læs mere</a>
</p>
</li>
</ul>
</x:expect>
</x:scenario>
<x:scenario label="when processing the fixture file for 4.5">
<x:context href="file:///Users/chriz/Development/XSLT/RenderNewsList-conversion/test/fixture_newslist_4-5.xml" select="/">
<x:param name="newsRoot" select="document('file:///Users/chriz/Development/XSLT/RenderNewsList-conversion/test/fixture_newslist_4-5.xml')/root/Site/Nyheder" />
</x:context>
<x:expect label="it should produce a list of three NewsItems">
<ul>
<li>
<h4>Dette er nyhed #1</h4>
<p>Der er et par ting vi skal vide om...</p>
<p>
<a href="1201">Læs mere</a>
</p>
</li>
<li>
<h4>Dette er nyhed #2</h4>
<p>Der er et par ting vi skal vide om...</p>
<p>
<a href="1202">Læs mere</a>
</p>
</li>
<li>
<h4>Dette er nyhed #3</h4>
<p>Der er et par ting vi skal vide om...</p>
<p>
<a href="1203">Læs mere</a>
</p>
</li>
</ul>
</x:expect>
</x:scenario>
</x:description>
- and these are the two test files or “fixtures” as I call them:
<root id="-1">
<node level="1" nodeTypeAlias="Site">
<node nodeTypeAlias="Nyheder">
<node nodeTypeAlias="NewsItem">
<data alias="propertyNewsHeader">Dette er nyhed #1</data>
<data alias="propertyNewsSummary">Der er et par ting vi skal vide om...</data>
<data alias="propertyNewsLink">1201</data>
</node>
<node nodeTypeAlias="NewsItem">
<data alias="propertyNewsHeader">Dette er nyhed #2</data>
<data alias="propertyNewsSummary">Der er et par ting vi skal vide om...</data>
<data alias="propertyNewsLink">1202</data>
</node>
<node nodeTypeAlias="NewsItem">
<data alias="propertyNewsHeader">Dette er nyhed #3</data>
<data alias="propertyNewsSummary">Der er et par ting vi skal vide om...</data>
<data alias="propertyNewsLink">1203</data>
</node>
</node>
</node>
</root>
<root id="-1">
<Site isDoc="" level="1">
<Nyheder isDoc="">
<NewsItem isDoc="">
<propertyNewsHeader>Dette er nyhed #1</propertyNewsHeader>
<propertyNewsSummary>Der er et par ting vi skal vide om...</propertyNewsSummary>
<propertyNewsLink>1201</propertyNewsLink>
</NewsItem>
<NewsItem isDoc="">
<propertyNewsHeader>Dette er nyhed #2</propertyNewsHeader>
<propertyNewsSummary>Der er et par ting vi skal vide om...</propertyNewsSummary>
<propertyNewsLink>1202</propertyNewsLink>
</NewsItem>
<NewsItem isDoc="">
<propertyNewsHeader>Dette er nyhed #3</propertyNewsHeader>
<propertyNewsSummary>Der er et par ting vi skal vide om...</propertyNewsSummary>
<propertyNewsLink>1203</propertyNewsLink>
</NewsItem>
</Nyheder>
</Site>
</root>
(Yes, I created the second one by transforming the first with this stylesheet)
Since I started with the original file, the first test run should obviously fail for the second scenario, which it did:
Here’s a fail before the final change, with a diff for where the output is erroneous:
Finally, when everything has been converted to be version-independent, we have “green bars”:
If you’re still here, I actually think you rock.
Now, I don’t do all of my XSLT development this way, but this was such a clear-cut case for dusting off XSpec that I couldn’t resist. Maybe you’ve been intrigued to try it for a project some time? You might actually like it.
Really, this is the end—see you next time!