Re: [xsl] Converting a Table of Contents to an index

Subject: Re: [xsl] Converting a Table of Contents to an index
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Thu, 11 Apr 2002 09:51:22 +0100
Hi Jim,

> I am trying to invert a tree - specifically to create an index for a
> document by pulling the leaves to the top and grouping them. A
> classic problem that I am just having a mental block on . . .
> ARGHHHH!!!

Classic indeed :) This is a grouping problem, where you want to group
the para elements throughout your document, first by their index and
second by their chapter. There are two things twists on normal
grouping that might be tripping you up here -- the fact that the para
elements each belong to more than one group, and the fact that there
are two levels of grouping.

The first level key has to group the para elements by their index.
However, each para element has more than one index. In the Muenchian
approach, you have to have a one-to-one mapping between the things you
group and the values you group them by. So in this situation, you
actually have to index the *index* elements by their value:

<xsl:key name="entries" match="para/index" use="." />

This enables you to get the first level of the grouping using the
normal method (see http://www.jenitennison.com/xslt/grouping):

<xsl:template match="book">
  <index>
    <xsl:for-each select="chapter/para/index
                            [count(.|key('entries', .)[1])) = 1]">
      <xsl:sort select="." />
      <entry>
        <text><xsl:value-of select="." /></text>
        ...
      </entry>
    </xsl:for-each>
  </index>
</xsl:template>

For the second level of the grouping, you need a key that groups
together the para elements by both their index entry and their
chapter. Again, it needs to be a one-to-one mapping so you actually
need to match the *index* elements. From the looks of it, you can use
the title of the chapter as an identifier for each chapter (otherwise
generate an id using generate-id()):

<xsl:key name="entries-by-chapter" match="para/index"
         use="concat(., '+', ancestor::chapter/@title)" />

You don't need to use this key to find the chapters for each index --
you can do that with a simple path up the ancestor axis:

<xsl:template match="book">
  <index>
    <xsl:for-each select="chapter/para/index
                            [count(.|key('entries', .)[1])) = 1]">
      <xsl:sort select="." />
      <entry>
        <text><xsl:value-of select="." /></text>
        
        <xsl:for-each select="key('entries', .)/ancestor::chapter">
          <referenceChapter name="{@title}">
            ...
          </referenceChapter>
        </xsl:for-each>

      </entry>
    </xsl:for-each>
  </index>
</xsl:template>

However, you can use this key to locate the index elements within the
chapter that have the entry that you're interested in, so that you can
create the referencePara elements:

<xsl:template match="book">
  <index>
    <xsl:for-each select="chapter/para/index
                            [count(.|key('entries', .)[1])) = 1]">
      <xsl:sort select="." />
      <xsl:variable name="entry" select="." />
      <entry>
        <text><xsl:value-of select="$entry" /></text>
        
        <xsl:for-each select="key('entries', $entry)/ancestor::chapter">
          <referenceChapter name="{@title}">

            <xsl:for-each select="key('entries-by-chapter',
                                      concat($entry, '+', @title)">
              <referencePara>
                <xsl:number count="para" />
              </referencePara>
            </xsl:for-each>

          </referenceChapter>
        </xsl:for-each>

      </entry>
    </xsl:for-each>
  </index>
</xsl:template>

---

In XSLT 2.0, you could do this grouping with:

<xsl:template match="book">
  <index>
    <xsl:for-each-group select="chapter/para/index" group-by=".">
      <xsl:sort select="." />
      <entry>
        <text><xsl:value-of select="." /></text>
        <xsl:for-each-group select="current-group()"
                            group-by="ancestor::chapter/@title">
          <referenceChapter name="{ancestor::chapter/@title}">
            <xsl:for-each select="current-group()">
              <referencePara>
                <xsl:number count="para" />
              </referencePara>
            </xsl:for-each>
          </referenceChapter>
        </xsl:for-each-group>
      </entry>
    </xsl:for-each-group>
  </index>
</xsl:template>

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread