The news from the XSLT front of late has been very good, with the release of the XSLT 2.0 standard, XPath 2 and XQuery 1 - and I just found another hopeful sign in this post from Mozilla’s Jonas Sicking:

We now have code checked in to support some parts of EXSLT
These functions will be supported in the upcoming Firefox 3
release (sorry, no chance of backporting to earlier releases)

exsl:node-set
exsl:object-type
regexp:test
regexp:match
regexp:replace
set:difference
set:distinct
set:intersection
set:distinct
set:has-same-node
set:leading
set:trailing
str:tokenize
str:concat
str:split
math:min
math:max
math:highest
math:lowest

Mozilla Firefox has definitely become my favorite browser, but I have to admit that I’ve long been frustrated that the browser was hampered with such a limited XSLT processor. Perhaps the chief complaint I’ve had comes from the lack of the node-set() method in XSLT’s XPath. Node-set() isn’t in XSLT 1 and it isn’t in XSLT 2, but for completely different reasons.

In XSLT1, there was a fundamental notion that XSLT should be completely side-effect free, to the extent that you couldn’t create intermediate XML to be processed by other templates, but instead had to live with “XML Fragments”. In XSLT2, the underlying data model was revamped to the extent, including the introduction of sequences (more about that in an upcoming post) and one immediate consequence of this was that you could get intermediate XML creation largely for free.

node-set() is an evolutionary step between 1 and 2, however, and was perhaps one of the biggest driving factors in the establishment of the EXSLT library. The nodeset method takes a string representation of a well-formed XML fragment and converts it into a nodeset that can then be assigned to a variable or processed in an apply-templates or for-each statement. This lack was realized early on by Microsoft in their browser, and the Saxon 6.x libraries included an equivalent statement, to the extent that by the early years of this decade the node-set() function was considered an “unofficial” but established XPath method.

The XSLT library in Mozilla, unfortunately, was based upon the early Transformiix library, which didn’t in fact support this capability, and there has long been a curious reticence on the part of the Mozilla development team to dig into the processor to upgrade it even to EXSLT recommendations. This has had the effect of keeping all but the simplest transformations off of the Mozilla platform, a pity given the otherwise superb XML support. As a point of note, the Safari and KDE Konqueror XSLT processors are both built upon Daniel Veillard’s superb libXSLT engine, which was a key participant in the EXSLT recommendations in the first place. (I believe that Opera’s XSLT processor is native to that particular product, but I don’t know for sure - ping me if you know, as I’d like to find out).

While not as elegant as XSLT 2, support for node-set() will make a key difference in what can be done in Mozilla, largely because it makes it possible to more efficiently pipeline transformation processes so that they stay within the context of a single transformation by permitting intermediate processing inline. This is especially true when combined with the str:tokenize() method, which, when passed a string and a parameter, generates a node-set of <token> elements that contain the items of the parsed string.

For instance, suppose that you had a calendar with the input being two sets of strings, being the comma delimited lists of the long and short (three letter) names of months. The following code should work, but doesn’t:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:str="http://exslt.org/strings"
    xmlns:exsl="http://exslt.org/common"
    extension-element-prefixes="str exsl"
    >
    <xsl:output method="xml" media-type="text/xhtml" indent="yes" omit-xml-declaration="yes"/>
    <xsl:param name="monthLongNames"
    select="'January,February,March,April,May,June,July,August,September,October,November,December'"/>
    <xsl:param name="monthShortNames"
    select="'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'"/>
    <xsl:variable name="months">
        <xsl:variable name="longNames" select="str:tokenize($monthLongNames,',')"/>
        <xsl:variable name="shortNames" select="str:tokenize($monthShortNames,',')"/>
        <xsl:for-each select="$longNames">
            <xsl:variable name="pos" select="position()"/>
            <month number="{position()}" shortName="{$shortNames[ position() = $pos]}"
longName="{.}"/>
        </xsl:for-each>
    </xsl:variable>
    <xsl:template match="/">
        <months><xsl:copy-of select="exsl:node-set($months)/*[position() =5]"/></months>
        <html>
            <head>
                <title>node-set() Test</title>
            </head>
            <body>
                <table border="1">
                    <tr>
                        <xsl:for-each select="$months/*">
                            <xsl:variable name="month" select="."/>
                            <th><xsl:value-of select="$month/@shortName"/></th>
                        </xsl:for-each>
                    </tr>
                </table>
            </body>
        </html>
    </xsl:template>
</xsl:stylesheet>

The reason for this is in the statement:

                        <xsl:for-each select="$months/*">

In this case, the pure XSLT 1.0 processor sees $months as containing an XML Fragment, not a collection of nodes, and so the statement $months/* is meaningless. However, using the exsl:node-set() function you can easily fix this particular transformation:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:str="http://exslt.org/strings"
    xmlns:exsl="http://exslt.org/common"
    extension-element-prefixes="str exsl"
    >
    <xsl:output method="xml" media-type="text/xhtml" indent="yes" omit-xml-declaration="yes"/>
    <xsl:param name="monthLongNames"
    select="'January,February,March,April,May,June,July,August,September,October,November,December'"/>
    <xsl:param name="monthShortNames"
    select="'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'"/>
    <xsl:variable name="months">
        <xsl:variable name="longNames" select="str:tokenize($monthLongNames,',')"/>
        <xsl:variable name="shortNames" select="str:tokenize($monthShortNames,',')"/>
        <xsl:for-each select="$longNames">
            <xsl:variable name="pos" select="position()"/>
            <month number="{position()}" shortName="{$shortNames[ position() = $pos]}"
longName="{.}"/>
        </xsl:for-each>
    </xsl:variable>
    <xsl:template match="/">
        <html>
            <head>
                <title>node-set() Test</title>
            </head>
            <body>
                <table border="1">
                    <tr>
<xsl:for-each select="exsl:node-set($months)/*">
<xsl:variable name="month" select="."/> <th><xsl:value-of select="$month/@shortName"/></th> </xsl:for-each> </tr> </table> </body> </html> </xsl:template> </xsl:stylesheet>

which generates the expected output:

<html>
    <head>
        <title>node-set() Test</title>
    </head>
    <body>
        <table border="1">
            <tr>
                <th>Jan</th>
                <th>Feb</th>
                <th>Mar</th>
                <th>Apr</th>
                <th>May</th>
                <th>Jun</th>
                <th>Jul</th>
                <th>Aug</th>
                <th>Sep</th>
                <th>Oct</th>
                <th>Nov</th>
                <th>Dec</th>
            </tr>
        </table>
    </body>
</html>

The other functions, especially the regular expression commands regexp:test,regexp:match
,
and regexp:replace, are a nice bonus feature, and perhaps with enough feedback it might be possible to see the rest of the EXSLT library end up in the Mozilla 3.0 trunk by the time it debuts this summer, but I have to admit to being very happy with the announcement on node-set() - it will significantly broaden the transformative aspect of XSLT in Firefox.

Kurt Cagle is a software consultant, analyst, and author specializing in XML, AJAX and web technologies. He currently lives in Victoria, BC, Canada, which is experiencing its first real sunshine since October.