Finn Christensen
Finn Christensen

Reputation: 31

XSL recursion axis fault?

I have the following xml sitemap file:

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="NewSiteMap.xsl"?>
<siteMap>
  <siteMapNode
    url="~/UsingMasterTemplate.aspx?id=1"
    title="Home"
    description="AAAAAAAAAAAAAAAAAAA">
    <siteMapNode
      url="~/UsingMasterTemplate.aspx?id=2"
      title="Profile"
      description="BBBBBBBBBBBBBBBBBB" />
    <siteMapNode
      url="~/UsingMasterTemplate.aspx?id=3"
      title="People"
      description="CCCCCCCCCCCCCCCCCCCCCCCC" />
    <siteMapNode
      url="~/UsingMasterTemplate.aspx?id=5"
      title="New Page"
      description="DDDDDDDDDDDDDDDDDDDD" />
  </siteMapNode>
</siteMap>

And the following xsl file to do recursion and output to ul:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method='xml' version='1.0' omit-xml-declaration="yes" encoding='UTF-8' indent='yes'/>

<xsl:template match="siteMap">
<!--  
<xsl:variable name='siteMapNode'>
 <xsl:value-of select='siteMap/siteMapNode'/>
</xsl:variable> 
-->
<html>
  <head>
     <link rel="stylesheet" href="xSiteMap.css" type="text/css" />  
  </head>

  <body>
       <h2>SiteMap:</h2>
   <ul>
    <!-- Check for empty sitemap -->
    <xsl:if test='siteMapNode'>
     <xsl:call-template name='BuildNavList'>
      <xsl:with-param name='siteMapNode' select='siteMapNode'/>
     </xsl:call-template>
    </xsl:if>     
      </ul>
  </body>

</html> 

</xsl:template> 

<xsl:template name='BuildNavList'>
 <xsl:param name='siteMapNode'/>
 <li> 
  <a>
   <xsl:attribute name="href">
    <xsl:value-of select="$siteMapNode/@url"/>
   </xsl:attribute>
   <xsl:attribute name="title">
    <xsl:value-of select="$siteMapNode/@description"/>       
   </xsl:attribute>
   <xsl:value-of select="$siteMapNode/@title"/>
  </a> 
    <!-- test for node-children, if true then recursion -->
  <xsl:if test='$siteMapNode/node()'>
   <ul>
    <xsl:for-each select="$siteMapNode/node()">
       <xsl:call-template name='BuildNavList'>
      <xsl:with-param name='siteMapNode' select='$siteMapNode/node()'/>
     </xsl:call-template>
    </xsl:for-each>  
   </ul>
  </xsl:if> 
 </li> 
</xsl:template>

</xsl:stylesheet>

But there seems to be an error in my recursion call (propably an axis error in my for-each statement)! What goes wrong here?

Upvotes: 2

Views: 285

Answers (3)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243549

Following the good answers by @LarsH and @Gaby, let me show my preferred way of solving this problem.

In XSLT any conditional (<xsl:if> or <xsl:when>) is an indication that the full power of XSLT pattern matching has not been used.

Instead of such conditionals, try to use as much as possible pattern-matching in the match attribute of <xsl:template>.

