Search

Here’s an XSLT challenge for you. I’ve been trying to get my head around it but my brain is too fried to figure it out, and it’s only Wednesday. Are you a master of recursion of axes? Please, read on.

Here’s an intriguing XML sample:

<datasource>
    <entry id="1" type="a" />
    <entry id="2" type="a" />
    <entry id="3" type="a" />
    <entry id="4" type="a" />
    <entry id="5" type="b" />
    <entry id="6" type="b" />
    <entry id="7" type="a" />
    <entry id="8" type="a" />
    <entry id="9" type="a" />
    <entry id="10" type="a" />
    <entry id="11" type="a" />
    <entry id="12" type="a" />
    <entry id="13" type="c" />
</datasource>

And here’s an equally intriguing HTML result that I’m after:

<div class="a">1</div>
<div class="a">2</div>
<div class="a">3</div>
<div class="a">4</div>
<div class="b">5</div>
<div class="b">6</div>
<div class="a">
    <span>7</span>
    <ul>
        <li>8</li>
        <li>9</li>
        <li>10</li>
        <li>11</li>
        <li>12</li>
    </ul>
</div>
<div class="c">13</div>
  • showing entries in a series of <div> elements, in the order in which they appear in the XML
  • when five or more of the same @type appear consecutively, group them into a single <div> container (representing the first of the five-or-more) followed by a list containing the remaining of the five-or-more

Simple? Prove it. My life stream depends on it!

Aha, I did something similar to this a few weeks ago.

Here’s my XML:

<research-papers>
    <entry id="338">
        <title mode="normal" handle="research-overview-alan-k-betts" word-count="5">RESEARCH OVERVIEW - Alan K. Betts</title>
        <date-of-publication>
            <date timeline="1" type="exact">
                <start iso="2010-07-19T00:00:00-04:00" time="00:00" weekday="1" offset="-0400">2010-07-19</start>
            </date>
        </date-of-publication>
    </entry>
</research-papers>

Here’s my XSLT:

<dl>
    <xsl:for-each select="/data/research-papers/entry">
        <xsl:variable name="jump-position" select="position()" />
        <xsl:if test="substring(substring-before(date-of-publication/date/start,'-'),1,3) != substring(substring-before(../entry[position() = $jump-position - 1]/date-of-publication/date/start,'-'),1,3)">
            <dt><xsl:value-of select="substring(substring-before(date-of-publication/date/start,'-'),1,3)"/><xsl:text>0's</xsl:text></dt>
        </xsl:if>
        <xsl:for-each select=".">
            <dd><xsl:value-of select="title" /></dd>
        </xsl:for-each>
    </xsl:for-each>
</dl>

The result on the page when all is said and done is this.

As you can see, I stripped out some of the excess XML but all the bits you need are there.

Basically, what it does is check to see if the current node is different from the previous node. If it is, it proceeds by creating a new DT. If it’s not, it just makes a new DD for the entry.

You could modify it with only a minimum of difficulty to get it to do what you want, but I think a definition list is more semantic for what you’re doing anyway.

Thanks dougoftheabaci. What you’ve got is kind of a Muenchian method for grouping entries by year, but I don’t think that would work for my needs since I’m “conditionally” grouping once a certain number of similar entries are found consecutively.

Maybe not the prettiest, but it works:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="xml"
    doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
    doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
    encoding="UTF-8"
    indent="yes" />

<xsl:template match="/">
    <xsl:call-template name="parse-entries"/>
</xsl:template>

<xsl:template name="parse-entries">
    <xsl:param name="start" select="1"/>
    <xsl:param name="context" select="1"/>
    <xsl:param name="counter" select="1"/>
    <xsl:choose>
        <xsl:when test="//datasource/entry[$context]/@type = //datasource/entry[$context + 1]/@type">
            <xsl:call-template name="parse-entries">
                <xsl:with-param name="start" select="$start"/>
                <xsl:with-param name="context" select="$context + 1"/>
                <xsl:with-param name="counter" select="$counter + 1"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:choose>
                <xsl:when test="$counter &gt; 4">
                    <xsl:apply-templates select="//datasource/entry[$start]" mode="group">
                        <xsl:with-param name="start" select="$start"/>
                        <xsl:with-param name="context" select="$context"/>
                        <xsl:with-param name="counter" select="$counter"/>
                    </xsl:apply-templates>
                    <xsl:if test="$context &lt;= count(//datasource/entry)">
                        <xsl:call-template name="parse-entries">
                            <xsl:with-param name="start" select="$context + 1"/>
                            <xsl:with-param name="context" select="$context + 1"/>
                            <xsl:with-param name="counter" select="1"/>
                        </xsl:call-template>
                    </xsl:if>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="//datasource/entry[position() &gt; ($start - 1) and position() &lt; ($context + 1)]"/>
                    <xsl:if test="$context &lt;= count(//datasource/entry)">
                        <xsl:call-template name="parse-entries">
                            <xsl:with-param name="start" select="$context + 1"/>
                            <xsl:with-param name="context" select="$context + 1"/>
                            <xsl:with-param name="counter" select="1"/>
                        </xsl:call-template>
                    </xsl:if>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

<xsl:template match="datasource/entry">
    <div class="{@type}"><xsl:value-of select="@id"/></div>
