简体   繁体   中英

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!

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 (ie 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>

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>

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM