Search code examples
xmlxslt

How to group adjacent xml elements in nested ol with xslt v2.0 or 3.0?


I have an XML document without proper hierarchy that I need to turn into a multilevel nested ordered list.

How to generate multiple-level ordered lists from the following source XML by grouping the elements into nested ordered lists based on the value of the name attribute?

The name attribute values that need to be grouped range from level_1 to level_6, so getting an XSLT transform to make the nesting more generic will really help. The elements with name level_0 name attribute value and all the other elements should be ignored or unchanged.

<root>
    <p name="level_0">Value excluded from grouping.</p>
    <p name="level_1">Value 1</p>
    <p name="level_1">Value 2</p>
    <p name="level_2">Value 3</p>
    <p name="level_2">Value 4</p>
    <p name="level_1">Value 5</p>
    <note>Some text</note>
    <p name="level_1">Another Value 1</p>
    <p name="level_2">Another Value 2</p>
    <p name="level_2">Another Value 3</p>
    <p name="level_3">Another Value 4</p>
    <p name="level_3">Another Value 5</p>
    <p name="level_4">Another Value 3</p>
    <note>Some text</note>
    <p name="level_4">Another Value 4</p>
    <p name="level_1">Another Value 5</p>
    <p name="level_0">Value excluded from grouping.</p>
</root>

The expected results should look like this

<ol class="ol-">
    <li class="li-level_1">Value 1</li>
    <li class="li-level_1">
        Value 2 <ol class="ol-level_2">
            <li class="li-level_2">Value 3</li>
            <li class="li-level_2">Value 4</li>
        </ol>
    </li>
    <li class="li-level_1">Value 5</li>
    <li class="li-level_1">
        Another Value 1
        <ol class="ol-level_2">
            <li class="li-level_2">Another Value 2</li>
            <li class="li-level_2">
                Another Value 3
                <ol class="ol-level_3">
                    <li class="li-level_3">Another Value 4</li>
                    <li class="li-level_3">
                        Another Value 5
                        <ol class="ol-level_4">
                            <li class="li-level_4">Another Value 3</li>
                            <li class="li-level_4">Another Value 4</li>
                        </ol>
                    </li>
                </ol>
            </li>
        </ol>
        <li class="li-level_1">Another Value 5</li>
    </li>
</ol>

I have tried the following xslt but the output is wrong.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:math="http://www.w3.org/2005/xpath-functions/math" exclude-result-prefixes="xs math"
  version="2.0">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="root">
    <ol class="ol-{@name}">
      <xsl:for-each-group select="*" group-adjacent="@name">
        <li class="li-{@name}">
          <xsl:value-of select="current-grouping-key()"/>
          <ol class="ol-{@name}">
            <xsl:for-each select="current-group()">
              <li class="li-{@name}">
                <xsl:value-of select="."/>
              </li>
            </xsl:for-each>
          </ol>
        </li>
      </xsl:for-each-group>
    </ol>
  </xsl:template>
</xsl:stylesheet>

Solution

  • You can use recursive grouping, here done with XSLT 3:

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:mf="http://example.com/mf"
        exclude-result-prefixes="#all"
        version="3.0">
      
      <xsl:function name="mf:group" as="node()*">
        <xsl:param name="items" as="element()*"/>
        <xsl:param name="level" as="xs:integer"/>
        <xsl:for-each-group select="$items" group-starting-with="p[@name = 'level_' || $level]">
          <xsl:choose>
            <xsl:when test="self::p[@name = 'level_' || $level]">
              <li class="li-{@name}">
                <xsl:apply-templates/>
                <xsl:where-populated>
                  <ol class="ol-level-{$level + 1}">
                    <xsl:sequence select="mf:group(tail(current-group()), $level + 1)"/>
                  </ol>
                </xsl:where-populated>
              </li>
            </xsl:when>
            <xsl:otherwise>
              <xsl:sequence select="mf:group(current-group(), $level + 1)"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each-group>
      </xsl:function>
      
      <xsl:template match="p[@name]">
        <li class="li-{@name}">
          <xsl:apply-templates/>
        </li>
      </xsl:template>
    
      <xsl:output method="xml" indent="yes"/>
      
      <xsl:template match="root">
        <ol class="ol-">
           <xsl:sequence select="mf:group(p[@name = (1 to 6)!('level_' || .)], 1)"/>
        </ol>
      </xsl:template>
      
    </xsl:stylesheet>