Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nicowilliams/4279509 to your computer and use it in GitHub Desktop.
Save nicowilliams/4279509 to your computer and use it in GitHub Desktop.
XSL idiom for converting from <hN>Section Title</hN> to nested <section> style
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ ]>
<xsl:stylesheet version="2.0"
xpath-default-namespace="http://www.w3.org/1999/xhtml"
xmlns="foobar"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:foo="foobar"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="foo">
<!-- The following is a snippet of XSLT for XML document structure deepening. -->
<xsl:template match="*[matches(name(), '^h[2-9]') and ends-with(@class, 'section')]">
<!-- N is the integer in hN -->
<xsl:variable name="N" select="substring-after(name(), 'h') cast as xs:integer"/>
<xsl:variable name="thisHN" select="name()"/>
<!-- We refer to this <hN> in various XPath contexts below where
current() will no longer be this <h2>, so we need to save it -->
<xsl:variable name="cur_sect" select="current()"/>
<xsl:element name="section">
<xsl:attribute name="title" select="text()"/>
<!-- Handle the contents of just this section. Ask for all
siblings of this <hN> where the nodes we're looking for are
NOT hN, and their preceding <hN> is this one. -->
<xsl:apply-templates
select="(following-sibling::*[not(matches(name(), '^h[2-9]')) and
(preceding-sibling::*[matches(name(), '^h[2-9]')])[last()] is $cur_sect])"/>
<!-- Handle sub-sections of this section. Ask for all sibling
hNs of this hN where their preceding parent hN is
this one. -->
<xsl:apply-templates
select="following-sibling::*[matches(name(), '^h[2-9]') and
(preceding-sibling::*[name() = $thisHN])[last()] is $cur_sect and
(substring-after(name(), 'h') cast as xs:integer = ($N + 1))
]"/>
</xsl:element>
</xsl:template>
It took me a while to figure out a generic solution to this problem of
deepening XML document structure using XSLT. I could not find general
solutions online (or in the two books on XSLT that I bought), and though
I never worked out an XSLT 1.0 solution that worked well enough, I did
find a solution that works perfectly with XSLT 2.0.
The idiom consists of a template matching <hN>s that applies templates
to all following-sibling nodes that are not <hN> nodes and whose
preceding <hN> sibling is the current <hN> (the content of this
section) and then applies templates to all following-sibling nodes that
are <hN> nodes with N+1 and whose preceding-sibling <hN> is this one.
These are somewhat convoluted XPath expressions, so let's lay them out
in the attached .xsl file.
The XPath expression for selecting "the contents of this <hN>" is
roughly this:
(following-sibling::*[not(matches(name(), '^h[2-9]')) and
(preceding-sibling::*[matches(name(), '^h[2-9]')])[last()] is $cur_sect])
where $cur_sect is a variable bound to "current()" prior to this
expression (i.e., the <hN> in question).
The XPath expression for selecting "sub-sections of this <hN>" is
roughly this:
following-sibling::*[matches(name(), '^h[2-9]') and
(preceding-sibling::*[name() = $thisHN])[last()] is $cur_sect and
(substring-after(name(), 'h') cast as xs:integer = ($N + 1))]
where $cur_sect is as described above, $thisHN is a variable bound to
$cur_sect's "name()", and $N is an xs:integer: the N in $thisHN
(<xsl:variable name="N" select="substring-after(name(), 'h') cast as xs:integer"/>).
That's it!
This generalizes really well, I think, at least as long as you can
figure out N or otherwise select following-sibling nodes that are
clearly sub-sections of the current one.
(I accidentally created this as an anonymous gist,
https://gist.github.com/4279476, even though I was signed in. This is
the same gist, re-created as me.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment