tags:

views:

77

answers:

6

I have a reasonably complex call to xsl:apply-templates:

<xsl:apply-templates select="columnval[@id 
                                       and not(@id='_Name_') 
                                       and not(@id='Group') 
                                       and not(@id='_Count_')]"/>

The expression is reused in other places like this:

<xsl:apply-templates select="someothernode[@id 
                                           and not(@id='_Name_') 
                                           and not(@id='Group') 
                                           and not(@id='_Count_')]"/>

I want to generalize it somehow, so I can define it once and reuse it elsewhere. However, this doesn't seem to work:

<xsl:variable name="x">@id and not(@id='_Name_') and not(@id='Group') and not(@id='_Count_')</xsl:variable>
<xsl:apply-templates select="columnval[$x]"/>
<xsl:apply-templates select="someothernode[$x]"/>

Is there a better / different way of doing this? All I want is to reuse the xpath expression in multiple different calls to xsl:apply-templates (some of which select from different children).

This is going to be used in a client application, so I can't use any extensions or switch to XSLT 2 unfortunately. :(

Thanks.

+1  A: 

I would take a look at using an extension to xslt. I don't think you can do it in "standard" xslt.

This extension can do what you want: http://www.exslt.org/dyn/functions/evaluate/index.html

Plaudit Design - Web Design
Updated question - we can't use extensions, since we're relying on MSXML to do the transform :(
Colen
+1  A: 

With the extension exsl:nodeset, you can create a named template that accepts a nodeset $x and returns the filtered nodeset according to your static predicate.

You can also define a function, in XSLT 2.0.

LarsH
Updated question - unfortunately we are stuck with XSLT 1.0. :(
Colen
+2  A: 

How about:

<xsl:variable name="filter" select="_Name_|Group|_Count_" />

<xsl:apply-templates select="columnval" mode="filtered" />
<xsl:apply-templates select="someothernode" mode="filtered" />

<xsl:template match="someothernode|columnval" mode="filtered">
  <xsl:if test="not(contains(
    concat('|', $filter,'|'), 
    concat('|', @id,'|'), 
  ))">
    <!-- whatever -->
  </xsl:if>
</xsl:template>

You could make $filter a param, and pass it in from the outside for example.

What you cannot do (as you've noticed) is use variables to store XPath expressions.

Tomalak
Stupid question - why can you do that with parameters, but not variables?
Colen
@Colen: The variable (or param, for that matter) `$filter` stores a string, not an expression. I chose `|` as the delimiter. ;-)
Tomalak
+1 Good answer!
Alejandro
+4  A: 

You can't construct XPath dynamically in XSLT (at least, not XSLT 1.0). But you can easily accomplish what you're trying to do using template modes:

<xsl:apply-templates select="columnval" mode="filter"/>
<xsl:apply-template select="someothernode" mode="filter"/>

...

<!-- this guarantees that elements that don't match the filter don't get output -->
<xsl:template match="*" mode="filter"/>

<xsl:template match="*[@id and not(@id='_Name_') and not(@id='Group') and not(@id='_Count_')]" mode="filter">
   <xsl:apply-templates select="." mode="filtered"/>
</xsl:template>

<xsl:template match="columnval" mode="filtered">
   <!-- this will only be applied to the columnval elements that pass the filter -->
</xsl:template>

<xsl:template match="someothernode" mode="filtered">
   <!-- this will only be applied to the someothernode elements that pass the filter -->
</xsl:template>
Robert Rossney
+1 this is an efficient approach. Only downside is that the filter is hardcoded and therefore not variable. If a variable filter is not needed, I would go with this solution.
Tomalak
+1 Good answer!
Alejandro
Template modes made no sense to me at all until I started running into problems like OP's.
Robert Rossney
+1 for a good answer.
Dimitre Novatchev
+1  A: 

Both XSLT 1.0 and XSLT 2.0 do not support dynamic evaluation.

One way to do this is using <xsl:function> in XSLT 2.0 or <xsl:call-template> in XSLT 1.0.

 <xsl:function name="my:test" as="xs:boolean">
  <xsl:param name="pNode" as="element()"/>

  <xsl:variable name="vid" select="$pNode/@id"/>

  <xsl:sequence select=
  "$vid and not($vid=('_Name_','Group','_Count_')"/>
 </xsl:function>

then you could use this function:

<xsl:apply-templates select="columnval[my:test(.)]"/>

Certainly, you could specify the test in specific match patterns as suggested by Robert Rossney, and this might be the best way.

In case you need to dynamically define which filtering function to use, one powerful tool is the FXSL library, which implements Higher-Order-Functions (HOF) in XSLT. HOF are functions that accept other functions as parameters and can return a function as their result.

Using this approach, you dynamically determine and pass to the my:test() as parameter a function that does the test.

Dimitre Novatchev
+2  A: 

Refactoring @Robert Rossney and @Tomalak

<xsl:apply-templates select="columnval" mode="filter"/> 
<xsl:apply-templates select="someothernode" mode="filter"/> 

<xsl:template match="*" mode="filter">
   <xsl:param name="pFilter" select="'_Name_|Group|_Count_'"/> 
   <xsl:apply-templates select="self::*
                                [not(contains( 
                                        concat('|',$pFilter,'|'),  
                                        concat('|',@id,'|')))
                                 and @id]"/> 
</xsl:template> 
Alejandro
Since attributes are by definition members of elements, you can replace the generic `node()` by `*`. :-)
Tomalak
+1 Fair enough!
Alejandro