My solution is:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/*">
    <html>
      <head>
         <link rel="stylesheet" href="xSiteMap.css" type="text/css" />
      </head>

      <body>
        <h2>SiteMap:</h2>
          <xsl:apply-templates select="siteMapNode"/>
      </body>
  </html>
 </xsl:template>

 <xsl:template match="siteMapNode[1]">
  <ul>
   <xsl:call-template name="buildNav"/>
   <xsl:apply-templates select="following-sibling::siteMapNode"
        mode="inList"/>
  </ul>
 </xsl:template>

 <xsl:template match="siteMapNode" name="buildNav">
  <li>
    <a href="{@url}" title="{@description}">
      <xsl:value-of select="@title"/>
    </a>
    <xsl:apply-templates select="siteMapNode"/>
  </li>
 </xsl:template>

 <xsl:template match="siteMapNode" mode="inList">
  <xsl:call-template name="buildNav"/>
 </xsl:template>
 <xsl:template match="siteMapNode[position() > 1]"/>
</xsl:stylesheet>

when this transformation is applied to the provided XML document:

<siteMap>
    <siteMapNode
    url="~/UsingMasterTemplate.aspx?id=1"
    title="Home"
    description="AAAAAAAAAAAAAAAAAAA">

        <siteMapNode
         url="~/UsingMasterTemplate.aspx?id=2"
         title="Profile"
         description="BBBBBBBBBBBBBBBBBB" />

        <siteMapNode
      url="~/UsingMasterTemplate.aspx?id=3"
      title="People"
      description="CCCCCCCCCCCCCCCCCCCCCCCC" />

        <siteMapNode
      url="~/UsingMasterTemplate.aspx?id=5"
      title="New Page"
      description="DDDDDDDDDDDDDDDDDDDD" /></siteMapNode>
</siteMap>

the wanted, correct answer is produced:

<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

      <link rel="stylesheet" href="xSiteMap.css" type="text/css">
   </head>
   <body>
      <h2>SiteMap:</h2>
      <ul>
         <li><a href="~/UsingMasterTemplate.aspx?id=1" title="AAAAAAAAAAAAAAAAAAA">Home</a><ul>
               <li><a href="~/UsingMasterTemplate.aspx?id=2" title="BBBBBBBBBBBBBBBBBB">Profile</a></li>
               <li><a href="~/UsingMasterTemplate.aspx?id=3" title="CCCCCCCCCCCCCCCCCCCCCCCC">People</a></li>
               <li><a href="~/UsingMasterTemplate.aspx?id=5" title="DDDDDDDDDDDDDDDDDDDD">New Page</a></li>
            </ul>
         </li>
      </ul>
   </body>
</html>

Do note: how the <xsl:if> s disappeared "magically".

Upvotes: 1

LarsH
LarsH

Reputation: 28004

In addition to Gaby's answer, you might want to know that using call-template and passing one parameter, a node, is just a roundabout way of saying apply-templates to that node (without template matching). Apply-templates is the normal XSLT way of doing what you're doing, and it's less verbose.

So your initial call-template

<xsl:if test='siteMapNode'>
 <xsl:call-template name='BuildNavList'>
  <xsl:with-param name='siteMapNode' select='siteMapNode'/>
 </xsl:call-template>
</xsl:if>   

can become

 <xsl:apply-templates select='siteMapNode'/>

which will apply to children of the context node named siteMapNode.

Then your recursive template becomes

<xsl:template match="siteMapNode">
 <li> 
  <a href="{@url}" title="{@description}">
   <xsl:value-of select="@title"/>
  </a> 
    <!-- test for siteMapNode element children, if true then recur -->
  <xsl:if test='siteMapNode'>
   <ul>
     <xsl:apply-templates select="siteMapNode" />
   </ul>
  </xsl:if> 
 </li> 
</xsl:template>

Notice that we eliminated a lot of references to the $siteMapNode parameter because that's now the context node. Notice also the Attribute Value Templates used for <a href="" and title="">. Much more succinct and readable!

XSLT really is more convenient when you understand and use it the way it was designed!

Upvotes: 2

Gabriele Petrioli
Gabriele Petrioli

Reputation: 196217

In the BuildNavList template change the inner template call to

<xsl:for-each select="$siteMapNode/siteMapNode">
   <xsl:call-template name='BuildNavList'>
  <xsl:with-param name='siteMapNode' select='.'/>
 </xsl:call-template>
</xsl:for-each>

the important thing is to use the . in the xsl:with-param because you are already inside the loop of the nodes...

the seconds issue is the for-each select. In this case i use the /siteMapNode to ignore the whitespace between the elements because the node() alternative takes whitespace into account as text nodes and gets messed up.

If you have to use the nodes() version (at the for-each select) then you can add <xsl:strip-space elements="*"/> on the top of your xslt so that it will remove them ..

Upvotes: 1

Related Questions