For now, I am using an ugly pre-processing step to handle the situation. After I process my documents through an identity transformation to resolve xi:includes, I process them again to resolve the following custom processing instructions. Then process the resulting files with the DocBook XSLT stylesheets to get my final output.
<?transclude-element my-element-id target-file=my-file.xml ?>
<?transclude-element my-other-element-id make-ids-unique ?>
These processing instructions will be replaced by the element identified by the xml:id that is named by the first space-separated string in the processing instruction. If the element is not in the current document, it will be found in a file named by the string marked by "target-file=." If the element is in the current document, including the string "make-ids-unique" will append a unique string to each xml:id in the target element to avoid duplication.
The custom XSLT 2.0 stylesheet that handles the processing instruction (below) figures out whether the transcluded element will be the child of a book element and transforms sections into chapters. If the transcluded element will be the child of a chapter or section, it transforms chapters into sections. Otherwise it just writes the target element in place of the processing instruction.
One reason I prefer this to xi:includes in some situations is that my XSLT processing chain only handles the simplest xpointers. Using this processing instruction allows me to reuse small elements like a table or paragraph that are deeply nested in other content.
Hopefully I will throw this away soon and start using topic and assembly to create modular documents in a more robust way. The custom XSLT that handles my processing instructions is pasted below. It's very specific to my element use (chapters and sections) so it will need to be altered to suit other environments. I use Saxon 9 for my pre-processing steps so that I can use XSLT 2.0.
Warning: I had to add steps to preserve the links between callouts and their callout bugs in programlistings when the stylesheet makes xml:ids unique. There are surely other situations in which this brutal technique will cause problems!
<?xml version="1.0" encoding="UTF-8" ?> <xsl:stylesheet version="2.0"
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" xmlns="http://docbook.org/ns/docbook"> <xsl:output method="xml" indent="no" /> <xsl:strip-space elements="d:title"/> <xsl:param name="current.docid" /> <xsl:template match="@*|node()"> <xsl:copy> <!-- If a chapter or section does not have an
xml:id attribute, generate one. -->
<xsl:if test="not(@xml:id) and name() = 'chapter' or name() = 'section' or name() = 'part'">
<xsl:attribute name="xml:id"> <xsl:value-of select="concat('storiant', generate-id(.))" /> </xsl:attribute> </xsl:if> <xsl:apply-templates select="@*|node()" /> </xsl:copy> </xsl:template> <!-- ************************************ --> <!-- *** Transclude elements. --> <!-- ************************************ --> <xsl:template match="processing-instruction('transclude-element')"> <xsl:variable name="pi.text">
<xsl:value-of select="normalize-space(.)" />
</xsl:variable> <xsl:variable name="transclusion.target.id">
<xsl:value-of select="tokenize($pi.text, '\s')[position() = 1]" />
</xsl:variable> <xsl:variable name="transclusion.target.file.path"> <xsl:choose> <xsl:when test="tokenize($pi.text, '\s')[contains(., 'target-file=')]"> <!-- Write the relative path from the stylesheet directory to the source directory. Brittle! --> <xsl:variable name="current.instruction.token"
select="tokenize($pi.text, '\s')[position() = 2]" />
<!-- The next line will surely not work anywhere
but my file system! --> <xsl:text>../../source/</xsl:text> <xsl:value-of
select="substring-after($current.instruction.token, 'target-file=')" /> </xsl:when> <xsl:otherwise> <xsl:value-of select="document-uri(/)" /> </xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:variable name="generated.transclusion.id"> <xsl:if test="contains($pi.text, 'make-ids-unique')"> <xsl:value-of select="generate-id()" /> </xsl:if> </xsl:variable> <xsl:variable name="transclusion.context.parent.name">
<xsl:value-of select="name(parent::*)" />
</xsl:variable> <xsl:choose>
<!-- Determine the parent element of the processing
instruction and handle the target elements differently
according to the context. I convert chapters to
sections when they are going into a section and
convert sections to chapters when they are going
into a book. -->
<xsl:when test="parent::d:book"> <xsl:apply-templates
select="document($transclusion.target.file.path)//*[@xml:id=$transclusion.target.id]" mode="transclude.element.book.parent"> <xsl:with-param name="transclusion.id"
select="$generated.transclusion.id"
tunnel="yes" /> </xsl:apply-templates> </xsl:when>
<xsl:when test="parent::d:chapter"> <xsl:apply-templates
select="document($transclusion.target.file.path)//*[@xml:id=$transclusion.target.id]" mode="transclude.element.chapter.parent"> <xsl:with-param name="transclusion.id"
select="$generated.transclusion.id" tunnel="yes" /> </xsl:apply-templates> </xsl:when> <xsl:otherwise> <xsl:apply-templates
select="document($transclusion.target.file.path)//*[@xml:id=$transclusion.target.id]" mode="transclude.element"> <xsl:with-param name="transclusion.id"
select="$generated.transclusion.id" tunnel="yes" /> </xsl:apply-templates> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="d:section" mode="transclude.element.book.parent"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:element name="chapter"> <xsl:if test="@xml:id"> <xsl:attribute name="xml:id"> <xsl:value-of select="@xml:id" /> <xsl:value-of select="$transclusion.id" /> </xsl:attribute> </xsl:if> <xsl:apply-templates mode="transclude.element"> <xsl:with-param name="transclusion.id"
select="$transclusion.id" tunnel="yes" /> </xsl:apply-templates> </xsl:element> </xsl:template> <xsl:template match="d:chapter" mode="transclude.element.chapter.parent"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:element name="section"> <xsl:if test="@xml:id"> <xsl:attribute name="xml:id"> <xsl:value-of select="@xml:id" /> <xsl:value-of select="$transclusion.id" /> </xsl:attribute> </xsl:if> <xsl:apply-templates mode="transclude.element"> <xsl:with-param name="transclusion.id"
select="$transclusion.id" tunnel="yes" /> </xsl:apply-templates> </xsl:element> </xsl:template> <xsl:template match="@xml:id" mode="transclude.element"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:attribute name="xml:id"> <xsl:value-of select="." /> <xsl:value-of select="$transclusion.id" /> </xsl:attribute> </xsl:template>
<!-- The next two templates preserve the relationship
between callouts and callout bugs. -->
<xsl:template match="d:co/@linkends" mode="transclude.element"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:attribute name="linkends"> <xsl:value-of select="." /> <xsl:value-of select="$transclusion.id" /> </xsl:attribute> </xsl:template> <xsl:template match="d:callout/@arearefs" mode="transclude.element"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:attribute name="arearefs"> <xsl:value-of select="." /> <xsl:value-of select="$transclusion.id" /> </xsl:attribute> </xsl:template>
<!-- I use olinks for cross-references. The next template
assumes that if it finds an xref, it must be pointing to
something very local and preserves the relationship. If
you use xref for cross-references, this will likely
cause problems. -->
<xsl:template match="d:xref/@linkend" mode="transclude.element"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:attribute name="linkend"> <xsl:value-of select="." /> <xsl:value-of select="$transclusion.id" /> </xsl:attribute> </xsl:template> <xsl:template match="@*|node()" mode="transclude.element"> <xsl:param name="transclusion.id" required="yes" tunnel="yes" /> <xsl:copy> <xsl:apply-templates select="@*|node()" mode="transclude.element"> <xsl:with-param name="transclusion.id"
select="$transclusion.id"
tunnel="yes" /> </xsl:apply-templates> </xsl:copy> </xsl:template> </xsl:stylesheet>