Re: [xsl] Constructing multi-level lists - any better than this?

Subject: Re: [xsl] Constructing multi-level lists - any better than this?
From: Abel Braaksma <abel.online@xxxxxxxxx>
Date: Sun, 16 Sep 2007 17:07:13 +0200
Hi Michael,

Though you didn't really requested it, I tried to take your example and make it generic. I.e.: a nesting level is demanded by the number in liX. This means that:

<li1>test1</li1>
<li3>test3</li3>

will return

<ul level="1">
 <li tag="li1" pos="1">test1</li>
 <ul level="2">
    <ul level="3">
       <li tag="li3" pos="1">test3</li>
    </ul>
 </ul>
</ul>


Same for any higher level. It will also successfully unwind to lower levels. However, there are some mistakes in my approach (well, depending on your requirements), which makes the unwinding be not successful when the difference is more than -1 (the upwinding goes well though). The reason is simple: I am working on a current group and the function doing the up-winding does not know of the next group. It shouldn't be too hard to change it.


All in all, I found applying the very simple rules to a general concept more challenging than I first thought (but it was late last night... ). I used the deep-equal() function to test my results with yours and with input only containing l1/l2 elements the output is correct.

The solution is a bit wieldy, but I don't see how it can be more concise or easier. I know I tackled the problem before, but couldn't find the solution I had back then... But if you leave out the functions (which can go elsewhere) it becomes less awkward (you must add a ns decl. for these functions, any will do).

I corrected a small errors on your side: I use local-name, where you use name(), which will fail when you add namespaces to your solution. Also, I followed your way of calculating "pos", but I think that the pos-values for your third example should be 1 and 2 for the last two nodes (because they are the first at their level). If you want that, change the 'ge' in my pos-calculation to 'eq'.


<xsl:template match="levels" mode="new">
<xsl:copy>
<xsl:for-each-group select="*" group-starting-with="p">
<!-- we have to work on a copy of the group, we want to loose the siblings we are not interested in -->
<xsl:variable name="group"><xsl:copy-of select="current-group()" /></xsl:variable>
<xsl:apply-templates select="$group/*[1]" mode="new" />
</xsl:for-each-group>
</xsl:copy>
</xsl:template>


   <xsl:template match="p" mode="new">
       <xsl:copy />
       <xsl:apply-templates select="following-sibling::*[1]" mode="new" />
   </xsl:template>

<xsl:template match="*[fn:is-li(.)]" mode="new">
<xsl:variable name="current-level" select="fn:li-nr(preceding-sibling::*[1])" />
<ul level="1"> <!-- this level is always one!, see fn:create-levels for others -->
<xsl:for-each-group select=". | following-sibling::*" group-adjacent="fn:li-nr(.)">
<xsl:apply-templates select="current-group()[fn:li-nr(.) eq $current-level]" mode="li" />
<xsl:copy-of select="fn:create-levels($current-level, current-group())" /> </xsl:for-each-group>
</ul>
</xsl:template>


<xsl:template match="*[fn:is-li(.)]" mode="li">
<li tag="{name()}" pos="{count(. | preceding-sibling::*[fn:is-li(.)][fn:li-nr(.) ge fn:li-nr(current())])}">
<xsl:value-of select="." />
</li>
</xsl:template>



<!-- recursively create a higher level when level numbers skip a level -->
<xsl:function name="fn:create-levels">
<xsl:param name="current-level" />
<xsl:param name="current-group" />
<xsl:if test="fn:li-nr($current-group[1]) gt $current-level">
<ul level="{$current-level + 1}">
<xsl:copy-of select="fn:create-levels($current-level + 1, $current-group)" />
<xsl:apply-templates select="$current-group[fn:li-nr(.) eq $current-level +1]" mode="li" />
</ul>
</xsl:if>
</xsl:function>


   <!-- true for a node of type <liX> where X is a number -->
   <xsl:function name="fn:is-li">
       <xsl:param name="node" />
       <xsl:sequence select="starts-with(local-name($node), 'li')" />
   </xsl:function>

