John Wickerson
John Wickerson

Reputation: 1232

Grouping nodes in XSLT

I have an XML file structured like so:

<root>
 <a>...</a>
 <b>...</b>
 <a>...</a>
 <a>...</a>
 <comment>...</comment>
 <a>...</a>
 <b>...</b>
 <comment>...</comment>
 <a>...</a>
 <b>...</b>
</root>

I would like to use XSLT to transform it so that each sequence of as and bs is gathered into a single <div>, like so:

<root>
 <div>
  <a>...</a>
  <b>...</b>
  <a>...</a>
  <a>...</a>
 </div>
 <comment>...</comment>
 <div>
  <a>...</a>
  <b>...</b>
 </div>
 <comment>...</comment>
 <div>
  <a>...</a>
  <b>...</b>
 </div>
</root>

My first attempt involved putting a <div>...</div> inside the root, and then wrapping each comment in </div>...<div> (note the reversal of the tags), but that's not allowed in XSLT. How can I do this?

The following question is related to mine, but it involves counting a fixed number of as and bs, whereas I want to count as and bs until I hit a comment:

Upvotes: 0

Views: 113

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 116959

Here's how this can be accomplished in XSLT 1.0:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:key name="group-by-comment" match="root/*" use="count(preceding-sibling::comment)" />

<xsl:template match="/root">
    <xsl:copy>
        <!-- process children of root using group mode -->
        <xsl:apply-templates select="*" mode="group"/>
    </xsl:copy>
</xsl:template>

<!-- [group mode] for each child of root that is first in its group: -->
<xsl:template match="*[count(. | key('group-by-comment', count(preceding-sibling::comment))[1]) = 1]" mode="group">
    <div>
        <!-- process the entire group -->
        <xsl:apply-templates select="key('group-by-comment', count(preceding-sibling::comment))"/>
    </div>
</xsl:template>

<!-- [group mode] special rule for the dividing comment element -->
<xsl:template match="comment" mode="group">
    <xsl:copy-of select="."/>
</xsl:template>

<!-- process group members -->
<xsl:template match="*">
    <xsl:copy-of select="."/>
</xsl:template>

<!-- exclude the dividing comment element from the processed group -->
<xsl:template match="comment"/>

</xsl:stylesheet>

When applied to the following test input:

<root>
    <alpha id="1"/>
    <bravo id="2"/>
    <charlie id="3"/>
    <alpha id="4"/>
    <bravo id="5"/>
    <comment id="6"/>
    <charlie id="7"/>
    <alpha id="8"/>
    <bravo id="9"/>
    <comment id="10"/>
    <alpha id="11"/>
    <charlie id="12"/>
</root>

the result is:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <div>
      <alpha id="1"/>
      <bravo id="2"/>
      <charlie id="3"/>
      <alpha id="4"/>
      <bravo id="5"/>
   </div>
   <comment id="6"/>
   <div>
      <charlie id="7"/>
      <alpha id="8"/>
      <bravo id="9"/>
   </div>
   <comment id="10"/>
   <div>
      <alpha id="11"/>
      <charlie id="12"/>
   </div>
</root>

Upvotes: 3

Martin Honnen
Martin Honnen

Reputation: 167401

John, consider to use XSLT 2.0 (exists since 2007) with an XSLT 2.0 processor like Saxon 9 and you can easily use

<xsl:template match="root">
  <xsl:copy>
    <xsl:for-each-group select="*" group-adjacent="boolean(self::a | self::b)">
      <xsl:choose>
        <xsl:when test="current-grouping-key()">
          <div>
            <xsl:copy-of select="current-group()"/>
          </div>
        </xsl:when>
        <xsl:otherwise>
          <xsl:copy-of select="current-group()"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each-group>
  </xsl:copy>
</xsl:template>

Upvotes: 2

Related Questions