alex.jordan
alex.jordan

Reputation: 368

XSLT discrepancy with how variable is used

My question is about xsl:variable and the syntax for a predicate in an Xpath. I've boiled down my question to the point where this short XML can help me demonstrate:

<root>
  <tabular>
    <col halign="left"/>
    <col halign="right"/>
    <row>
      <cell>Some content</cell>
      <cell>Some content</cell>
    </row>
  </tabular>
</root>

In my application, when I am applying a template on a cell, I need to access the @halign of the corresponding col. In doing so, I have encountered a discrepancy between Xpath expressions that I thought should be equivalent. I would like to understand why this happens. To demonstrate, I apply the XSL at the end of this post using XSLT 1.0.

The cell template in my XSLT here is silly but it lays out the discrepancy I don't understand. Basically it repeatedly tries to print the @halign value corresponding to the second cell. First, using the $col variable that has value 2. Then using [position()=$col]. Then using [number($col)]. Then simply using [2], hard coded. Lastly, using a separate $colsel variable that was defined using a @select attribute.

I expect to see:

ancestor::tabular/col[...]/@halign
                [2]  makes  right
     [position()=2]  makes  right
        [number(2)]  makes  right
(hard 2)        [2]  makes  right
(var @select)   [2]  makes  right

but instead I see:

ancestor::tabular/col[...]/@halign
                [2]  makes  left
     [position()=2]  makes  right
        [number(2)]  makes  right
(hard 2)        [2]  makes  right
(var @select)   [2]  makes  right

Is anyone able to offer an explanation for why using [$col] behaves differently?

Here is the XSL:

<xsl:template match="/">
    <xsl:apply-templates select="root/tabular"/>
</xsl:template>

<xsl:template match="tabular">
  <xsl:apply-templates select="row"/>
</xsl:template>

<xsl:template match="row">
  <xsl:apply-templates select="cell"/>
</xsl:template>

<?xml version='1.0'?> <!-- As XML file -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:template match="/">
    <xsl:apply-templates select="root/tabular"/>
</xsl:template>

<xsl:template match="tabular">
  <xsl:apply-templates select="row"/>
</xsl:template>

<xsl:template match="row">
  <xsl:apply-templates select="cell[2]"/>
</xsl:template>

<xsl:template match="cell[2]">
    <xsl:variable name="col">
        <xsl:value-of select="2"/>
    </xsl:variable>
    <xsl:variable name="colsel" select="2"/>
    <xsl:text>ancestor::tabular/col[...]/@halign</xsl:text>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>                [</xsl:text>
    <xsl:value-of select="$col"/>
    <xsl:text>]  makes  </xsl:text>
    <xsl:value-of select="ancestor::tabular/col[$col]/@halign"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>     [position()=</xsl:text>
    <xsl:value-of select="$col"/>
    <xsl:text>]  makes  </xsl:text>
    <xsl:value-of select="ancestor::tabular/col[position()=$col]/@halign"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>        [number(</xsl:text>
    <xsl:value-of select="$col"/>
    <xsl:text>)]  makes  </xsl:text>
    <xsl:value-of select="ancestor::tabular/col[number($col)]/@halign"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>(hard 2)        [2]  makes  </xsl:text>
    <xsl:value-of select="ancestor::tabular/col[2]/@halign"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>(var @select)   [</xsl:text>
    <xsl:value-of select="$colsel"/>
    <xsl:text>]  makes  </xsl:text>
    <xsl:value-of select="ancestor::tabular/col[$colsel]/@halign"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>&#xa;</xsl:text>
</xsl:template>

</xsl:stylesheet>

Upvotes: 1

Views: 47

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 116992

Let us use a more convenient example:

XML

<root>
    <item>first</item>
    <item>second</item>
</root>

XSLT 1.0

<xsl:stylesheet version="2.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:variable name="num" select="2"/>
<xsl:variable name="str" select="string(2)"/>
<xsl:variable name="rtf">2</xsl:variable>

<xsl:template match="/root">
    <results>
        <num>
            <xsl:copy-of select="item[$num]"/>
        </num>
        <str>
            <xsl:copy-of select="item[$str]"/>
        </str>
        <rtf>
            <xsl:copy-of select="item[$rtf]"/>
        </rtf>
    </results>
</xsl:template>

</xsl:stylesheet>

Result

<?xml version="1.0" encoding="UTF-8"?>
<results>
  <num>
    <item>second</item>
  </num>
  <str>
    <item>first</item>
    <item>second</item>
  </str>
  <rtf>
    <item>first</item>
    <item>second</item>
  </rtf>
</results>

Now you ask why the difference in the results. The answer can be found in the XPath specification that prescribes how a predicate is to be evaluated:

A PredicateExpr is evaluated by evaluating the Expr and converting the result to a boolean. If the result is a number, the result will be converted to true if the number is equal to the context position and will be converted to false otherwise; if the result is not a number, then the result will be converted as if by a call to the boolean function.

In the first instance the value of the $num variable is the number 2. Therefore the result of evaluating the expression within the predicate is a number, and the predicate will be true when the number is equal to the context position - which is only true for the item in the second position.

In the second instance, the value of the $str variable is the string "2". Therefore the expression within the predicate does not evaluate to a number and will be converted to boolean by doing:

boolean("2") 

which returns true() for all items, regardless of their position.

In the third instance, the value of the $rtf variable is a result tree fragment that contains a text node that consists of the character "2". When placed in a predicate, the outcome will be similar to the previous instance: the result of evaluating the expression is not a number, and converting it to a boolean will produce a value of true(). Note that your:

<xsl:variable name="col">
    <xsl:value-of select="2"/>
</xsl:variable>

does exactly the same thing.


Note also that in XSLT 1.0 the xsl:value-of instruction returns the value of the first node in the selected node-set. Therefore, if we change our template to:

<xsl:template match="/root">
    <results>
        <num>
            <xsl:value-of select="item[$num]"/>
        </num>
        <str>
            <xsl:value-of select="item[$str]"/>
        </str>
        <rtf>
            <xsl:value-of select="item[$rtf]"/>
        </rtf>
    </results>
</xsl:template>

the result will be:

<results>
  <num>second</num>
  <str>first</str>
  <rtf>first</rtf>
</results>

but still both items are selected by item[$str] and by item[$rtf].

Upvotes: 2

Mads Hansen
Mads Hansen

Reputation: 66723

Change the variable declaration to:

<xsl:variable name="col" select="2"/>

and it will behave as you expect and select the second col.

You had declared the variable using xsl:value-of: <xsl:value-of select="2"/>, which creates a computed text() node.

When you use that $col variable by itself in a predicate, that string value "2" it is evaluated as true() in the predicate test, rather than if it were a number() and would then be interpreted as short-hand for position() = 2.

Upvotes: 1

Related Questions