</xsl:template>

<xsl:template match="datasource/entry" mode="group-item">
    <li><xsl:value-of select="@id"/></li>
</xsl:template>

<xsl:template match="datasource/entry" mode="group">
    <xsl:param name="start"/>
    <xsl:param name="context"/>
    <xsl:param name="counter"/>
    <div class="{@type}">
        <span><xsl:value-of select="@id"/></span>
        <ul>
            <xsl:apply-templates select="//datasource/entry[position() &gt; $start and (position() &lt; $start + $counter)]" mode="group-item"/>
        </ul>
    </div>
</xsl:template>

</xsl:stylesheet>

Odds it takes Allen 10 minutes and half the code to do the same thing…

Can’t resist these kinds of challenges. My XSLT is a little rusty but:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
    <xsl:apply-templates select="/datasource/entry[1]"/>
</xsl:template>

<xsl:template match="entry">
    <xsl:variable name="c">
        <xsl:apply-templates select="." mode="peek" />
    </xsl:variable>
    <xsl:variable name="following" select="following-sibling::entry" />

    <div class="{@type}">
        <xsl:choose>
            <xsl:when test="$c &gt; 3">
                <span><xsl:value-of select="@id" /></span>
                <ul>
                    <xsl:for-each select="$following[position() &lt;= $c]">
                        <li><xsl:value-of select="@id" /></li>
                    </xsl:for-each>
                </ul>
                <xsl:apply-templates select="$following[$c+1]" />
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="@id" />
                <xsl:apply-templates select="$following[1]" />
            </xsl:otherwise>
        </xsl:choose>
    </div>
</xsl:template>

<xsl:template match="entry" mode="peek">
    <xsl:param name="c" select="0" />
    <xsl:variable name="next" select="following-sibling::entry[1]" />

    <xsl:choose>
        <xsl:when test="$next/@type = @type">
            <xsl:apply-templates select="$next[1]" mode="peek">
                <xsl:with-param name="c" select="$c+1" />
            </xsl:apply-templates>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$c" />
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

Wonderfully you each tried the different approaches I did (hardcore recursion vs. mix of recursion and axes) and solved it with two very separate approaches. Thank you both very much :-)

Because I’m a sucker for pretty code I’m going with Carl’s (I had to modify it slightly to get the <div> elements nesting correctly).

Cheers all. My new lifestream list will no longer be chock full of Last.fm plays ;-)

Hi Nick,

I’m a sucker for XSLT challenges so here’s my take on it just for sports:

<?xml version="1.0" encoding="utf-8" ?>

<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />

<xsl:template match="/">
    <div>
        <!-- Start from the beginning -->
        <xsl:apply-templates select="/datasource/entry[1]" />

    </div>
</xsl:template>

<xsl:template match="entry">
    <xsl:variable name="myType" select="@type" />
    <xsl:choose>
        <xsl:when test="count(. | following-sibling::entry[position() &lt; 5][@type = $myType]) = 5">
            <xsl:apply-templates select="." mode="container" />
        </xsl:when>
        <xsl:otherwise>
            <xsl:apply-templates select="." mode="default" />
            <!-- Do the next one -->
            <xsl:apply-templates select="following-sibling::entry[1]" />
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

<xsl:template match="entry" mode="default">
    <div class="{@type}">
        <xsl:value-of select="@id" />
    </div>
</xsl:template>

<xsl:template match="entry" mode="container">
    <xsl:variable name="myType" select="@type" />
    <div class="{@type}">
        <span>
            <xsl:value-of select="@id" />
        </span>
        <ul>
            <xsl:apply-templates select="following-sibling::entry[1][@type = $myType]" mode="grouped" />
        </ul>
    </div>

    <!-- Continue after the group -->
    <xsl:apply-templates select="following-sibling::entry[not(@type = $myType)][1]" />
</xsl:template>

<xsl:template match="entry" mode="grouped">
    <xsl:variable name="myType" select="@type" />
    <li>
        <xsl:value-of select="@id" />
    </li>
    <xsl:apply-templates select="following-sibling::entry[1][@type = $myType]" mode="grouped" />
</xsl:template>

/Chriztian

This is great stuff! How about we start a Symphony sporting event where we challenge XSLT champions to engage in a duel to the death, or maybe something a little more civilized, like XSLT ping pong?

@bauhouse - That sounds like a great idea, XSLT ping pong! I think that would be a cool opportunity and fun way of sharing some XSLT and a great learning tool.

xslt ping pong = instant win.

This would be great, esp. if we could tie it in to common symphony/xslt problems.

“Lllllets get ready to XPaaaaaaath!”

Unfortunately I found a few bugs with this, and my requirements ended up changing a bit. So I went with a PHP grouping algorithm in the end.

But thank you all for your suggestions :-)

Create an account or sign in to comment.

Symphony • Open Source XSLT CMS

Server Requirements

  • PHP 5.3-5.6 or 7.0-7.3
  • PHP's LibXML module, with the XSLT extension enabled (--with-xsl)
  • MySQL 5.5 or above
  • An Apache or Litespeed webserver
  • Apache's mod_rewrite module or equivalent

Compatible Hosts

Sign in

Login details