CodeCore
CodeCore

Reputation: 89

HTML Lists - XSLT Multiple Nested For Each Loop

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

Answers (3)

Dimitre Novatchev
Dimitre Novatchev

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(., '&#xA;')"/>
    <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:

  • Level 1 - Item 1
    • Level 2 - Item 1-1
    • Level 2 - Item 1-2
  • Level 2 - Item 2
    • Level 2 - Item 2-1
      • Level 3 - Item 2-1-1
      • Level 3 - Item 2-1-2
      • Level 3 - Item 2-1-3
    • Level 2 - Item 2-2
      • Level 3 - Item 2-2-1
      • Level 3 - Item 2-2-2

Explanation:

  1. 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.

  2. 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

Maestro13
Maestro13

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

Tomalak
Tomalak

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

Related Questions