<!-- returns the X as an integer from the node of type <liX> -->
<xsl:function name="fn:li-nr">
<xsl:param name="node" />
<xsl:variable name="num" select="replace(local-name($node), '[a-zA-Z]', '')" />
<xsl:sequence select="if($num) then xs:integer($num) else 1" />
</xsl:function>




Cheers and have fun with it!

-- Abel Braaksma


Michael M|ller-Hillebrand wrote:
Hello,

I have a working stylesheet, but it uses modes when I think it could be a bit slimmer and maybe more flexible.

The task is to create list containers (<ul>) around list elements in a flat element tree. I tried to follow Jeni Tennison's advice for constructing hierarchies <http://jenitennison.com/xslt/hierarchies-out.xml> and also evaluated xsl:for-each-group, but the latter seems not to work very well for a stylesheet in push mode (no changes to the element order).

To correctly group the <li1>s and <li2>s (even in the third case) in this example I successfully used the XSL below. I found no good enough example in the FAQs, so I dare to ask, whether there is a more elegant solution. (I simplified the case, in reality there are multiple elements that are either level 1 or level 2 list elements, and I do not use starts-with() to detect element names.)

Any advice is greatly appreciated!

- Michael M|ller-Hillebrand

<?xml version="1.0" encoding="UTF-8"?>
<levels>
<p/>
<li1>1</li1>
<li1>2</li1>
<li1>3</li1>
<li1>4</li1>
<p/>
<li1>5</li1>
<li1>6</li1>
<li2>7.1</li2>
<li2>7.2</li2>
<p/>
<li2>8.1</li2>
<li2>8.2</li2>
<li1>9</li1>
<li1>10</li1>
<p/>
</levels>

---------

<xsl:stylesheet version="2.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
<xsl:strip-space elements="*"/>
<xsl:output method="xml" indent="yes" />

<!-- Root element -->
<xsl:template match="levels">
  <xsl:copy>
    <xsl:apply-templates />
  </xsl:copy>
</xsl:template>

<!-- Keys to identify list elements after first element -->
<xsl:key name="list12-other"
  match="*[self::*[starts-with(name(), 'li')]
    and preceding-sibling::*[1][starts-with(name(), 'li')]]"
  use="generate-id(
    preceding-sibling::*[
      starts-with(name(), 'li')
      and not(preceding-sibling::*[1][starts-with(name(), 'li')])
    ][1])" />

<xsl:key name="list2-other"
  match="li2[preceding-sibling::*[1][self::li2]]"
  use="generate-id(
    preceding-sibling::li2[
      not(preceding-sibling::*[1][self::li2])
    ][1])" />

<!-- List 1 Container-->
<xsl:template match="*[starts-with(name(), 'li')
and not(preceding-sibling::*[1][starts-with(name(), 'li')])]" priority="1">
<ul level="1">
<xsl:apply-templates mode="list1"
select=". | key('list12-other', generate-id())" />
</ul>
</xsl:template>
<!-- List 1 elements -->
<xsl:template match="li1" mode="list1">
<li tag="{name()}" pos="{position()}">
<xsl:apply-templates />
</li>
</xsl:template>


<!-- or List 2 Container-->
<xsl:template match="li2[not(preceding-sibling::*[1][self::li2])]"
  mode="list1" priority="1">
  <ul level="2">
    <xsl:apply-templates mode="list2"
      select=". | key('list2-other', generate-id())" />
  </ul>
</xsl:template>
<!-- List 2 elements -->
<xsl:template match="li2" mode="list2">
  <li tag="{name()}" pos="{position()}">
    <xsl:apply-templates />
  </li>
</xsl:template>

<!-- skip list elements when matched outside list -->
<xsl:template match="li2" mode="list1"/>
<xsl:template match="*[starts-with(name(), 'li')]"/>

<!-- all other nodes -->
<xsl:template match="node()">
  <xsl:copy>
    <xsl:apply-templates />
  </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Current Thread