Home
About
Projects
Articles
Resources
Music

AsciiDoc Website

Goal

I've been searching for usable authoring tools for a while. Word processors like Microsoft Word are nice, because you can immediately get HTML or something of reasonable print quality. But it's not so nice if you want to use a version control system like subversion for keeping your sources, if you want really high quality print output, or if you actually want features like styles, outlines, and auto-numbering to work consistently.

What's needed is a lightweight-markup system which will accept a text-based source document and generate output in the desired formats.

Since I'm building a website, I want website-like navigational structures. many available markup systems are oriented towards building documentation and books, which have different needs. For my website, I want html chunks with top-level and secondary-level navigation menus included in each chunk.

Lastly, the system must be flexible enough and provide customization methods to accommodate my needs.

Tools

asciidoc is a promising candidate, since it can generate output in both docbook and html formats. As noted above, it very document oriented. However, the docbook xsl transformations can produce chunked html and allow quite a bit of customization, so by producing docbook xml from my asciidoc and then transforming the docbook to html with a custom transformation layer included, I can build the site to my liking.

This requires the docbook xsl transformations, and of course access to Bob Stayton's indispensible guide, Docbook XSL: The Complete Guide. Also helpful will be the complete docbook xslt parameter reference on the sourceforge project site.

An XSLT processor is required. I use xsltproc.

Note

There is a modification of docbook for generating a website, but it unfortunately does not use docbook xml! I have a tool for generating docbook xml from text, but not website xml. So this is not a viable solution.

Method

The docbook xsl transformations can be easily customized through pre-defined parameters as well as by direct outright replacement of specific xslt templates. A customization layer is necessary for the latter and convenient for the former. The customization layer will import the desired templates and then override specific parameters and templates as necessary.

I'll call my stylesheet myproc.xsl. Here's what it looks like so far:

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:import href="/usr/share/docbook-xsl/xhtml/chunkfast.xsl"/>
</xsl:stylesheet>

Getting Chunky

The chunkfast stylesheet produces multiple html files from a single document. Bob Stayton describes chunking in chapter 7 of his Docbook XSLT Guide. By default, the html chunker produces a separate file for each level 1 section in the document. This means one page for each major division. I want one page for each subdivision, so I'll increase this by changing the chunk.section.depth parameter.

The chunker, by default, creates files numbered by logical position. I'll be generating an article type document, divided into sections which are further subdivided, also into sections. So the second subsection of the third section would be named ar01s03s02.html. I want filenames which convey more meaning than that, so I'll set the use.id.as.filename parameter. This will also help ensure that URL's will remain valid even if I make minor changes to the website organization.

Getting Style

I want easy control over the webpage look. I create a stylesheet for my webpage, and use the html.stylesheet parameter to link it in with my generated output.

By default, the docbook html stylesheets produce some html elements with inline style declarations. I disable this by setting css.decoration to zero. Stayton describes other styling options here.

So now the custom stylesheet looks as follows:

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:import href="/usr/share/docbook-xsl/xhtml/chunkfast.xsl"/>
    <!-- chunking options -->
<xsl:param name="chunk.section.depth" select="2"/>
<xsl:param name="use.id.as.filename" select="0"/>
<xsl:param name="chapter.autolabel" select="0"/>
        <!-- css options -->
    <xsl:param name="html.stylesheet" select="'style.css'"/>
    <xsl:param name="css.decoration" select="'0'"/>
</xsl:stylesheet>

Head to Toe

When generating chunked html output with docbook, each page has a navigation (previous and next) header and footer. Additionally, each section is prefixed with a table of contents. There are several parameters that control which sections will have a table of contents and to what depth, summed up nicely in Stayton's guide.

To make a website-style navigation bar, I need a TOC for the whole document at the top of each chunk. I want that followed by a section level TOC, where necessary. Each of these should be only one level deep. To do this, I will simply disable the standard TOC altogether and replace the navigational header with a table of contents. I set generate.toc to an empty string and toc.max.depth to 1.

To create my own navigational header, I need to define a custom xsl template which will override the standard one. Stayton describes the method here.

The docbook xsl set contains a named template header.navigation which generates the navigational header on each page. It also includes a named template make.toc which generates a table of contents. So I can create my own header with a simple template:

<xsl:template name="header.navigation">
    <div class="top-nav">
        <xsl:call-template name="make.toc">
            <xsl:with-param name="toc-context" select="/article"/>
            <xsl:with-param name="toc.title.p" select="false()"/>
            <xsl:with-param name="nodes" select="/article/section"/>
        </xsl:call-template>
    </div>
</xsl:template>

Here I passed as toc-context the root document element, since I want a top-level TOC. The nodes parameter specifies section elements to be included in the TOC. A true value for toc.title.p would instruct the template to prefix the title Table of Contents to the TOC, so I pass a false value.

