The XML SiteXSL templates for creating and updating numbered article Tables of Contents

Generating Tables of Contents

A Table of Contents is a nice addition to a web article, and also a great application for our framework. When done manually, TOCs are hard to create and maintain. They get quickly out of sync with the article's content and are very error prone. This is where XSLT comes to help.

A XSLT Table of Contents will automatically extract the entry text information (section titles), keep it updated, establish the indentation and number the sections. It should look something like this:

  • 1 First Section
    • 1.1 First Subsection of the first section
    • 1.2 Second Subsection of the first section
  • 2 Second Section

The XML Schema

We will start with some modifications to the existing schema definitions.

Section TypeThe section type

First of all, we need to allow toc elements in the article sections. The toc elements will be block-level elements, just like the paragraphs. They are optional and can appear in any order, number and position.

We'll modify the section type as follows:

      ...
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="toc" type="tocType"/>
        <xs:element name="paragraph" type="xs:string"/>
      </xs:choice>
      ...

TOC TypeThe toc type

The toc element only has two attributes:

  • global - a true/false boolean
  • style - of type list-style
  <xs:complexType name="tocType">
    <xs:attribute name="style" type="list-styleType" default="unordered"/>
    <xs:attribute name="global" type="xs:boolean" default="false"/>
  </xs:complexType>

The global toc attribute

The Table of Contents can be:

  • global - per the entire article
  • local (default) - includes only the sections contained in the parent section of the toc element

List-Style TypeThe list-style type

The list-style is an enumeration establishing how a hierarchical list should be created in HTML. A TOC is such a list.

There are three types of lists:

  • ordered - the OL tag will be used to create the list
  • unordered (default) - the UL tag will be used to create the list
  • numbered - the UL tag will be used to create the list, but the entries will be numbered using the same numbering abilities as when numbering section titles.
  <xs:simpleType name="list-styleType">
    <xs:restriction base="xs:string">
      <xs:enumeration value="unordered"/>
      <xs:enumeration value="ordered"/>
      <xs:enumeration value="numbered"/>
    </xs:restriction>
  </xs:simpleType>

Hiding the UL bullets for numbered lists gives a true TOC numbering "1.1." styles.

The Website XML content

Integrating the TOC in the article is very simple:

      <section>
        <title>Table of Contents</title>
        <toc style="numbered" global="true"/>
      </section>

The XSLT processing rules

We need to match and process the toc element, taking into consideration the set attributes.

The toc template

This template will be called for each toc element. It will prepare the parameters and call the do-toc template.

  <xsl:template match="toc">
    <xsl:call-template name="do-toc">
      <xsl:with-param name="entires"
        select="if (@global='true') then ancestor::article/section[title] else ../section[title]"/>
      <xsl:with-param name="element"
        select="if (@style='ordered') then 'ol' else 'ul'" tunnel="yes"/>
      <xsl:with-param name="numbered" select="@style='numbered'" tunnel="yes"/>
      <xsl:with-param name="global" select="@global='true'" tunnel="yes"/>
    </xsl:call-template>
  </xsl:template>

The entries parameter is the set of section elements which are recursively iterated and placed in the TOC. The initial entries set depends on if the TOC is global or local:

  • global TOC - the initial entry set represents all the titled sections included in the enclosing article.
  • local TOC - the initial entry set includes all the sibling titled section elements

The do-toc template

This template will be recursively called for each set of TOC entries. If the set is not empty, the list tags are outputted. Then, for each entry in the set:

  • a TOC item is outputted
  • a new entry set is constructed, containing all the child titled sections of the current section
  • the do-toc template is called on the new set
  <xsl:template name="do-toc">
    <xsl:param name="entires"/>
    <xsl:param name="element" tunnel="yes"/>
    <xsl:param name="numbered" tunnel="yes"/>
    <xsl:if test="$entires">
      <xsl:element name="{$element}">
        <xsl:for-each select="$entires">
          <li>
            <xsl:call-template name="title-number"/>
            <xsl:value-of select="title"/>
            <xsl:call-template name="do-toc">
              <xsl:with-param name="entires" select="section[title]"/>
            </xsl:call-template>
          </li>
        </xsl:for-each>
      </xsl:element>
    </xsl:if>
  </xsl:template>

The title-number template

This template numbers entries just like section titles. The numbering is turned on by the sheet parameter $numbered-sections because we should have TOC numbering whenever we have title numbering. Otherwise, it is turned on by the numbered list style type.

When numbering because we have numbered sections, the numbered-first-section sheet parameter controls if the outer most section is counted.

When opting for a numbered style, a global TOC causes us to number all the sections, while a local one only numbers title sections from the current level down.

  <xsl:template name="title-number">
    <xsl:param name="numbered" tunnel="yes"/>
    <xsl:param name="global" tunnel="yes"/>
    <xsl:choose>
      <xsl:when test="$numbered-sections">
        <xsl:choose>
          <xsl:when test="$numbered-first-section">
            <xsl:number level="multiple" count="section" format="1.1 "/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:number level="multiple" count="section/section" format="1.1 "/>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:when>
      <xsl:when test="$numbered">
        <xsl:choose>
          <xsl:when test="$global">
            <xsl:number level="multiple" count="section" format="1.1 "/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:number level="multiple" count="section[toc]//section"
              format="1.1 "/>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:when>
    </xsl:choose>
  </xsl:template>

Warning: This strategy fails if we have a toc element in a section that is descendant of another toc-element-holding section. But we shouldn't have that anyway - that type of overlapping TOC constructs don't make much sense in a single web page.

The title template

It only makes sense to use this new, more powerful numbering template for the section titles as well.

    ...
    <xsl:element name="h{$heading}">
      <xsl:call-template name="title-number"/>
      <xsl:apply-templates/>
    </xsl:element>
    ...

The HTML output

The generated TOC should look something like this:

      <h2>Table of Contents</h2>
      <ul>
         <li>1 Main Section
            <ul>
               <li>1.1 Table of Contents</li>
               <li>1.2 Sub-Section 1
                  <ul>
                     <li>1.2.1 Sub-Section 1.1
                        <ul>
                          ...

Further development

Additional features we will implement for Table of Contents when we have the link templates:

  • links from the TOC entries to the respective section titles
  • use the absence/presence of the anchor (section id) as TOC section inclusion criteria

Download: article files.

Read on: Links in Tables of Contents.

First Posted: December 19th, 2005 - Monday.
Last Updated: January 18th, 2006 - Wednesday.