Reputation: 89
I'm trying to generate a multi level nested html list from xml/xsl.
For example, a preferred html output would be:
<ul>
<li>Level 1 - Item 1</li>
<ul>
<li>Level 2 - Item 1-1</li>
<li>Level 2 - Item 1-2</li>
</ul>
<li> Level 1 - Item 2</li>
<ul>
<li>Level 2 - Item 2-1
<ul>
<li>Level 3 - Item 2-1-1</li>
<li>Level 3 - Item 2-1-2</li>
<li>Level 3 - Item 2-1-3</li>
</ul>
</li>
<li>Level 2 - Item 2-2
<ul>
<li>Level 3 - Item 2-2-1</li>
<li>Level 3 - Item 2-2-2</li>
</ul>
</li>
</ul>
XML:
<doc>
<item>
<one>Level 1 - Item 1</one>
<two>Level 2 - Item 1-1</two>
<two>Level 2 - Item 1-2</two>
</item>
<item>
<one>Level 2 - Item 2</one>
<two>Level 2 - Item 2-1</two>
<three>Level 3 - Item 2-1-1</three>
<three>Level 3 - Item 2-1-2</three>
<three>Level 3 - Item 2-1-3</three>
<two>Level 2 - Item 2-2</two>
<three>Level 3 - Item 2-2-1</three>
<three>Level 3 - Item 2-2-2</three>
</item>
</doc>
My poor attempt XSL:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<xsl:for-each select="doc/item">
<li><xsl:value-of select="one" />
<ul>
<xsl:for-each select="two">
<li><xsl:value-of select="."/>
<xsl:for-each select="../three"><ul><li><xsl:value-of select="."/></li></ul></xsl:for-each>
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
This is what I'm getting below... Notice that when there's a level 3 item then all of items have merged and then displaying under both.
<li>Level 1 - Item 1<ul>
<li>Level 2 - Item 1-1</li>
<li>Level 2 - Item 1-2</li>
</ul>
</li>
<li>Level 2 - Item 2<ul>
<li>Level 2 - Item 2-1<ul>
<li>Level 3 - Item 2-1-1</li>
</ul>
<ul>
<li>Level 3 - Item 2-1-2</li>
</ul>
<ul>
<li>Level 3 - Item 2-1-3</li>
</ul>
<ul>
<li>Level 3 - Item 2-2-1</li>
</ul>
<ul>
<li>Level 3 - Item 2-2-2</li>
</ul>
</li>
<li>Level 2 - Item 2-2<ul>
<li>Level 3 - Item 2-1-1</li>
</ul>
<ul>
<li>Level 3 - Item 2-1-2</li>
</ul>
<ul>
<li>Level 3 - Item 2-1-3</li>
</ul>
<ul>
<li>Level 3 - Item 2-2-1</li>
</ul>
<ul>
<li>Level 3 - Item 2-2-2</li>
</ul>
</li>
</ul>
</li>
Please provide me with 1.0 solutions and then of course show 2.0 examples to help others as well.
thank you!
Upvotes: 3
Views: 11527
Reputation: 243449
This simple (no inline XML, no document()
and contains()
functions), short and efficient transformation:
<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:key name="kFollowing" match="two"
use="generate-id(preceding-sibling::one[1])"/>
<xsl:key name="kFollowing" match="three"
use="generate-id(preceding-sibling::two[1])"/>
<xsl:template match="/*">
<ul>
<xsl:apply-templates select="item/one" mode="inGroup"/>
</ul>
</xsl:template>
<xsl:template match="one|two" mode="inGroup">
<li><xsl:value-of select="concat(., '
')"/>
<xsl:variable name="vGroup" select=
"key('kFollowing', generate-id())"/>
<xsl:apply-templates select=
"$vGroup[1]">
<xsl:with-param name="pGroup" select="$vGroup"/>
</xsl:apply-templates>
</li>
</xsl:template>
<xsl:template match="two|three">
<xsl:param name="pGroup"/>
<xsl:if test="position() = 1">
<ul>
<xsl:apply-templates select="$pGroup" mode="inGroup"/>
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="three" mode="inGroup">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<doc>
<item>
<one>Level 1 - Item 1</one>
<two>Level 2 - Item 1-1</two>
<two>Level 2 - Item 1-2</two>
</item>
<item>
<one>Level 2 - Item 2</one>
<two>Level 2 - Item 2-1</two>
<three>Level 3 - Item 2-1-1</three>
<three>Level 3 - Item 2-1-2</three>
<three>Level 3 - Item 2-1-3</three>
<two>Level 2 - Item 2-2</two>
<three>Level 3 - Item 2-2-1</three>
<three>Level 3 - Item 2-2-2</three>
</item>
</doc>
produces the wanted, correct result:
<ul>
<li>Level 1 - Item 1
<ul>
<li>Level 2 - Item 1-1
</li>
<li>Level 2 - Item 1-2
</li>
</ul>
</li>
<li>Level 2 - Item 2
<ul>
<li>Level 2 - Item 2-1
<ul>
<li>Level 3 - Item 2-1-1</li>
<li>Level 3 - Item 2-1-2</li>
<li>Level 3 - Item 2-1-3</li>
</ul>
</li>
<li>Level 2 - Item 2-2
<ul>
<li>Level 3 - Item 2-2-1</li>
<li>Level 3 - Item 2-2-2</li>
</ul>
</li>
</ul>
</li>
</ul>
and it is displayed by the browser as:
Explanation:
There is a single key kFollowing
(with two separate definitions) that indexes any two
or three
element by the value of generate-id()
of its logical parent (respectively one
or two
). This helps us to have a single template matching both one
and two
elements.
Every first-in-group (two
or three
) element is matched and processed in no mode. In this template the wrapping ul
is generated, then all elements in the group (passed as a parameter) are processed in mode named inGroup
.
Upvotes: 2
Reputation: 3696
Try the below. Explanation: select all "three" siblings that have the same first "two" sibling preceding it. I ran this in XML Spy and got the wanted output (see below xslt).
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:template match="/">
<html>
<body>
<xsl:for-each select="doc/item">
<li>
<xsl:value-of select="one" />
<ul>
<xsl:for-each select="two">
<li>
<xsl:value-of select="."/>
<ul>
<xsl:for-each select="following-sibling::three[preceding-sibling::two[1]=current()]">
<li>
<xsl:value-of select="."/>
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Output:
<html>
<body>
<li>Level 1 - Item 1
<ul>
<li>Level 2 - Item 1-1
<ul></ul>
</li>
<li>Level 2 - Item 1-2
<ul></ul>
</li>
</ul>
</li>
<li>Level 2 - Item 2
<ul>
<li>Level 2 - Item 2-1
<ul>
<li>Level 3 - Item 2-1-1</li>
<li>Level 3 - Item 2-1-2</li>
<li>Level 3 - Item 2-1-3</li>
</ul>
</li>
<li>Level 2 - Item 2-2
<ul>
<li>Level 3 - Item 2-2-1</li>
<li>Level 3 - Item 2-2-2</li>
</ul>
</li>
</ul>
</li>
</body>
</html>
Upvotes: 4
Reputation: 338178
Here is an XSLT 1.0 solution that works with your input XML.
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://tempuri.org"
exclude-result-prefixes="my"
>
<xsl:output indent="yes" />
<!-- define which elements are where in the hierarchy -->
<my:level name="one" higher="" deeper="two,three" />
<my:level name="two" higher="one" deeper="three" />
<my:level name="three" higher="one,two" deeper="" />
<xsl:template match="doc">
<body>
<xsl:apply-templates mode="ul" select="item/*[1]" />
</body>
</xsl:template>
<xsl:template match="one|two|three" mode="ul">
<ul>
<xsl:apply-templates mode="li" select="." />
</ul>
</xsl:template>
<xsl:template match="one|two|three" mode="li">
<xsl:variable name="myName" select="name()" />
<xsl:variable name="myID" select="generate-id()" />
<!-- select the appropriate hierarchy info for this node -->
<xsl:variable name="level" select="
document('')/*/my:level[@name = $myName]
" />
<li>
<xsl:value-of select="." />
<!-- create <ul> if immediately follwing sibling is deeper -->
<xsl:apply-templates mode="ul" select="
following-sibling::*[1][contains($level/@deeper, name())]
" />
</li>
<!-- process contiguous following siblings of same level -->
<xsl:apply-templates mode="li" select="
following-sibling::*[name() = $myName][
generate-id(
preceding-sibling::*[contains($level/@higher, name())][1]/following-sibling::*[1]
)
= $myID
]
" />
</xsl:template>
</xsl:stylesheet>
Given the input document from your question, it produces this output:
<body>
<ul>
<li>Level 1 - Item 1
<ul>
<li>Level 2 - Item 1-1</li>
<li>Level 2 - Item 1-2</li>
</ul>
</li>
</ul>
<ul>
<li>Level 2 - Item 2
<ul>
<li>Level 2 - Item 2-1
<ul>
<li>Level 3 - Item 2-1-1</li>
<li>Level 3 - Item 2-1-2</li>
<li>Level 3 - Item 2-1-3</li>
</ul>
</li>
<li>Level 2 - Item 2-2
<ul>
<li>Level 3 - Item 2-2-1</li>
<li>Level 3 - Item 2-2-2</li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
Frankly, I'm too tired right now to explain the solution in detail. I've left a few comments though. Suffice it to say that it is pretty complicated.
If your XML would look like this (i.e. properly nested):
<doc>
<item title="Level 1 - Item 1">
<item title="Level 2 - Item 1-1" />
<item title="Level 2 - Item 1-2" />
</item>
<item title="Level 2 - Item 2">
<item title="Level 2 - Item 2-1">
<item title="Level 3 - Item 2-1-1" />
<item title="Level 3 - Item 2-1-2" />
<item title="Level 3 - Item 2-1-3" />
</item>
<item title="Level 2 - Item 2-2">
<item title="Level 3 - Item 2-2-1" />
<item title="Level 3 - Item 2-2-2" />
</item>
</item>
</doc>
a solution that would produce the same HTML result as above would look like this:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output indent="yes" />
<xsl:template match="doc">
<body>
<xsl:for-each select="item">
<ul>
<xsl:apply-templates select="." />
</ul>
</xsl:for-each>
</body>
</xsl:template>
<xsl:template match="item">
<li>
<xsl:value-of select="@title" />
<xsl:if test="item">
<ul>
<xsl:apply-templates select="item" />
</ul>
</xsl:if>
</li>
</xsl:template>
</xsl:stylesheet>
Upvotes: 4