stwissel
stwissel

Reputation: 20384

Obtaining Previous element in xslt 1.0 sort order, not dom order

I have a CD list with entries that are not in any specific sort order:

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
    <cd>
        <title>Empire Burlesque</title>
        <artist>Bob Dylan</artist>
        <country>USA</country>
        <company>Columbia</company>
        <price>10.90</price>
        <year>1985</year>
    </cd>
    <cd>
        <title>Hide your heart</title>
        <artist>Bonnie Tyler</artist>
        <country>UK</country>
        <company>CBS Records</company>
        <price>9.90</price>
        <year>1988</year>
    </cd>
    <cd>
        <title>Greatest Hits</title>
        <artist>Dolly Parton</artist>
        <country>USA</country>
        <company>RCA</company>
        <price>9.90</price>
        <year>1982</year>
    </cd>
    <cd>
        <title>Still got the blues</title>
        <artist>Gary Moore</artist>
        <country>UK</country>
        <company>Virgin records</company>
        <price>10.20</price>
        <year>1990</year>
    </cd>
</catalog>

I want to output the list in html, grouped by the first letter of the title. Since it eluded me how, I first ran the XML through a simple sort:

<?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"
    exclude-result-prefixes="xs" version="1.0">
    <xsl:output method="xml" indent="yes" />
    <xsl:template match="catalog">
        <catalog>
            <xsl:apply-templates select="cd"><xsl:sort select="title"/></xsl:apply-templates>
        </catalog>
    </xsl:template>
    <xsl:template match="cd"><xsl:copy-of select="."/></xsl:template>
</xsl:stylesheet>

