DocBook Transclusion
08 Jan 2015
1. Introduction
This document describes the syntax, semantics, and processing model
for the DocBook transclusion mechanism. Please be aware that this is a
working draft – everything described below might change or disappear
completely. This proposal addresses the issues described in Requirements
for transclusion in DocBook. The DocBook TC welcomes
feedback on this draft, especially from users and developers of
DocBook authoring and processing tools. Please direct your comments to
the DocBook mailing list by sending email to
<docbook@lists.oasis-open.org>
.
Note
Previous version of this draft proposed the new elements
ref
and def
for implementing
transclusions. Meanwhile DocBook TC decided that transclusions should
rely as much as possible on standard technologies. XInclude 1.1 added
new features that allow to implement transclusion features on
top of the XInclude. As a result transclusions were completely
redesigned to be layered on top of XInclude 1.1.
This resulted in a loss of some features. For example, it's no longer possible to locally redefine text replacement (see this example from the April 20, 2014 draft of this document).
The following namespace bindings are used as prefixes in this document:
trans
DocBook transclusion namespace (
http://docbook.org/ns/transclusion
)db
DocBook namespace (
http://docbook.org/ns/docbook
)xi
XInclude namespace (
http://www.w3.org/2001/XInclude
)local
XInclude namespace for copying attributes without a namespace (
http://www.w3.org/2001/XInclude/local-attributes
)
2. Transclusion processing
The processing model for transclusion is simple and cosists of the following steps:
Normal XInclude 1.1 processing on the input document.
DocBook transclusion processing on the result to fix problems such as duplicate IDs and broken cross-references.
Normal processing on the resulting document using the DocBook stylesheets or equivalent tools.
Transclusion processing is controlled by
attributes from the http://docbook.org/ns/transclusion
namespace. These attributes are typically inserted into a
document by using the attribute copying feature of XInclude 1.1 [], which
is described in section 4.3
Attribute Copying when processing XML of the XInclude 1.1 specification.
The transclusion processor copies documents node by node. For most nodes this is an identity transformation. The few exceptions are controlled by transclusion properties. Transclusion properties suffix and linkscope are defined for each node in the document as follows:
- suffix
- Defines a value to be appended to all ID values on the element (elements and attributes whose values should be treated as IDs are listed in the ID-list property)
- Default is an empty string
- Inherited
- Can be changed by the
trans:idfixup
andtrans:suffix
attributes
- linkscope
- Defines how to correct ID references (elements and attributes whose values should be treated as ID references are listed in the IDREF-list property)
- Allowed values are
user
,local
,near
, andglobal
- Default value is
near
- Inherited
- Can be changed by the
trans:linkscope
attribute
For each document type there are two properties, ID-list and IDREF-list, which control the transclusion process. The ID-list property defines the attributes and elements whose values should be treated as IDs. Similarly, the IDREF-list property defines the attributes and elements whose values should be treated as ID references. These properties are defined for DocBook 5.0 in Definition of ID-list and IDREF-list properties for DocBook 5.0. Transclusion processors are free to support the ID-list and IDREF-list properties with other document types, for example DocBook 4.x or TEI.
ID references identified in the IDREF-list are corrected as follows:
- When
linkscope=user
No adjustment is made.
- When
linkscope=local
The value of the suffix property is added to every ID reference as a suffix.
- When
linkscope=near
Each ID reference is adjusted to point to the closest element that has a matching ID.
To find the closest element, the parent element of the element that contains the ID reference is searched for a matching ID, followed by that element's descendants. If no matching ID is found, that element's parent is inspected in the same way. This continues until a match is found or the root element is reached.
- When
linkscope=global
Each ID reference is adjusted to point to the first element in document order that has a matching ID.
A matching ID does not mean exact string equality between ID and IDREF values. A matching ID means that the values of an ID and IDREF match after removing any added suffixes.
Transclusion properties can be set on any element using the following attributes:
trans:idfixup
attribute- value
none
The suffix property is set to an empty string.
- value
suffix
The suffix property is set to the concatenation of the inherited suffix value and the value specified in the
trans:suffix
attribute.- value
auto
The suffix property is set to a unique value for each element.[1]
- value
trans:suffix
attributeThis attribute defines the value of the suffix property used when
trans:idfixup="suffix"
.It's an error to use this attribute when the attribute/value pair
trans:idfixup="suffix"
is not also on the element.trans:linkscope
attributeSets the value of the linkscope property. Permitted values are:
user
,local
,near
, andglobal
.
During the transclusion process all attributes from the transclusion namespace are removed from the resulting document.
In DocBook 5.0, the ID-list and IDREF-list properties contain the attributes shown here:
- ID-list
xml:id
- IDREF-list
linkend
,linkends
,otherterm
,zone
,startref
,arearefs
,targetptr
, andendterm
.The
href
attribute in the XLink namespace is considered a member of IDREF-list when its value begins with#
.
A. Using XInclude 1.1 features for your content
The most common transclusion scenario is reuse of shared strings (see https://docbook.org/docs/transclusion-requirements/transclusion-requirements.html#uc-1). With XInclude 1.0 this could only be done using XPointer schemes that were not very interoperable. With XInclude 1.1, doing this is much easier.
Let's assume we have defined a set of shared strings in separate document (see Example A.1, “Definitions stored in a separate document (definitions.001.xml
)”).
If you transclude parts of this document as shown in Example A.2, “Transclusion with duplicate IDs”, you will end up with duplicate IDs. In this example, the same definition is included twice. Because the ID attributes are passed through unchanged, the ID value “product-name” occurs twice in the resulting document.
XInclude 1.1 has an attribute, set-xml-id
,
which can be used to change or remove the xml:id
attribute on included content. Example A.3, “Using set-xml-id
to remove the top-level ID during transclusion” uses this attribute to avoid duplicate IDs.
Another new XInclude 1.1 feature lets you override
attributes on included content. Example A.4, “Overriding DocBook attributes on inclusion” shows how to use this feature to
override effectivity attributes. This feature works by placing attributes from a specially
defined namespace (http://www.w3.org/2001/XInclude/local-attributes
) on
the xi:include
element. These attributes define the values that should be
used to replace attributes of the same name, but no namespace, in the transcluded content.
B. Special ID/IDREF processing
Transcluded content can contain xml:id
attributes. If one fragment is
transcluded more than once, then the resulting document after transclusion will
contain duplicate IDs. The same problem can occur if the same ID is used in two different
transcluded modules. To overcome this problem, IDs and ID references
can be adjusted during the transclusion process.
The trans:idfixup
attribute on
the xi:include
element determines how IDs are adjusted during transclusion.
Of course, if IDs are adjusted then all corresponding ID references also have to be adjusted.
These adjustments are controlled by the trans:idfixup
and trans:linkscope
attributes.
The following examples show the effect of using those two
attributes. Each example transcludes the procedure shown in Example B.1, “Module with sample procedure”, which
contains one internal link and one external link.
Example B.2, “Automatic ID/IDREF adjustment” transcludes this module twice to show how we can deal with the duplicate ID problem.
Specifying db:idfixup
triggers
the automatic ID/IDREF fixup. All IDs in the transcluded
modules are automatically suffixed to prevent ID collisions. Then,
IDREFs are fixed so that links point to the nearest possible
target. In Example B.2, “Automatic ID/IDREF adjustment”, the link from step 2 to step 1 in the procedure always
points to the same instance of the procedure. However, the “buy”
link correctly points to the target in the main document.
Example B.3, “Global linkscope” uses trans:linkscope="global"
for the second transclusion. The result is that the link from step 2 in the second
procedure links to step 1 in the first procedure.
Example B.4, “Local linkscope” uses db:linkscope="local"
on
the first transclusion. This means that every link from this transclusion must
point to an ID inside the transcluded content; no external links are allowed.
Because the transcluded content does contain a link to an external ID (“buy”),
this example is broken, since the transclusion process rewrites “buy”
to be “buy---d1e23”, for which there is no corresponding target.
This method of transclusion can be useful if you are transcluding foreign content and want to isolate its links from the rest of your document.
You can manually assign the value of the suffix used in transcluded content. Example B.5, “Manually assigned suffix” shows how to do this.
By default, XInclude does not do any postprocessing. Thus, the resulting document in Example B.6, “Disabling ID fixup” contains duplicate IDs and is not valid.
C. DocBook schema with support for transclusions
TBD
D. Sample transclusion processor written in XSLT 2.0
Please note that this sample transclusion processor is not yet feature complete. It supports only a subset of the proposal.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:f="http://docbook.org/xslt/ns/extension"
xmlns:mp="http://docbook.org/xslt/ns/mode/private"
xmlns:db="http://docbook.org/ns/docbook"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:ta="http://docbook.org/ns/transclude"
xmlns:xia="http://www.w3.org/2001/XInclude/local-attributes"
exclude-result-prefixes="f mp xs ta xia">
<!-- Remove for production files, pretty print can harm mixed content -->
<xsl:output indent="yes"/>
<xsl:template match="/">
<xsl:variable name="adjusted" select="f:adjust-ids(/)"/>
<xsl:variable name="result" select="f:adjust-idrefs($adjusted)"/>
<xsl:sequence select="f:transclude-cleanup($result)"/>
</xsl:template>
<!-- Separator for auto generated suffixes -->
<xsl:param name="psep" select="'---'"/>
<!-- Function and mode for changing IDs based on provided suffix -->
<xsl:function name="f:adjust-ids" as="node()+">
<xsl:param name="doc" as="node()+"/>
<xsl:apply-templates select="$doc" mode="mp:transclude"/>
</xsl:function>
<xsl:template match="node()" mode="mp:transclude">
<xsl:param name="idfixup" select="'none'" tunnel="yes"/>
<xsl:param name="suffix" tunnel="yes"/>
<xsl:copy>
<xsl:copy-of select="@* except @xml:id"/>
<xsl:if test="@xml:id">
<xsl:choose>
<xsl:when test="($idfixup = 'none')">
<xsl:copy-of select="@xml:id"/>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="xml:id" select="concat(@xml:id, $suffix)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:apply-templates mode="mp:transclude"/>
</xsl:copy>
</xsl:template>
<xsl:template match="node()[@ta:*]" mode="mp:transclude">
<xsl:param name="idfixup" select="'none'" tunnel="yes"/>
<xsl:param name="suffix" tunnel="yes"/>
<xsl:variable name="new-idfixup" select="if (@ta:idfixup) then @ta:idfixup else $idfixup"/>
<xsl:variable name="linkscope" select="if (@ta:linkscope) then @ta:linkscope else 'near'"/>
<xsl:variable name="new-suffix">
<xsl:choose>
<xsl:when test="$new-idfixup = 'auto'">
<xsl:sequence select="concat($psep, generate-id(.))"/>
</xsl:when>
<xsl:when test="$new-idfixup = 'suffix'">
<xsl:sequence select="concat($suffix, @ta:suffix)"/>
</xsl:when>
<xsl:otherwise></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:copy>
<xsl:copy-of select="@* except @xml:id"/>
<xsl:if test="@xml:id">
<xsl:choose>
<xsl:when test="($new-idfixup = 'none')">
<xsl:copy-of select="@xml:id"/>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="xml:id" select="concat(@xml:id, $new-suffix)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:attribute name="ta:suffix" select="$new-suffix"/>
<xsl:apply-templates mode="mp:transclude">
<xsl:with-param name="idfixup" select="$new-idfixup" tunnel="yes"/>
<xsl:with-param name="suffix" select="$new-suffix" tunnel="yes"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<!-- Function and mode for adjusting references to IDs -->
<xsl:function name="f:adjust-idrefs" as="node()+">
<xsl:param name="doc" as="node()+"/>
<xsl:apply-templates select="$doc" mode="mp:adjust-idrefs"/>
</xsl:function>
<xsl:template match="node()|@*" mode="mp:adjust-idrefs">
<xsl:copy>
<xsl:apply-templates select="@* | node()" mode="mp:adjust-idrefs"/>
</xsl:copy>
</xsl:template>
<!-- FIXME: add support for @linkends, @zone, @arearefs -->
<!-- FIEMX: add support for xlink:href starting with # -->
<xsl:template match="@linkend | @endterm | @otherterm | @startref" mode="mp:adjust-idrefs">
<xsl:variable name="idref" select="."/>
<xsl:variable name="annotation" select="ancestor-or-self::*[@ta:linkscope][1]"/>
<xsl:variable name="linkscope" select="($annotation/@ta:linkscope, 'near')[1]"/>
<xsl:variable name="suffix" select="$annotation/@ta:suffix"/>
<xsl:attribute name="{local-name(.)}">
<xsl:choose>
<xsl:when test="$linkscope = 'user'">
<xsl:value-of select="$idref"/>
</xsl:when>
<xsl:when test="$linkscope = 'local'">
<xsl:value-of select="concat($idref, $suffix)"/>
</xsl:when>
<xsl:when test="$linkscope = 'near'">
<xsl:value-of select="f:nearest-matching-id($idref, ..)"/>
</xsl:when>
<xsl:when test="$linkscope = 'global'">
<xsl:value-of select="f:nearest-matching-id($idref, root(.))"/>
</xsl:when>
</xsl:choose>
</xsl:attribute>
</xsl:template>
<!-- Function searches nearest matching ID in a given context -->
<xsl:function name="f:nearest-matching-id" as="xs:string?">
<xsl:param name="idref" as="xs:string"/>
<xsl:param name="context" as="node()"/>
<!-- FIXME: key() requires document-node() rooted subtree -->
<!-- <xsl:variable name="targets" select="key('unprefixed-id', f:unprefixed-id($idref, $context), $context)"/> -->
<xsl:variable name="targets" select="$context//*[@xml:id][f:unprefixed-id(@xml:id, .) eq f:unprefixed-id($idref, $context)]"/>
<xsl:choose>
<xsl:when test="not($targets) and $context/..">
<xsl:sequence select="f:nearest-matching-id($idref, $context/..)"/>
</xsl:when>
<xsl:when test="$targets">
<xsl:sequence select="$targets[1]/string(@xml:id)"/>
</xsl:when>
<xsl:otherwise>
<xsl:message>Error: no matching ID for reference "<xsl:value-of select="$idref"/>" was found.</xsl:message>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
<!-- FIXME: type annotation should be without ?, find why it is called with empty sequence -->
<xsl:function name="f:unprefixed-id" as="xs:string?">
<xsl:param name="id" as="xs:string?"/>
<xsl:param name="context" as="node()"/>
<xsl:variable name="suffix" select="$context/ancestor-or-self::*[@ta:suffix][1]/@ta:suffix"/>
<xsl:sequence select="if ($suffix) then substring-before($id, $suffix) else $id"/>
</xsl:function>
<!--
<xsl:key name="unprefixed-id" match="*[@xml:id]" use="f:unprefixed-id(@xml:id, .)"/>
-->
<!-- Function and mode for removing transclusion attributes from the final output -->
<xsl:function name="f:transclude-cleanup" as="node()+">
<xsl:param name="doc" as="node()+"/>
<xsl:apply-templates select="$doc" mode="mp:transclude-cleanup"/>
</xsl:function>
<xsl:template match="node()" mode="mp:transclude-cleanup">
<xsl:copy>
<xsl:apply-templates mode="mp:transclude-cleanup"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*" mode="mp:transclude-cleanup" priority="10">
<xsl:element name="{name()}" namespace="{namespace-uri()}">
<xsl:copy-of select="@* except @ta:*"/>
<xsl:apply-templates mode="mp:transclude-cleanup"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Bibliography
[XI11] XML Inclusions (XInclude) Version 1.1. W3C Last Call Working Draft. 16 December 2014. Available at https://www.w3.org/TR/2014/WD-xinclude-11-20141216/.
[1]
For example, XSLT-based
implementations can use the generate-id()
function to
generate a unique suffix.