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.
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.
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.
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>
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.
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>
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'])">
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.
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;}