which then enables me to use preceding-sibling to check for a change in the title letter. The resulting sheet looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <html>
      <head>
        <title> Booklist with books <xsl:value-of select="count(/catalog/cd)"/>
        </title>
        <style type="text/css">
          table.main {width : 100%}
          table.main td {padding : 2px; border-bottom : 1px solid gray}
          th {text-align : left}
          tr.header {background-color : #9acd32}
          table.bar {border: 1px solid gray; background-color #CACACA}
          table.bar td {border-left : 1px solid gray; padding : 4px; margin : 2px; font-size : x-large}
          tr.firstbook {background-color : #CACACA}
          td.firstbook {font-size : xx-large}
          td.firstbook a.up {text-decoration: none; font-size : normal}
        </style>
      </head>
      <body>
        <xsl:apply-templates mode="header"/>
        <xsl:apply-templates/>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="catalog">
    <table class="main">
      <tr class="header">
        <th> Title </th>
        <th> Artist </th>
        <th> Country </th>
        <th> Company </th>
        <th> Price </th>
        <th> Year </th>
      </tr>
      <xsl:apply-templates select="cd">
        <xsl:sort select="title"/>
      </xsl:apply-templates>
    </table>
  </xsl:template>

  <xsl:template match="cd">
    <xsl:variable name="firstLetter" select="substring(title,1,1)"/>
    <xsl:variable name="oldLetter" select="substring(preceding-sibling::*[1]/title,1,1)"/>

    <xsl:if test="not($firstLetter=$oldLetter)">
      <tr class="firstbook">
        <td class="firstbook" colspan="5">
          <a name="{$firstLetter}">
            <xsl:value-of select="$firstLetter"/>
          </a>
        </td>
        <td class="firstbook">
          <a class="up" href="#">&#11014;</a>
        </td>
      </tr>
    </xsl:if>

    <tr>
      <td>
        <xsl:value-of select="title"/>
      </td>
      <td>
        <xsl:value-of select="artist"/>
      </td>
      <td>
        <xsl:value-of select="country"/>
      </td>
      <td>
        <xsl:value-of select="company"/>
      </td>
      <td>
        <xsl:value-of select="price"/>
      </td>
      <td>
        <xsl:value-of select="year"/>
      </td>
    </tr>
  </xsl:template>

  <!-- Header link handling -->
  <xsl:template match="catalog" mode="header">
    <table class="bar">
      <tr>
        <xsl:apply-templates mode="header"
          select="cd[not(substring(title,1,1)=substring(preceding-sibling::*[1]/title,1,1))]">
          <xsl:sort select="title"/>
        </xsl:apply-templates>
      </tr>
    </table>
  </xsl:template>

  <xsl:template mode="header" match="cd">
    <xsl:variable name="firstLetter" select="substring(title,1,1)"/>
    <td>
      <a href="#{$firstLetter}">
        <xsl:value-of select="$firstLetter"/>
      </a>
    </td>
  </xsl:template>

</xsl:stylesheet>

The key part is this comparison: not(substring(title,1,1)=substring(preceding-sibling::*[1]/title,1,1)) which looks at the DOM and not the result of a sort operation.

What I'm looking for is a way in xslt-1.0 to combine the effect of the two transformations, so I have one stylesheet, an unsorted input list and a result that looks like the two stylesheets currently produce:

Screenshot from the XSLT result

How do I do that?

Upvotes: 1

Views: 106

Answers (2)

stwissel
stwissel

Reputation: 20384

Thx to the link provided my @michael.hor I had a look at the Muenchian again. I wasn't aware that you can actually use this technique with substring(). Also I'm not a big fan of for-each.

It turned out I can use template matching and functions for keys. So the solution, running independent of extension functions, with current XSLT engines (never tested on IE, don't have that on my Linux or Mac) looks like this:

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

  <xsl:output method="html"/>
  <xsl:key name="cd-by-letter" match="cd" use="substring(title,1,1)"/>

  <xsl:template match="/">
    <html>
      <head>
        <title> Booklist with books <xsl:value-of select="count(/catalog/cd)"/>
        </title>
        <style type="text/css">
          table.main {width : 100%}
          table.main td {padding : 2px; border-bottom : 1px solid gray}
          th {text-align : left}
          tr.header {background-color : #9acd32}
          table.bar {border: 1px solid gray; background-color #CACACA}
          table.bar td {border-left : 1px solid gray; padding : 4px; margin : 2px; font-size : x-large}
          tr.firstbook {background-color : #CACACA}
          td.firstbook {font-size : xx-large}
          td.firstbook a.up {text-decoration: none; font-size : normal}
        </style>
      </head>
      <body>
        <xsl:apply-templates mode="header"/>
        <xsl:apply-templates/>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="catalog">
    <table class="main">
      <tr class="header">
        <th> Title </th>
        <th> Artist </th>
        <th> Country </th>
        <th> Company </th>
        <th> Price </th>
        <th> Year </th>
      </tr>

      <xsl:apply-templates select="cd[count(. | key('cd-by-letter', substring(title,1,1))[1]) = 1]">
        <xsl:sort select="title"/>
      </xsl:apply-templates>
    </table>
  </xsl:template>

  <xsl:template match="cd">
    <xsl:variable name="firstLetter" select="substring(title,1,1)"/>

    <tr class="firstbook">
      <td class="firstbook" colspan="5">
        <a name="{$firstLetter}">
          <xsl:value-of select="$firstLetter"/>
        </a>
      </td>
      <td class="firstbook">
        <a class="up" href="#">&#11014;</a>
      </td>
    </tr>

    <xsl:apply-templates select="key('cd-by-letter',$firstLetter)" mode="group">
      <xsl:sort select="title"/>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="cd" mode="group">
    <tr>
      <td>
        <xsl:value-of select="title"/>
      </td>
      <td>
        <xsl:value-of select="artist"/>
      </td>
      <td>
        <xsl:value-of select="country"/>
      </td>
      <td>
        <xsl:value-of select="company"/>
      </td>
      <td>
        <xsl:value-of select="price"/>
      </td>
      <td>
        <xsl:value-of select="year"/>
      </td>
    </tr>
  </xsl:template>

  <!-- Header link handling -->
  <xsl:template match="catalog" mode="header">
    <table class="bar">
      <tr>
        <xsl:apply-templates mode="header"
          select="cd[count(. | key('cd-by-letter', substring(title,1,1))[1]) = 1]">
          <xsl:sort select="title"/>
        </xsl:apply-templates>
      </tr>
    </table>
  </xsl:template>

  <xsl:template mode="header" match="cd">
    <xsl:variable name="firstLetter" select="substring(title,1,1)"/>
    <td>
      <a href="#{$firstLetter}">
        <xsl:value-of select="$firstLetter"/>
      </a>
    </td>
  </xsl:template>

</xsl:stylesheet>

Thx everybody for helping!

Upvotes: 1

Martin Honnen
Martin Honnen

Reputation: 167716

You can sort first into a variable (which then in XSLT is a result tree fragment), then you can use an extension function like exsl:node-set to convert your result tree fragment into a node-set to be processed further with your existing code.

So you need two changes, the template for catalog has to be

  <xsl:template match="catalog">
    <table class="main">
      <tr class="header">
        <th> Title </th>
        <th> Artist </th>
        <th> Country </th>
        <th> Company </th>
        <th> Price </th>
        <th> Year </th>
      </tr>
      <xsl:variable name="sorted-cds">
        <xsl:for-each select="cd">
          <xsl:sort select="title"/>
          <xsl:copy-of select="."/>
        </xsl:for-each>
      </xsl:variable>
      <xsl:apply-templates select="exsl:node-set($sorted-cds)/cd"/>
    </table>
  </xsl:template>

and your stylesheet root has to declare the exsl namespace:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
  exclude-result-prefixes="exsl">

Note that not all XSLT processors support exsl:node-set while they usually support at least a similar extension function in a proprietary namespace. So assuming you want to use Microsoft's MSXML (for instance inside of Internet Explorer), then you need to use <xsl:apply-templates select="ms:node-set($sorted-cds)/cd"/> and

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:ms="urn:schemas-microsoft-com:xslt"
  exclude-result-prefixes="ms">

Upvotes: 1

Related Questions