XSL template for Generating Per Page TOC for Docbook HTML Output

Sometime back I worked on this upcoming Ruby On Rails Book and helped the author convert his docbook to various formats like HTML, PDF, EPUB, Kindle etc. While working with the HTML output, I figured that the standard output spewed out by the Docbook XSL stylesheets is quite boring. So I improvised a lil, added per page TOC, pretty css, icons etc. I spent quite some time on the per page TOC feature. Here’s the XSL template that I wrote for the TOC.

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:d="http://docbook.org/ns/docbook" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl" exclude-result-prefixes="exsl d" version="1.0">
  <xsl:template name="breadcrumbs">
    <xsl:param name="this.node" select="."/>
    <ul class="breadcrumbs">
      <xsl:for-each select="$this.node/ancestor::*">
        <xsl:variable name="node.is.component">
          <xsl:call-template name="is.component">
            <xsl:with-param name="node" select="."/>
          </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="this.is.component">
          <xsl:call-template name="is.component">
            <xsl:with-param name="node" select="$this.node"/>
          </xsl:call-template>
        </xsl:variable>
        <xsl:if test="$node.is.component != 0 or count(ancestor-or-self::*) = 2">
          <xsl:call-template name="print-previous-components">
            <xsl:with-param name="context" select="$this.node"/>
            <xsl:with-param name="temp-name" select="'print-previous-main-loop-first'"/>
          </xsl:call-template>
        </xsl:if>
        <xsl:call-template name="breadcrumb.href.target">
          <xsl:with-param name="context" select="$this.node"/>
          <xsl:with-param name="temp-name" select="'breadcrumb-first-level'"/>
        </xsl:call-template>
        <xsl:if test="position() = last()">
          <!-- If the selected node is a component or a level2 link, print its previous siblings-->
          <xsl:if test="$this.is.component != 0 or count($this.node/ancestor-or-self::*) = 2">
            <xsl:call-template name="print-previous-components">
              <xsl:with-param name="context" select="$this.node"/>
              <xsl:with-param name="node" select="$this.node"/>
              <xsl:with-param name="temp-name" select="'print-previous-main-loop-second'"/>
            </xsl:call-template>
          </xsl:if>
          <xsl:call-template name="main-breadcrumb-plus-neighbours">
            <xsl:with-param name="node" select="$this.node"/>
            <xsl:with-param name="context" select="$this.node"/>
          </xsl:call-template>
          <!-- If the selected node is not a component or a level2 link, 
            print all the components or level 2 links which are following siblings of its ancestor-->
          <xsl:if test="$node.is.component != 0 or $this.is.component != 0 or count($this.node/ancestor-or-self::*) = 2">
            <xsl:call-template name="print-following-components">
              <xsl:with-param name="context" select="$this.node"/>
              <xsl:with-param name="node" select="$this.node"/>
            </xsl:call-template>
          </xsl:if>
        </xsl:if>
      </xsl:for-each>
      <xsl:if test="count($this.node/ancestor::*) = 0">
        <li class="breadcrumb-text highlight">
          <xsl:apply-templates select="$this.node" mode="title.markup"/>
        </li>
        <xsl:for-each select="$this.node/child::*[local-name() != 'bookinfo' and local-name() != 'info' and local-name() != 'articleinfo']">
          <xsl:call-template name="breadcrumb.href.target">
            <xsl:with-param name="object" select="."/>
            <xsl:with-param name="context" select="$this.node"/>
            <xsl:with-param name="temp-name" select="'indexchild'"/>
          </xsl:call-template>
        </xsl:for-each>
      </xsl:if>
    </ul>
  </xsl:template>
  
  <xsl:template name="print-following-components">
    <xsl:param name="node" select="."/>
    <xsl:param name="context" select="."/>
    <xsl:param name="temp-name" select="'pfc'"/>
    <xsl:for-each select="$node/ancestor-or-self::*">
      <!-- reversing the node set -->
      <xsl:sort select="position()" data-type="number" order="descending"/>
        <xsl:for-each select="following-sibling::*">  
          <xsl:call-template name="breadcrumb.href.target">
            <xsl:with-param name="object" select="."/>
            <xsl:with-param name="context" select="$context"/>
            <xsl:with-param name="temp-name" select="$temp-name"/>
          </xsl:call-template>
        </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

  <xsl:template name="print-previous-components">
    <xsl:param name="node" select="."/>
    <xsl:param name="context" select="."/>
    <xsl:param name="temp-name" select="'ppc'"/>
    <xsl:for-each select="$node/preceding-sibling::*">
      <xsl:if test="local-name(.) != 'info' and local-name(.) != 'bookinfo' and local-name() != 'articleinfo'">
        <xsl:call-template name="breadcrumb.href.target">
          <xsl:with-param name="object" select="."/>
          <xsl:with-param name="context" select="$context"/>
          <xsl:with-param name="temp-name" select="$temp-name"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>

  <xsl:template name="main-breadcrumb-plus-neighbours">
  <!--<xsl:param name="node" select="."/>-->
  <!--<xsl:param name="context" select="."/>-->
    <xsl:param name="node"/>
    <xsl:param name="context"/>
