Search code examples
xmlxsltxpathxslt-grouping

XSL to create nested list from flat tree problem


I need to be able to create nested lists from a flat tree. For example, the input might be something like this:

<root>
    <h1>text</h1>
    <list level="1">num1</list>
    <list level="1">num2</list>
    <list level="2">sub-num1</list>
    <list level="2">sub-num2</list>
    <list level="3">sub-sub-num1</list>
    <list level="1">num3</list>
    <p>text</p>
    <list>num1</list>
    <list>num2</list>
    <h2>text</h2>
</root>

and the output should be nested as follows:

<root>
<h1>text</h1>
    <ol>
        <li>num1</li>
        <li>num2
             <ol>
                <li>sub-num1</li>
                <li>sub-num2
                    <ol>
                        <li>sub-sub-num1</li>
                    </ol>
                </li>
            </ol>
        </li>
        <li>num3</li>
    </ol>
    <p>text</p>
    <ol>
        <li>num1</li>
        <li>num2</li>
    </ol>
    <h2>text</h2>
</root>

I've tried a few approaches but just can't seem to get it. Any help is greatly appreciated. Note: I need to do this using XSLT 1.0.


Solution

  • This 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="kListGroup" match="list"
      use="generate-id(
              preceding-sibling::node()[not(self::list)][1]
                       )"/>
    
     <xsl:template match="node()|@*">
      <xsl:copy>
       <xsl:apply-templates select="node()[1]|@*"/>
      </xsl:copy>
      <xsl:apply-templates select=
       "following-sibling::node()[1]"/>
     </xsl:template>
    
     <xsl:template match=
      "list[preceding-sibling::node()[1][not(self::list)]]">
    
      <ol>
        <xsl:apply-templates mode="listgroup" select=
         "key('kListGroup',
              generate-id(preceding-sibling::node()[1])
              )
              [not(@level) or @level = 1]
         "/>
      </ol>
      <xsl:apply-templates select=
       "following-sibling::node()[not(self::list)][1]"/>
     </xsl:template>
    
     <xsl:template match="list" mode="listgroup">
      <li>
        <xsl:value-of select="."/>
    
        <xsl:variable name="vNext" select=
         "following-sibling::list
                [not(@level > current()/@level)][1]
         |
          following-sibling::node()[not(self::list)][1]
         "/>
    
         <xsl:variable name="vNextLevel" select=
         "following-sibling::list
         [@level = current()/@level +1]
          [generate-id(following-sibling::list
                [not(@level > current()/@level)][1]
               |
                 following-sibling::node()[not(self::list)][1]
                      )
          =
           generate-id($vNext)
          ]
         "/>
    
         <xsl:if test="$vNextLevel">
         <ol>
          <xsl:apply-templates mode="listgroup"
            select="$vNextLevel"/>
         </ol>
         </xsl:if>
      </li>
     </xsl:template>
    </xsl:stylesheet>
    

    when applied on this XML document (intentionally complicated to show that the solution works in many edge cases):

    <root>
        <h1>text</h1>
        <list level="1">1.1</list>
        <list level="1">1.2</list>
        <list level="2">1.2.1</list>
        <list level="2">1.2.2</list>
        <list level="3">1.2.2.1</list>
        <list level="1">1.3</list>
        <p>text</p>
        <list>2.1</list>
        <list>2.2</list>
        <h2>text</h2>
        <h1>text</h1>
        <list level="1">3.1</list>
        <list level="1">3.2</list>
        <list level="2">3.2.1</list>
        <list level="2">3.2.2</list>
        <list level="3">3.2.2.1</list>
        <list level="1">3.3</list>
        <list level="2">3.3.1</list>
        <list level="2">3.3.2</list>
        <p>text</p>
    </root>
    

    produces the wanted, correct result:

    <root>
       <h1>text</h1>
       <ol>
          <li>1.1</li>
          <li>1.2<ol>
                <li>1.2.1</li>
                <li>1.2.2<ol>
                      <li>1.2.2.1</li>
                   </ol>
                </li>
             </ol>
          </li>
          <li>1.3</li>
       </ol>
       <p>text</p>
       <ol>
          <li>2.1</li>
          <li>2.2</li>
       </ol>
       <h2>text</h2>
       <h1>text</h1>
       <ol>
          <li>3.1</li>
          <li>3.2<ol>
                <li>3.2.1</li>
                <li>3.2.2<ol>
                      <li>3.2.2.1</li>
                   </ol>
                </li>
             </ol>
          </li>
          <li>3.3<ol>
                <li>3.3.1</li>
                <li>3.3.2</li>
             </ol>
          </li>
       </ol>
       <p>text</p>
    </root>
    

    or as displayed by the browser:

    text

    1. 1.1
    2. 1.2
      1. 1.2.1
      2. 1.2.2
        1. 1.2.2.1
    3. 1.3

    text

    1. 2.1
    2. 2.2

    text

    text

    1. 3.1
    2. 3.2
      1. 3.2.1
      2. 3.2.2
        1. 3.2.2.1
    3. 3.3
      1. 3.3.1
      2. 3.3.2

    text