Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 702
CodeCore Avatar asked Jun 04 '26 19:06

CodeCore


1 Answers

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>
like image 144
Tomalak Avatar answered Jun 07 '26 15:06

Tomalak



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!