tags:

views:

251

answers:

3

Given the following XML:

<databases>
    <database>
        <title_display>Aardvark</title_display>
    </database>
    <database>
        <title_display>Apple</title_display>
    </database>
    <database>
        <title_display>Blue</title_display>
    </database>
    <database>
        <title_display>Car</title_display>
    </database>
</databases>

How can I get the following HTML output with XSLT?

<h2>A</h2>
<div class="a-content">
    <ul>
        <li>Aardvark</li>
        <li>Aardvark</li>
    </ul>
</div>

<h2>B</h2>
<div class="b-content">
    <ul>
        <li>Blue</li>
    </ul>
</div>

<h2>C</h2>
<div class="c-content">
    <ul>
        <li>Car</li>
    </ul>
</div>

I can safely assume that all <database> elements are already in alphabetical order. Thanks!

Edit: For future reference, the "Accordion" part is that the HTML is turned into an accordion element with JavaScript.

+1  A: 

You could try this for grouping. Note that xslt2.0 has a for-each-group that makes this much easier.

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  version="1.0">
  <xsl:output method="html"/>

  <xsl:template match="/databases">
    <xsl:for-each select="database">
        <xsl:variable name="Init" select="substring(title_display,1,1)"/>
        <xsl:if test="not(preceding-sibling::*[substring(title_display,1,1)=$Init])">
            <h2><xsl:value-of select="$Init"/></h2>
            <div>
            <xsl:attribute name="class">
                <xsl:value-of select="$Init"/><xsl:text>-content</xsl:text>
            </xsl:attribute>
            <ul>
            <xsl:for-each select="../database[substring(title_display,1,1)=$Init]">
                 <li><xsl:value-of select="title_display"/></li>
            </xsl:for-each>
            </ul>
            </div>
        </xsl:if>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

This works by looping through all the databases, but only emiting data for the first database that starts with each letter. It then selects all the items that start with that letter and handles them as a group.

Your other option is to go with the Muenchian method of grouping in xslt.

Eclipse
Thanks! I was, somehow, thinking I would have to do some bizarre recursion with templates to get this to work. It's often the simple solutions that elude us, right?
Tyson
This solution produces:<div class="A-content">but what is required is:<div class="a-content">Also, it can be quite inefficient -- O(N^2) if there are many "database" siblings.
Dimitre Novatchev
Which is why I mentioned the Muenchian method. Sometimes the slower but easier method is all that is needed.
Eclipse
A: 

Here's a case-insensitive version of Josh's solution:

<xsl:variable name="lower">abcdefghijklmnopqrstuvwxyz</xsl:variable>
<xsl:variable name="upper">ABCDEFGHIJKLMNOPQRSTUVWXYZ</xsl:variable>

<xsl:for-each select="databases/database">
    <xsl:variable name="Init" select="translate(substring(title_display,1,1), $lower, $upper)"/>
    <xsl:if test="not(preceding-sibling::*[translate(substring(title_display,1,1), $lower, $upper)=$Init])">
        <h2><xsl:value-of select="$Init"/></h2>
        <div>
            <xsl:attribute name="class">
                <xsl:value-of select="translate(substring($Init,1,1), $upper, $lower)"/><xsl:text>-content</xsl:text>
            </xsl:attribute>
            <ul>
                <xsl:for-each select="../database[translate(substring(title_display,1,1), $lower, $upper)=$Init]">
                    <li><xsl:value-of select="title_display"/></li>
                </xsl:for-each>
            </ul>
        </div>
    </xsl:if>
</xsl:for-each>
Tyson
+2  A: 

Here is a more efficient solution, using the classical Muenchian method for grouping -- with keys.

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:variable name="vLower" select=
 "'abcdefghijklmnopqrstuvwxyz'"/>

 <xsl:variable name="vUpper" select=
 "'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

 <xsl:key name="kTitleBy1stLetter" match="database"
  use="substring(title_display,1,1)"/>

    <xsl:template match="/*">
      <xsl:for-each select=
      "database
        [generate-id()
        =
         generate-id(key('kTitleBy1stLetter',
                         substring(title_display,1,1)
                         )[1]
                    )
        ]"
      >
        <xsl:variable name="v1st" 
         select="substring(title_display,1,1)"/>
        <h2><xsl:value-of select="$v1st"/></h2>
        <div class="{translate($v1st, 
                     $vUpper,
                     $vLower)}-content">
          <ul>
            <xsl:for-each select=
              "key('kTitleBy1stLetter',$v1st)">
               <li><xsl:value-of select="title_display"/></li>
            </xsl:for-each>
          </ul>
      </div>
      </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

When applied on the originally provided XML document:

<databases>
    <database>
        <title_display>Aardvark</title_display>
    </database>
    <database>
        <title_display>Apple</title_display>
    </database>
    <database>
        <title_display>Blue</title_display>
    </database>
    <database>
        <title_display>Car</title_display>
    </database>
</databases>

produces exactly the wanted result:

<h2>A</h2>
<div class="a-content">
   <ul>
      <li>Aardvark</li>
      <li>Apple</li>
   </ul>
</div>
<h2>B</h2>
<div class="b-content">
   <ul>
      <li>Blue</li>
   </ul>
</div>
<h2>C</h2>
<div class="c-content">
   <ul>
      <li>Car</li>
   </ul>
</div>

Do note, that the Muenchian method is hugely more efficient than the O(N^2) solution that uses comparison of all database elements on the preceding-sibling:: axis.

Also, this solution produces the class attributes value with the required capitalization.

Dimitre Novatchev