Supporting Multiple Formats In One XSLT File

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:

The original “RenderNewsList.xslt”

<?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:

  1. The $root and $newsRoot variables use “node”
  2. The root (match="/") template uses “node”
  3. The node template’s match attribute uses “node”
  4. The node template’s content uses data[@alias = '...']

How we’ll do it

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="node[@nodeTypeAlias = 'GuitarTAB'] | GuitarTAB"
select="$currentPage/*[@nodeTypeAlias = 'GuitarTAB' or self::GuitarTAB]"

So here we go, rewriting the patterns and expressions of the file:

Converting the variables

Variables fixed

<xsl:variable name="root" select="$currentPage/ancestor-or-self::*[@level = 1]" />
<xsl:variable name="newsRoot" select="$root/*[@nodeTypeAlias = 'Nyheder' or self::Nyheder]" />

The root template

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:

New version of the root template

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

The node template

First 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:

Dual format template matching

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

Rendering the properties

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:

Updated property accessors

<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:

The final cross-version compatible “RenderNewsList.xslt”

<?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 …


One more thing

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.

XSpec

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

The XSpec file for “RenderNewsList.xslt”

<?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:

File: “fixture_newslist_4-0.xml” (legacy-format)

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

File: “fixture_newslist_4-5.xml” (new schema)

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

First test run should fail

Since I started with the original file, the first test run should obviously fail for the second scenario, which it did:

Failed test in XSpec

Here’s a fail before the final change, with a diff for where the output is erroneous:

Failed test when href of link is generated empty

Finally, when everything has been converted to be version-independent, we have “green bars”:

All tests pass

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!