This works fine, but there are two small problems. I would like the navigational header—and only the navigational header—to indicate the current location. The TOC I generated with the template above contains links to every section of the document. I want links to all sections except the current section. Ideally it would be wrapped in a <span> as opposed to an <a>. The other problem is the section title. By default, the chunker bundles the first subsection with the section title and prelude. That's more or less what I want, but without that section title.

Solving the second problem is actually easier, since I can just hide the section title with css. The titles are generated using the classic html title elements, so the document title is <h1> and the section titles are <h2> and so forth. I just add the following to my css:

h2.title { display:none; }

The first problem is somewhat more difficult. I solved it by storing the output of the make.toc template in an xsl variable and then using the extended xslt function exsl:node-set() to apply my own filter to the node tree returned in that variable, so I must also include the exsl namespace in the stylesheet header.

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:exsl="http://exslt.org/common"
                version="1.0">
    <xsl:import href="/usr/share/docbook-xsl/xhtml/chunkfast.xsl"/>

            <!-- chunking options -->
    <xsl:param name="chunk.section.depth" select="2"/>
    <xsl:param name="use.id.as.filename" select="1"/>

            <!-- css options -->
    <xsl:param name="html.stylesheet" select="'style.css'"/>
    <xsl:param name="css.decoration" select="'0'"/>

            <!-- toc options -->
    <xsl:param name="toc.max.depth" select="1"/>
    <xsl:param name="generate.toc" select="''"/>

            <!-- custom header menu / top-level TOC -->
    <xsl:template name="header.navigation">
      <div class="top-nav">
        <xsl:variable name="tocTree">
          <xsl:call-template name="make.toc">
              <xsl:with-param name="toc-context" select="/article"/>
              <xsl:with-param name="toc.title.p" select="false()"/>
              <xsl:with-param name="nodes" select="/article/section"/>
          </xsl:call-template>
        </xsl:variable>
        <xsl:apply-templates select="exsl:node-set($tocTree)" mode="filter.links">
          <xsl:with-param name="filter">
                <xsl:call-template name="href.target">
                  <xsl:with-param name="object"
                      select="ancestor-or-self::section[parent::article]"/>
                  <xsl:with-param name="context"
                      select="ancestor-or-self::section[parent::article]"/>
                </xsl:call-template>
          </xsl:with-param>
        </xsl:apply-templates>
      </div>
    </xsl:template>

        <!-- filter for changing link to current page to span -->
    <xsl:template match="@*|node()" mode="filter.links">
      <xsl:param name="filter"/>
      <xsl:choose>
        <xsl:when test="self::a[@href=$filter]">
          <span>
            <xsl:apply-templates select="@*|node()" mode="filter.links">
              <xsl:with-param name="filter" select="$filter"/>
            </xsl:apply-templates>
          </span>
        </xsl:when>
        <xsl:otherwise>
          <xsl:copy>
            <xsl:apply-templates select="@*|node()" mode="filter.links">
              <xsl:with-param name="filter" select="$filter"/>
            </xsl:apply-templates>
          </xsl:copy>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

The filter takes as a parameter the URL of the current section which it compares against the href attribute of all <a> elements in the node tree. I can determine the URL for a given element using the docbook template href.target, but first I have to locate the right element. I am chunking both sections and subsections, so for a giving chunk, the level 1 section element could be the current element or a parent. Hence the XPath

ancestor-or-self::section[parent::article]

This doesn't catch the correct TOC entry on the index.html front page, because the XPath doesn't match the top level element /article. In that case the filter parameter will be an empty string. This can be addressed by changing the test expression in the filter template:

<xsl:when test="(string-length($filter) > 0 and self::a[@href=$filter])
    or (string-length($filter) = 0 and self::a[@href='index.html'])">

Secondary Navigation

Next I want to generate a secondary-level navigation menu on the side of the page. I'll use the same method as above to process a second-level toc and add that to the navigation header below the main menu. It's important to note, at this point that the first subsection is included on a single page with the beginning of the section. This can be changed, but it's the behavior I want for now. However, this means that the link for the first subsection of each section has an anchor appended (i.e. _articles.html#_website) In order to make the filter to work, I'll use the XPath contains() function instead of the equality test.

Finishing Touches

Lastly, I add a footer.navigation template with a copyright notice.

Now that I have the desired content on the page, I need to style it. I won't go through all the details, just the main points. The following css snippet makes the top level navigation menu appear horizontally across the top, and the second level menu on the side:

.top-nav .toc dt { display:inline; }
.sub-nav .toc    { float:left; }

I indent the content, so it won't wrap underneath the side menu.

.sub-nav .toc dl { width:150px; }
body > div.section { margin-left:175px;}

Download code