2
votes

I'm working with XSLT 1.0 from PHP and want to wrap all the sibling elements after a heading (h2) into a div so I can toggle them.

The input would look like

...
<h2>Nth title</h2>
<first child>...</first child>
...
<last child>...</last child>
<h2>N+1st title</h2>
...

and the output should be

...
<h2>Nth title</h2>
<div>
  <first child>...</first child>
  ...
  <last child>...</last child>
</div>
<h2>N+1st title</h2>
...

Is there a way to do this in XSLT 1.0?

2
Good question, +1. See my answer for a complete, short and easy solution that is based on the most fundamental and powerful XSLT design pattern and that is efficiently using keys.Dimitre Novatchev
possible duplicate of How can I wrap a group of adjacent elements using XSLT?user357812

2 Answers

2
votes

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:key name="kFollowing" match="node()[not(self::h2)]"
  use="generate-id(preceding-sibling::h2[1])"/>

 <xsl:template match="node()|@*" name="identity">
     <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
 </xsl:template>

 <xsl:template match="h2">
  <xsl:call-template name="identity"/>
  <div>
   <xsl:apply-templates mode="copy"
       select="key('kFollowing', generate-id())"/>
  </div>
 </xsl:template>

 <xsl:template match="node()[not(self::h2)][preceding-sibling::h2]"/>

 <xsl:template match="node()" mode="copy">
  <xsl:call-template name="identity"/>
 </xsl:template>
</xsl:stylesheet>

when applied on this XML document:

<html>
    <h2>Nth title</h2>
    <first-child>...</first-child> ... 
    <last-child>...</last-child>
    <h2>N+1st title</h2> ...
    <x/>
     <y/>
     <z/>
</html>

produces the wanted, correct result:

<html>
   <h2>Nth title</h2>
   <div>
      <first-child>...</first-child> ... 

      <last-child>...</last-child>
   </div>
   <h2>N+1st title</h2>
   <div> ...

      <x></x>
      <y></y>
      <z></z>
   </div>
</html>

Explanation:

  1. The identity rule/template copies every node "as-is".

  2. The identity rule is overriden for h2 elements. Here the action is to copy the h2 element and then to output a div and inside it to apply templates (in a special mode) to all nodes (that are not h2 themselves) for which this h2 element is the first preceding-sibling h2 element.

  3. The nodes to include in the previous step are conveniently defined as an <xsl:key> instruction.

  4. In order to stop the nodes that are wrapped in div to be output again by the identity rule, we provide a template matching such nodes, that simply ignores them.

0
votes

Yes. Make a template that matches h2 elements; within that template, you can select all following siblings before the next h2 using this xpath expression: following-sibling::*[count(preceding-sibling::h2[1] | current()) = 1].