Reputation: 31
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
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
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
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