XSLT Basics (& Qt Bugs)

Introduction

The Layout is implemented using XML data from the database, variables from the running process, and a set of XSLT define in your layouts and internal extraneous XSLT definitions as offered by the core and different extensions.

XSLT is used to transform the XML data into an actual page that can be display in a website.

This page gives some information about XSLT and different features available in that language.

[Note: I'm writing this before doing the actual implementation of the data and therefore it is likely to change by the time it is fully implemented.]

At the bottom I include a set of bugs found in the Qt version. It is important to be aware that the XSLT implementation of Qt is not perfect.

The Basics

The XSLT format is using XML as its base format. The language is defined using the namespace named xsl. So XSLT instructions are all using xsl:<name>. For example, to display a variable in your output, you use <xsl:value-of select="$varname"/>.

<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="snap">
        <html>
            <xsl:apply-templates/>
        </html>
    </xsl:template>
    <xsl:template name="body" match="snap/body">
        <body>
            <xsl:attribute name="class">xslt-generated</xsl:attribute>
            <xsl:for-each select="part">
                <div><xsl:copy-of select="part"/></div>
            </xsl:for-each>
        </body>
    </xsl:template>
</xsl:stylesheet>

This XSLT example shows you how to retrieve all the <part> tags of your input file and add them to the output, putting each one of them in a <div> tag.

As we can see, the main template includes all the others inside <html> ... </html> and the body is put inside <body> ... </body> delimiters.

matching path

Templates and select attributes are used to match the path of the node you are trying to access.

By default, the path is composed of node names:

snap/head/metadata

Select all the nodes in <snap><head><metadata>...</metadata></head></snap>

It is possible to check attributes using square brackets and the @ character (an abbreviation of attribute::). Attributes are either checked against a value or when just their name are defined for their presence.

snap/head/metadata/desc[@type='name']/data

Match the <data> tag defined in a <desc> tag that has a type attribute set to the value "name" (i.e. against ...<desc type="name"><data>Foo</data></desc>... we get Foo.) @type='name' could also be written attribute::type="name".

By default, a name in a path is viewed as a child:

child::<name>

The opposite is the parent (parent::<name>). Note that the parent of an attribute is its node even though it is not exactly a parent as per the XML reference.

You can access all the descendants (descendant::<name>) and all the ancestors (ancestor::<name>).

You can refer the node itself (self::<name>), any descendant including the node itself (descendant-or-self::<name>), or any ancestor including the node itself (ancestor-or-self::<name>).

A sibling that follows the current node (following-sibling::<name>), that precedes the current node (preceding-sibling::<name>), the next node (following::<name>), or the previous node (preceding::<name>).

xs and fn namespace specifications

To access functions such as xs:decimal() you have to add the following xmlns specification:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                              xmlns:xs="http://www.w3.org/2001/XMLSchema">

It is also possible to add the fn namespace although though functions are available as is too.

  xmlns:fn="http://www.w3.org/2005/xpath-functions/"

The documentation is likely to show functions called as fn:position() so having the definition can help avoid problems.

Defining and Using Global Parameters

The Core layout manager and some plugins will add parameters to the QXmlQuery object. For example, the core defines the year when the page being displayed was created in the Cassandra database:

query.bindVariable("year_created", QVariant(2010));

In your XSLT code, you can access that variable using the dollar sign:

<p>This page was created in <xsl:value-of select="$year_created"/>.</p>

Localized parameters, defined within an <xsl:template> tag, can be used the same way. It can be quite useful if the same heavy computation would otherwise be repeated many times.

To use a variable inside an attribute, you want to use the curly brackets trick as follow:

<p class="year-{$year}">This text class and thus CSS changes at midnight on Jan 1</p>

Actually, the use of curly brackets allows you to write complete expressions. For example, to show only the century, you could use {$year idiv 100}.

Sorting

It is possible to sort sequences. When you match a set of nodes, it is called a sequence.

The sort can use a wide range of parameters for the sort. For example, to sort using the value of an attribute, you'd use @<attribute-name>:

<xsl:sort select="@priority" data-type="number"/>

Note that some functions, such as xs:decimal() may not be available in your template because it was not included. The data-type can be used to sort numbers though (data is sorted as strings by default.)

The sort has to be placed right after the <xsl:for-each ...> tag.

WARNING: Sorting and Position Conflict

When sorting a sequence of nodes, the fn:position() function continues to return the position of the original node and not the position of the sorted sequence. In other words, the fn:position() function is likely NOT going to be in order and using that value for tricks such as defining a class such as "odd" or "even" will fail (see Known Bugs below.)

Conditions

It is possible to test whether something is defined or not and react accordingly1.

Here is an example adding a <div> only if the second column is defined.

<xsl:if test="column2 != ''">
    <div class="column"><xsl:copy-of select="column2"/></div>
</xsl:if>

Note that there are no <xsl:else> or <xsl:elif> because that breaks a standard XML tree and you cannot always use the opposite condition (<xsl:if test="column2 = ''"> is not exactly the opposite of <xsl:if test="column2 != ''">). To obtain the equivalent of an else, make use of the <xsl:choose> with a <xsl:otherwise> tags instead:

<xsl:choose>
    <xsl:when test="column2 != ''">
        <div class="column"><xsl:copy-of select="column2"/></div>
    </xsl:when>
    <xsl:otherwise>
        <div class="column"> -- a default column 2 value -- </div>
    </xsl:otherwise>
</xsl:choose>

Known Bugs and Issues in QXmlQuery

The following are problems to be aware of when you write your XSLT files because these things do not work in the current implementation (At this point this is being tested with Qt 4.7).

Sorting and position() [Qt 4.7]

Contrary to what would be expected, the position() value is not sequencial in an XSLT sequence that was sorted because position() of each item does not change whether it got sorted or not.

Element Sorted Elements XSLT proper
position()
QXmlQuery
position()
A B 1 2
B C 2 3
C A 3 1

This means you cannot make use of the position() in a sorted list (i.e. to know about the first and last elements, to define odd/even classes, etc.)

This should rarely be an issue since in most cases elements will already be sorted as required by the end user when your XSLT is getting processed.

Global Variables and Functions [Qt 4.7]

If you define a variable in the global space and then attempt to call a function with that variable, then the empty sequence is passed to the function (i.e. equivalent to NULL.)

<xsl:variable name="global_var" select="this value"/>
<xsl:function name="snap:my_func"><xsl:param name="g"/>...</xsl:function>
<xsl:template match="snap">
  <xsl:value-of select="my_func($global_var)"/>
</xsl:template>

The call to my_func() defines parameter g with NULL. Try to use that parameter in the function fails. To circumvent the problem, you have to access the global variable at least once:

[...snip...]
<xsl:template match="snap">
  <xsl:if test="$global_var"></xsl:if><!-- void statement accessing $global_var -->
[...snip...]

Now the call to my_func() works as expected!

Note: somehow this now works fine in the XSLT I'm working on, without the extra definitions.

xsl:for-each and xsl:choose [Qt 4.7]

When using the xsl:for-each, as little as possible should appear in the loop or the position() and all the content will be set to the content of the first item matched!

The easiest way to fix this problem is to use a named template. In that case, the correct data is propagated. At times, however, placing an xsl:variable definition right after the xsl:for-each seems to fix the problem.

First the failing XSLT code:

<xsl:for-each select="page/body/bookmarks/link">
    <xsl:variable name="rel" select="@rel"/>
    <xsl:variable name="title" select="@title"/>
    <xsl:variable name="href" select="@href"/>
    <xsl:choose>
        <xsl:when test="$title">
            <link rel="{$rel}" title="{$title}" href="{$href}"/>
        </xsl:when>
        <xsl:otherwise>
            <link rel="{$rel}" href="{$href}"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:for-each>

The solution is to call a template setting parameters to the corresponding fields of interest.

<xsl:for-each select="page/body/bookmarks/link">
    <xsl:call-template name="bookmark-link">
        <xsl:with-param name="rel" select="@rel"/>
        <xsl:with-param name="href" select="@href"/>
        <xsl:with-param name="title" select="@title"/>
    </xsl:call-template>
</xsl:for-each>

<xsl:template name="bookmark-link">
    <xsl:param name="rel"/>
    <xsl:param name="href"/>
    <xsl:param name="title"/>
    <xsl:choose>
        <xsl:when test="$title">
            <link rel="{$rel}" title="{$title}" href="{$href}"/>
        </xsl:when>
        <xsl:otherwise>
            <link rel="{$rel}" href="{$href}"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

This is a little more work, but the result is exactly what you would expect.

References

The different references used to learn more about XSLT:

XSLT 2.0 — http://www.w3.org/TR/xslt20/

XML Path — http://www.w3.org/TR/xpath20/

XML Path Functions — http://www.w3.org/TR/xpath-functions/

XML Query — http://www.w3.org/TR/xquery/

  • 1. Obviously, XSLT makes use of XPath which has a match capability that can already be viewed as a conditional selection. However, in some circumstances, using a simple if or a choose function makes things a lot easier.

Snap! Websites
An Open Source CMS System in C++

Contact Us Directly