How can I make my XSLT template cope with missing parent elements?
I'd like to apologize for the poor title - I really didn't know how to phrase it any better. I'm currently working on an XSLT 1.0 script (using xsltproc) which transforms a simple XML format into a text representation suitable for consumption by an PDF generator.
In my XML format, there are just three elements: <book>
, <chapter>
and <section>
. However, due to some nasty feature of the DTD, I have a hard time writing a proper XSLT script to transform the document. Here's the DTD which describes their relation:
<!ELEMENT book ((chapter|section)*)>
<!ELEMENT chapter (section*)>
<!ELEMENT section (#PCDATA)>
Here's the my XSLT stylesheet which performs the translation:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>
<xsl:strip-space elements="*"/>
<xsl:template match="section">
<xsl:if test="not(preceding-sibling::section)">@BeginSections
</xsl:if>
<xsl:text>@Section @Begin
</xsl:text>
<xsl:apply-templates/>
<xsl:text>@End @Section
</xsl:text>
<xsl:if test="not(following-sibling::section)">@EndSections
</xsl:if>
</xsl:template>
<xsl:template match="chapter">
<xsl:if test="not(preceding-sibling::chapter)">@BeginChapters
</xsl:if>
<xsl:text>@Chapter @Begin
</xsl:text>
<xsl:apply-templates/>
<xsl:text>@End @Chapter
</xsl:text>
<xsl:if test="not(following-sibling::chapter)">@EndChapters
</xsl:if>
</xsl:template>
<xsl:template match="/book">
<xsl:text>@Book @Begin
</xsl:text>
<xsl:apply-templates/>
<xsl:text>@End @Book
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Now, here comes the tricky part and my question: the DTD makes it possible to have <section>
elements as the direct children of <book>
. However, I still have to yield the same output as if that /book/section
element was actually /book/chapter/section
.
So e.g.: <book><section/><chapter/></book>
becomes (I indented the output for better readability)
@Book @Begin
@BeginChapters
@Chapter @Begin
@BeginSections
@Section @Begin
@End @Section
@EndSections
@End @Chapter
@Chapter @Begin
@End @Chapter
@EndChapters
@End @Book
So what I'd like to do is to adjust my XSLT script so that the 'section' template somehow also calls the 'chapter' template in case the given <section>
element is not within a <chapter>
. How could I do this?
For what it's worth, here's another example. Multiple <section>
elements which are not already in a <chapter>
should get merged into one. Hence,<book><section/><section/><section/><chapter/><section/></book>
yields output for three chapters (the first of which has three sections, the second has none, the third has o开发者_Go百科ne section).
This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>
<xsl:strip-space elements="*"/>
<xsl:template match="chapter|section" mode="chapter" name="makeChapter">
<xsl:text> @Chapter @Begin
</xsl:text>
<xsl:apply-templates select="self::chapter/node()[1]|
self::section"
mode="section"/>
<xsl:text> @End @Chapter
</xsl:text>
<xsl:apply-templates
select="self::chapter/following-sibling::node()[1]|
self::section/following-sibling::chapter[1] "
mode="chapter"/>
</xsl:template>
<xsl:template match="section" mode="makeSection" name="makeSection">
<xsl:text> @Section @Begin
</xsl:text>
<xsl:apply-templates/>
<xsl:text> @End @Section
</xsl:text>
<xsl:apply-templates select="following-sibling::node()[1]/self::section"
mode="makeSection"/>
</xsl:template>
<xsl:template match="section" mode="section">
<xsl:text> @BeginSections
</xsl:text>
<xsl:call-template name="makeSection"/>
<xsl:text> @EndSections
</xsl:text>
</xsl:template>
<xsl:template match="book/chapter|book/section">
<xsl:text> @BeginChapters
</xsl:text>
<xsl:call-template name="makeChapter"/>
<xsl:text> @EndChapters
</xsl:text>
</xsl:template>
<xsl:template match="book">
<xsl:text>@Book @Begin
</xsl:text>
<xsl:apply-templates select="node()[1]"/>
<xsl:text>@End @Book
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Output:
@Book @Begin
@BeginChapters
@Chapter @Begin
@BeginSections
@Section @Begin
@End @Section
@EndSections
@End @Chapter
@Chapter @Begin
@End @Chapter
@EndChapters
@End @Book
Note: Fine grained traversal, grouping adjacents book/sections into one Chapter.
Edit: Corrected following sibling process for Chapters.
With this input:
<book>
<section/>
<section/>
<section/>
<chapter/>
<section/>
</book>
Output:
@Book @Begin
@BeginChapters
@Chapter @Begin
@BeginSections
@Section @Begin
@End @Section
@Section @Begin
@End @Section
@Section @Begin
@End @Section
@EndSections
@End @Chapter
@Chapter @Begin
@End @Chapter
@Chapter @Begin
@BeginSections
@Section @Begin
@End @Section
@EndSections
@End @Chapter
@EndChapters
@End @Book
Edit: Better named templates to understand.
Note: Five rules: book
rule "opens" a book and process first child; book/section|book/chapter
rule (always the first because the fine grained transversal) "opens" book chapters, calls makeChapter
; makeChapter
rule, "opens" a chapter, process first child if context is chapter
or self if context is section
both in section
mode, process next sibling if context is chapter
or following chapter
if context is section
in chapter
mode (meaning next chapter); section
rule in section
mode (because the node by node process, it will always match the first sections
for adjacents sections
) "opens" chapter sections an calls makeSection
rule; makeSection
rule "opens" a section an process childs, then process next sibling section
in makeSection
mode (this rule).
you need to first wrap the orphan sections into a single chapter..
we create a variable for this to hold the wrapped elements
<xsl:variable name="orphan">
<chapter>
<xsl:for-each select="/book/section">
<xsl:copy-of select="." />
</xsl:for-each>
</chapter>
</xsl:variable>
then when you apply the templates inside the /book
matching template you need to also use this newly created variable
<xsl:apply-templates select="chapter|exslt:node-set($orphan)"/>
and to use the exslt
you need to add the namespace
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common">
Final result is
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common">
<xsl:output method="html" encoding="iso-8859-1"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="orphan">
<chapter>
<xsl:for-each select="/book/section">
<xsl:copy-of select="." />
</xsl:for-each>
</chapter>
</xsl:variable>
<xsl:template match="section">
<xsl:if test="not(preceding-sibling::section)">@BeginSections
</xsl:if>
<xsl:text>@Section @Begin
</xsl:text>
<xsl:apply-templates/>
<xsl:text>@End @Section
</xsl:text>
<xsl:if test="not(following-sibling::section)">@EndSections
</xsl:if>
</xsl:template>
<xsl:template match="chapter">
<xsl:if test="not(preceding-sibling::chapter)">@BeginChapters
</xsl:if>
<xsl:text>@Chapter @Begin
</xsl:text>
<xsl:apply-templates/>
<xsl:text>@End @Chapter
</xsl:text>
<xsl:if test="not(following-sibling::chapter)">@EndChapters
</xsl:if>
</xsl:template>
<xsl:template match="/book">
<xsl:text>@Book @Begin
</xsl:text>
<xsl:apply-templates select="chapter|exslt:node-set($orphan)"/>
<xsl:text>@End @Book
</xsl:text>
</xsl:template>
</xsl:stylesheet>
精彩评论