<!-- And display the current node, but not as a link -->
    <xsl:variable name="node.is.component">
      <xsl:call-template name="is.component">
        <xsl:with-param name="node" select="$node"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:if test="$node.is.component = 0 and count($node/ancestor-or-self::*) > 2">
      <xsl:call-template name="print-previous-components">
        <xsl:with-param name="context" select="$context"/>
        <xsl:with-param name="node" select="$node"/>
        <xsl:with-param name="temp-name" select="'print-previous-main-breadcrumb-preceding'"/>
      </xsl:call-template>
    </xsl:if> 
    <xsl:call-template name="breadcrumb.href.target">
      <xsl:with-param name="object" select="$node"/>
      <xsl:with-param name="context" select="$context"/>
      <xsl:with-param name="temp-name" select="'main-breadcrumb'"/>
    </xsl:call-template>

    <xsl:for-each select="$node/child::*[position() != 1]">
      <xsl:call-template name="breadcrumb.href.target">
        <xsl:with-param name="context" select="$node"/>
        <xsl:with-param name="temp-name" select="'main-breadcrumb-link'"/>
      </xsl:call-template>
    </xsl:for-each>
  </xsl:template>

  <xsl:template name="breadcrumb.href.target">
    <xsl:param name="object" select="."/>
    <xsl:param name="context" select="."/>
    <xsl:param name="temp-name" select="'bhf'"/>
    <xsl:if test="$object/child::*[local-name() = 'title'] or $object/info/child::*[local-name() = 'title'] or local-name($object) = 'set' or local-name($object) = 'partintro' or local-name($object) = 'book' or local-name($object) = 'article'">
      <li>
        <xsl:attribute name="class">
          <xsl:call-template name="breadcrumb-classes">
            <xsl:with-param name="node" select="$object"/>
            <xsl:with-param name="context" select="$context"/>
            <xsl:with-param name="temp-name" select="$temp-name"/>
          </xsl:call-template>
        </xsl:attribute>
        <xsl:choose>
          <xsl:when test="$object = $context">
            <xsl:apply-templates select="$object" mode="title.markup"/>
          </xsl:when>
          <xsl:otherwise>
            <a>
              <xsl:attribute name="href">
                <xsl:call-template name="href.target">
                  <xsl:with-param name="object" select="$object"/>
                  <xsl:with-param name="context" select="$context"/>
                </xsl:call-template>
              </xsl:attribute>
              <xsl:attribute name="title">
                <xsl:call-template name="breadcrumb-classes">
                  <xsl:with-param name="node" select="$object"/>
                  <xsl:with-param name="context" select="$context"/>
                  <xsl:with-param name="temp-name" select="$temp-name"/>
                </xsl:call-template>
              </xsl:attribute>
              <xsl:apply-templates select="$object" mode="title.markup"/>
            </a>
          </xsl:otherwise>
        </xsl:choose>
      </li>
    </xsl:if>
  </xsl:template>

  <xsl:template name="breadcrumb-classes">
    <xsl:param name="node" select="."/>
    <xsl:param name="context" select="."/>
    <xsl:param name="temp-name">
      <xsl:text></xsl:text>
    </xsl:param>
    <xsl:value-of select="$temp-name"/>
    <xsl:text> level</xsl:text>
    <xsl:value-of select="count($node/ancestor-or-self::*)"/>
    <xsl:text> breadcrumb-link </xsl:text>
    <xsl:value-of select="local-name($node)"/>
    <xsl:if test="$node = $context">
      <xsl:text> highlight </xsl:text>  
    </xsl:if>
    <xsl:variable name="node.is.section">
      <xsl:call-template name="is.section">
        <xsl:with-param name="node" select="$node"/>
      </xsl:call-template>
    </xsl:variable>           
    <xsl:if test="$node.is.section != 0">
      <xsl:variable name="sectionlevel">
        <xsl:text> sectlevel</xsl:text>
        <xsl:call-template name="section.level">
          <xsl:with-param name="node" select="$node"/>
        </xsl:call-template>
      </xsl:variable>
      <xsl:value-of select="$sectionlevel"/>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

You could either save this code in a separate file and include it in your customization layer.

<xsl:include href="breadcrumbs.xsl" />

or simply copy-paste the whole template into your customization layer.

The xsl:call-template will typically go into the chunk-element-content template customization.

<xsl:call-template name="breadcrumbs"/>

About Pratik Sinha

Linux Nerd, Socialist, Atheist, Adventuristic, Nature Lover, Geeky.

Leave a Reply