WeavePart of
Literate Programming in XML05 Oct 2001
$Id: weave.xweb,v 1.5 2002/03/15 18:12:01 nwalsh Exp $
0.105 Oct 2001ndwInitial draft.NormanWalshThe weave.xsl stylesheet transforms an
XWEB document into a documentation document. This
is accomplished by weaving the documentation from
the XWEB file with a pretty-printed version of the source code.The resulting document is ready to be processed by whatever
down-stream publishing tools are appropriate.The StylesheetThe stylesheet performs some initialization, begins processing
at the root of the XWEB document, and processes fragments and
elements. This stylesheet also requires some recursive templates that
are stored at the end of the stylesheet.<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="xsl src xml"
version="1.0">
</xsl:stylesheet>InitializationThe stylesheet initializes the processor by loading its version
information (stored in a separate file because it is shared by several
stylesheets), telling the processor to preserve whitespace on all
input elements, setting the output method, and initializing
the excluded result prefixes.The stylesheet also constructs a key for the ID values used on
fragments. Because XWEB documents do not have to be valid according
to any particular DTD or Schema, the stylesheet cannot rely on having
the IDs identified as type ID in the source document. <xsl:include href="VERSION"/>
<xsl:preserve-space elements="*"/>
<xsl:output method="xml"/>
<xsl:key name="fragment"
match="src:fragment"
use="@id"/>
<xsl:param name="top"
select="'top'"/>Default Exclude Result PrefixesGenerally, the namespace declarations for namespaces used by
the source code portion of the XWEB file are not needed in the
woven documentation. To reduce the size of the documentation
file, and to reduce the clutter of unnecessary declarations, you can
specify prefixes that should be excluded.This is done as a parameter so that it can be adjusted dynamically,
though it rarely needs to be. The initial value comes from the
mundane-result-prefixes
attribute on the XWEB file's top src:fragment.<xsl:param name="mundane-result-prefixes"
select="key('fragment',$top)/@mundane-result-prefixes"/>Named TemplatesCorrectly copying elements requires the ability to calculate
applicable namespaces and output the appropriate namespace psuedo-attributes
and attributes. These templates accomplish those tasks.Root TemplateThe root template begins processing at the root of the XWEB
document. It outputs a couple of informative comments and then
processes the document.Source code fragments in the XWEB document are not required
to be sequential, we assume that they appear in the order in which
they should be documented.<xsl:template match="/">
<xsl:text>
</xsl:text>
<xsl:comment>
<xsl:text> This file was generated by weave.xsl version </xsl:text>
<xsl:value-of select="$VERSION"/>
<xsl:text>. Do not edit! </xsl:text>
</xsl:comment>
<xsl:text>
</xsl:text>
<xsl:comment> See http://sourceforge.net/projects/docbook/ </xsl:comment>
<xsl:apply-templates/>
</xsl:template>FragmentsThe goal when copying the source code fragments
is to preserve the src:fragment
elements in the documentation file (so that they can be formatted
appropriately) but to escape all of the fragment content so that
it appears simply as text in the documentation.For example, if the following fragment appears in the XWEB file:
<src:fragment id="foo">
<emphasis>some code</emphasis>
</src:fragment>the documentation must contain:<src:fragment id="foo">
<emphasis>some code</emphasis>
</src:fragment>The significance of this escaping is less obvious when the
fragment contains non-XML code, but it is in fact still relevant.This task is accomplished by constructing a literal
src:fragment element and then copying the content
of the source document's src:fragment element
in a mode that escapes all markup characters.<xsl:template match="src:fragment">
<src:fragment id="{@id}">
<xsl:call-template name="copy-content"/>
</src:fragment>
</xsl:template>
Copying ContentThe copy-content template
could be as simple as:<xsl:apply-templates mode="copy"/>but we play one more trick for the convenience of XWEB authors.
It's convenient for authors to use newlines at the beginning
and end of each program fragment, producing fragments that look like
the one shown above. The problem is that white space is significant
inside fragments, so the resulting documenation will contain a listing
like this: 1 |
2 | <emphasis>some code</emphasis>
3 |The leading and trailing blank lines in this listing are distracting
and almost certainly insignificant. Authors can avoid this problem by
removing the offending newlines:<src:fragment id="foo"><emphasis>some code</emphasis></src:fragment>but this makes the source document more difficult to read and
introduces tedious cut-and-paste problems. To avoid this problem, the
copy-content template takes special
pains to trim off one optional leading newline and one optional
trailing newline. It does this by dealing with the first, last, and
middle nodes of the src:fragment elements
separately:<xsl:template name="copy-content">
</xsl:template>Convenience VariablesFor convenience, we store subexpressions containing the first,
last, and all the middle nodes in variables. <xsl:variable name="first-node"
select="node()[1]"/>
<xsl:variable name="middle-nodes"
select="node()[position() > 1 and position() < last()]"/>
<xsl:variable name="last-node"
select="node()[position() > 1 and position() = last()]"/>Handle First NodeHandling the leading newline is conceptually a simple matter of
looking at the first character on the line and skipping it if it is
a newline. A slight complexity is introduced by the fact that if the
fragment contains only a single text node, the first node is also the
last node and we have to possibly trim off a trialing newline as well.
We separate that out as a special case.
<xsl:choose>
</xsl:choose>Handle A Fragment that Contains a Single NodeIf the $first-node is a text node and the
fragment contains only a single child, then it is also the last node.In order to deal with a single text node child, we must address
four cases: the node has both leading and trailing newlines, the node
has only leading newlines, only trailing newlines, or no newlines at
all. <xsl:when test="$first-node = text() and count(node()) = 1">
<xsl:choose>
</xsl:choose>
</xsl:when>More Convenience VariablesFor convenience, we calculate whether or not the node in question
has leading and/or trailing newlines and store those results in variables.
<xsl:variable name="leading-nl"
select="substring($first-node, 1, 1) = '
'"/>
<xsl:variable name="trailing-nl"
select="substring($first-node, string-length($first-node), 1) = '
'"/>Handle a Single Node With Leading and Trailing NewlinesIf the node has both leading and trailing newlines, trim a character
off each end. <xsl:when test="$leading-nl and $trailing-nl">
<xsl:value-of select="substring($first-node, 2, string-length($first-node)-2)"/>
</xsl:when>Handle a Single Node With Only Leading NewlinesIf the node has only leading newlines, trim off the first character.
<xsl:when test="$leading-nl">
<xsl:value-of select="substring($first-node, 2)"/>
</xsl:when>Handle a Single Node with Only Trailing NewlinesIf the node has only trailing newlines, trim off the last character.
<xsl:when test="$trailing-nl">
<xsl:value-of select="substring($first-node, 1, string-length($first-node)-1)"/>
</xsl:when>Handle a Single Node with No NewlinesOtherwise, the node has no newlines and it is simply printed.
<xsl:otherwise>
<xsl:value-of select="$first-node"/>
</xsl:otherwise>Handle a First Node with a Leading NewlineIf the first node is a text node and begins with a newline,
trim off the first character. <xsl:when test="$first-node = text() and substring($first-node, 1, 1) = '
'">
<xsl:value-of select="substring($first-node, 2)"/>
</xsl:when>Handle a First Node without a Leading NewlineOtherwise, the first node is not a text node or does not begin
with a newline, so use the copy mode to copy it to
the result tree. <xsl:otherwise>
<xsl:apply-templates select="$first-node"
mode="copy"/>
</xsl:otherwise>Handle Last NodeHandling the last node is roughly analagous to handling the first
node, except that we know this code is only evaluated if the last node
is not also the first node.If the last node is a text node and ends with a newline, strip
it off. Otherwise, just copy the content of the last node
using the copy mode.
<xsl:choose>
<xsl:when test="$last-node = text() and substring($last-node, string-length($last-node), 1) = '
'">
<xsl:value-of select="substring($last-node, 1, string-length($last-node)-1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="$last-node"
mode="copy"/>
</xsl:otherwise>
</xsl:choose>Handle the Middle NodesThe middle nodes are easy, just copy them
using the copy mode. <xsl:apply-templates select="$middle-nodes"
mode="copy"/>Fragment ReferencesFragment references, like fragments, are simply copied to the
documentation file. The use of
disable-output-escaping is
unique to this template (it instructs the tangle
stylesheet to make a literal copy of the src:fragref,
rather than expanding it, as it usually would).<xsl:template match="src:fragref">
<src:fragref linkend="{@linkend}"/>
<xsl:apply-templates/>
</src:fragref>
</xsl:template>Copying ElementsCopying elements to the result tree can be divided into four
cases: copying passthrough elements,
copying fragment references and
copying everything else.Copying src:passthroughPassthrough elements contain text that is intended to appear
literally in the result tree. We simply copy it through.
<xsl:template match="src:passthrough"
mode="copy"
priority="3">
<xsl:apply-templates select="node()|@*"
mode="copy"/>
</xsl:template>Copying src:fragrefBecause tangle and
weave are XSLT stylesheets that process
XSLT stylesheets, processing src:fragref poses
a unique challenge.In ordinary tangle processing, they
are expanded and replaced with the content of the fragment that they
point to. But when weave.xweb is tangled, they
must be copied through literally. The
disable-output-escaping attribute
provides the hook that allows this.
When we're weaving, if the
disable-output-escaping attribute
is yes, the src:fragref is treated literally.
When it isn't, the element is copied through literally.<xsl:template match="src:fragref"
mode="copy"
priority="3">
<xsl:choose>
<xsl:when test="@disable-output-escaping='yes'">
<xsl:text><src:fragref linkend="</xsl:text>
<xsl:value-of select="@linkend"/>
<xsl:text>"/></xsl:text>
<xsl:apply-templates mode="copy"/>
<xsl:text></src:fragref></xsl:text>
</xsl:when>
<xsl:otherwise>
<src:fragref linkend="{@linkend}"/></src:fragref>
</xsl:otherwise>
</xsl:choose>
</xsl:template>Copying Everything ElseThere are two kinds of everything else: elements and other
nodes.This element template is quite complex, but it's goal is simple:
to translate bona-fide elements in the source document into text in
the result document. In other words, where the element
<foo> occurs in the source
document, the result document should contain
<foo>.Three things make this tricky:Elements in the source documents may have namespace
nodes associated with them that are not explicitly declared on them.
To the best of our ability, we must avoid copying these namespace
nodes to the result tree.
Attributes must be copied and formatted in some reasonable
way in order to avoid excessively long lines in the documentation.
Empty elements must be printed using the appropriate
empty-element syntax. (It is simply impossible to determine what syntax
was used in the source document, the best we can do is always use the
empty-element syntax in the result as it is likely to be more common
in the soruce.)
The plan of attack is:Calculate what namespaces should be excluded (by prefix).
Calculate the applicable namespaces.
Output the leading < and the element
name.
Output the applicable namespaces.
Output the attributes.
If the element is not empty, finish the start tag,
copy the element contents, and output and end tag. If the element is
empty, finish the start tag with the empty-element syntax.
<xsl:template match="processing-instruction()"
mode="copy"
priority="2">
<xsl:text><?</xsl:text>
<xsl:value-of select="name(.)"/>
<xsl:value-of select="."/>
<xsl:text>?></xsl:text>
</xsl:template>
<xsl:template match="comment()"
mode="copy"
priority="2">
<xsl:text><!--</xsl:text>
<xsl:value-of select="."/>
<xsl:text>--></xsl:text>
</xsl:template>
<xsl:template match="*"
mode="copy"
priority="2">
<xsl:variable name="exclude">
</xsl:variable>
<xsl:variable name="applicable.namespaces">
<xsl:call-template name="count.applicable.namespaces">
<xsl:with-param name="namespaces"
select="namespace::*"/>
<xsl:with-param name="exclude-prefixes"
select="$exclude"/>
<xsl:with-param name="exclude-uri"
select="'http://nwalsh.com/xmlns/litprog/fragment'"/>
</xsl:call-template>
</xsl:variable>
<xsl:text><</xsl:text>
<xsl:value-of select="name(.)"/>
<xsl:if test="$applicable.namespaces > 0">
<xsl:call-template name="output.applicable.namespaces">
<xsl:with-param name="namespaces"
select="namespace::*"/>
<xsl:with-param name="exclude-prefixes"
select="$exclude"/>
<xsl:with-param name="exclude-uri"
select="'http://nwalsh.com/xmlns/litprog/fragment'"/>
</xsl:call-template>
</xsl:if>
<xsl:choose>
<xsl:when test="node()">
<xsl:text>></xsl:text>
<xsl:apply-templates select="node()"
mode="copy"/>
<xsl:text></</xsl:text>
<xsl:value-of select="name(.)"/>
<xsl:text>></xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>/></xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>The preceding template handles elements. Everything else is simply
copied.<xsl:template match="node()|@*"
mode="copy">
<xsl:copy>
<xsl:apply-templates select="@*|node()"
mode="copy"/>
</xsl:copy>
</xsl:template>Calculate Excluded PrefixesCalculating the excluded prefixes requires evaluating the following
conditions:If the element we are copying is inside a
src:fragment element that specifies a set of
mundane-result-prefixes, use
those prefixes.
Otherwise, use the
$mundane-result-prefixes we
calculated earlier.
Note that in every case we exclude the namespace associated
with xml. <xsl:choose>
<xsl:when test="ancestor::src:fragment/@mundane-result-prefixes">
<xsl:value-of select="concat(' xml ', ancestor::src:fragment/@exclude-result-prefixes, ' ')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat(' xml ', $mundane-result-prefixes, ' ')"/>
</xsl:otherwise>
</xsl:choose>Output AttributesThe mechanics of outputting the applicable attributes is
described in . The
only wrinkle here is that if we have already output
xmlns declarations for namespaces,
the first real attribute is not really the first thing that looks
like an attribute in the result. <xsl:choose>
<xsl:when test="$applicable.namespaces > 0">
<xsl:call-template name="output.applicable.attributes">
<xsl:with-param name="attributes"
select="attribute::*"/>
<xsl:with-param name="first"
select="'0'"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="output.applicable.attributes">
<xsl:with-param name="attributes"
select="attribute::*"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>Count Applicable NamespacesThe applicable namespaces are determined by walking recursively
over the list of namespace nodes associated with an element.For each namespace node, if it has a prefix that is in the list
of excluded prefixes or if it is the Literate Programming namespace,
it is not counted (because it will not be output). Otherwise, it is
counted.The recursion bottoms out when the list of namespace nodes has
been exhausted. The total number of counted namespaces is then
returned.<xsl:template name="count.applicable.namespaces">
<xsl:param name="namespaces"
select="namespace::*"/>
<xsl:param name="exclude-prefixes"
select="''"/>
<xsl:param name="exclude-uri"
select="'http://nwalsh.com/xmlns/litprog/fragment'"/>
<xsl:param name="count"
select="'0'"/>
<xsl:choose>
<xsl:when test="count($namespaces) = 0">
<xsl:value-of select="$count"/>
</xsl:when>
<xsl:when test="not(contains($exclude-prefixes, name($namespaces[1])) or ($namespaces[1] = $exclude-uri))">
<xsl:call-template name="count.applicable.namespaces">
<xsl:with-param name="namespaces"
select="$namespaces[position()>1]"/>
<xsl:with-param name="exclude-prefixes"
select="$exclude-prefixes"/>
<xsl:with-param name="exclude-uri"
select="$exclude-uri"/>
<xsl:with-param name="count"
select="$count + 1"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="count.applicable.namespaces">
<xsl:with-param name="namespaces"
select="$namespaces[position()>1]"/>
<xsl:with-param name="exclude-prefixes"
select="$exclude-prefixes"/>
<xsl:with-param name="exclude-uri"
select="$exclude-uri"/>
<xsl:with-param name="count"
select="$count"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>Output Applicable Attributes and Pseudo-AttributesOutputing the attributes (or namespace psuedo-attributes) is
straightforward, the only tricky part is pretty-printing the resulting
document.Pretty-printing has three cases:Before outputting the very first
attribute or psuedo-attribute, we want to output only a single space,
to separate the result from the preceding element name.
Before outputting any additional attribute or
psuedo-attribute, we want to output a line-feed and then indent the
result appropriately. This prevents the attributes and psuedo-attributes
from appearing as one huge, long line in the result.
If the element has no attributes or psuedo attributes,
we don't want to output anything; we want the closing tag delimiter
to appear immediately after the element name.
Output Applicable NamespacesThe applicable namespaces are determined by walking recursively
over the list of namespace nodes associated with an element.For each namespace node, if it has a prefix that is in the list
of excluded prefixes or if it is the Literate Programming namespace,
it is not output, otherwise, it is.The recursion bottoms out when the list of namespace nodes has
been exhausted.<xsl:template name="output.applicable.namespaces">
<xsl:param name="namespaces"
select="namespace::*"/>
<xsl:param name="exclude-prefixes"
select="''"/>
<xsl:param name="exclude-uri"
select="'http://nwalsh.com/xmlns/litprog/fragment'"/>
<xsl:param name="first"
select="'1'"/>
<xsl:choose>
<xsl:when test="count($namespaces) = 0">
<!-- do nothing -->
</xsl:when>
<xsl:when test="not(contains($exclude-prefixes, name($namespaces[1])) or ($namespaces[1] = $exclude-uri))">
<xsl:text>xmlns</xsl:text>
<xsl:if test="name($namespaces[1]) != ''">:</xsl:if>
<xsl:value-of select="name($namespaces[1])"/>
<xsl:text>="</xsl:text>
<xsl:value-of select="$namespaces[1]"/>
<xsl:text>"</xsl:text>
<xsl:call-template name="output.applicable.namespaces">
<xsl:with-param name="namespaces"
select="$namespaces[position()>1]"/>
<xsl:with-param name="exclude-prefixes"
select="$exclude-prefixes"/>
<xsl:with-param name="exclude-uri"
select="$exclude-uri"/>
<xsl:with-param name="first"
select="0"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="output.applicable.namespaces">
<xsl:with-param name="namespaces"
select="$namespaces[position()>1]"/>
<xsl:with-param name="exclude-prefixes"
select="$exclude-prefixes"/>
<xsl:with-param name="exclude-uri"
select="$exclude-uri"/>
<xsl:with-param name="count"
select="$first"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>Indent AttributeIf this is not the first attribute or pseudo-attribute, output a
newline and then indent an appropriate amount. Otherwise, simply output
a space. <xsl:choose>
<xsl:when test="$first = 0">
<xsl:text>
</xsl:text>
<xsl:call-template name="indent"/>
</xsl:when>
<xsl:otherwise>
<xsl:text> </xsl:text>
</xsl:otherwise>
</xsl:choose>Indenting is accomplished by outputting a series of spaces. The
number of spaces is determined by the length of the name of the current
element plus two (one for the leading < and one for
the space that separates the name from the first attribute).<xsl:template name="indent">
<xsl:param name="name"
select="name(.)"/>
<xsl:variable name="indent-spaces">
<xsl:call-template name="trailing-space-chars">
<xsl:with-param name="string"
select="preceding-sibling::text()"/>
</xsl:call-template>
</xsl:variable>
<!-- +2 for the leading < and the space after the name -->
<xsl:variable name="indentlen"
select="string-length($name) + $indent-spaces + 2"/>
<xsl:call-template name="spaces">
<xsl:with-param name="count"
select="$indentlen"/>
</xsl:call-template>
</xsl:template>Spaces is a recursive template that outputs a specified
number of spaces.<xsl:template name="spaces">
<xsl:param name="count"
select="'0'"/>
<xsl:if test="$count > 0">
<xsl:text> </xsl:text>
<xsl:call-template name="spaces">
<xsl:with-param name="count"
select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>Given a string, this template walks it recursively counting
and returning the number of trailing spaces.<xsl:template name="trailing-space-chars">
<xsl:param name="string"
select="''"/>
<xsl:param name="count"
select="0"/>
<xsl:choose>
<xsl:when test="$string = '' or substring($string,string-length($string),1) != ' '">
<xsl:value-of select="$count"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="trailing-space-chars">
<xsl:with-param name="string"
select="substring($string,1,string-length($string)-1)"/>
<xsl:with-param name="count"
select="$count + 1"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>Output Applicable AttributesThis template walks recursively over the attributes associated
with a node and outputs each one of them in turn. (All attributes
are applicable.)<xsl:template name="output.applicable.attributes">
<xsl:param name="attributes"
select="attribute::*"/>
<xsl:param name="first"
select="'1'"/>
<xsl:choose>
<xsl:when test="count($attributes) = 0">
<!-- do nothing -->
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="name($attributes[1])"/>
<xsl:text>="</xsl:text>
<xsl:value-of select="$attributes[1]"/>
<xsl:text>"</xsl:text>
<xsl:call-template name="output.applicable.attributes">
<xsl:with-param name="attributes"
select="$attributes[position()>1]"/>
<xsl:with-param name="first"
select="'0'"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>Other ContentThe remaining elements, processing instructions, and comments are
part of the documentation and must simply be copied to the result:ElementsThe default template handles copying elements.
It is a five step process:
Save a copy of the context node in
$node so that we can refer to it later from
inside an xsl:for-each.Construct a new node in the result tree with
the same qualified name and namespace as the context node.Copy the namespace nodes on the context node to the
new node in the result tree. We must do this manually because the
XWEB file may have broken the content of this element into several
separate fragments. Breaking things into separate fragments makes it
impossible for the XSLT processor to always construct the right namespace
nodes automatically.Copy the attributes.
Copy the children.
<xsl:template match="*">
<xsl:variable name="node"
select="."/>
<xsl:element name="{name(.)}"
namespace="{namespace-uri(.)}">
<xsl:copy-of select="@*"/>
<xsl:apply-templates/>
</xsl:element>
</xsl:template>Copy NamespacesCopying the namespaces is a simple loop over the elements on
the namespace axis, with one wrinkle.It is an error to copy a namespace node onto an element if a
namespace node is already present for that namespace. The fact that
we're running this loop in a context where we've constructed the
result node explicitly in the correct namespace means that attempting
to copy that namespace node again will produce an error. We work
around this problem by explicitly testing for that namespace and not
copying it.
<xsl:for-each select="namespace::*">
<xsl:if test="string(.) != namespace-uri($node)">
<xsl:copy/>
</xsl:if>
</xsl:for-each>Processing InstructionsProcessing instructions are simply copied through.<xsl:template match="processing-instruction()">
<xsl:processing-instruction name="{name(.)}">
<xsl:value-of select="."/>
</xsl:processing-instruction>
</xsl:template>CommentsComments are simply copied through. Note, however, that many
processors do not preserve comments in the source document, so this
template may never be matched.<xsl:template match="comment()">
<xsl:comment>
<xsl:value-of select="."/>
</xsl:comment>
</xsl:template>Weaving DocBookIt's no secret (and probably no surprise) that I use DocBook for
most of my document authoring. Web files are no exception, and I have
DocBook customization layer that validates woven XWEB documentation
files.In order to validate my woven documentation, I need to make sure
that the appropriate document type declaration is associated with the
documents. This is a simple change to the xsl:output
instruction.This stylesheet imports weave.xsl for the
weaving functionality and simply sets the public and system identifiers.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="xsl src xml"
version="1.0">
<xsl:import xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
href="weave.xsl"/>
<xsl:output xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
method="xml"
doctype-public="-//DocBook Open Repository//DTD DocBook Literate Programming V0.0//EN"
doctype-system="http://docbook.sourceforge.net/release/litprog/current/dtd/ldocbook.dtd"/>
</xsl:stylesheet>