user3051624
user3051624

Reputation: 33

XSLT; Group 2 or more different elements BUT only when adjacent

Initially this seemed a trivial problem, but it seems to be harder than I thought.

In the following xml, I want to group adjacent 'note' and p elements only. A note should always start a notegroup and any following p should be included. No other elements are allowed in the group.

From this:

<doc>
 <note />
 <p/>
 <p/>
 <other/>
 <p/>
 <p/>
</doc>

To this:

<doc>
 <notegroup>
   <note />
   <p/>
   <p/>
 </notegroup>
 <other/>
 <p/>
 <p/>
</doc>

Seems ridiculously easy, but the rule is: 'note' and any following 'p'. Any p on their own are to be ignored (as in the last 2 p above)

With xslt 2.0, if I try something like:

<xsl:for-each-group select="*" group-adjacent="boolean(self::note) or boolean(self::p)"> 

fails because it also groups the two later p elements.

Or the 'starts-with' approach on the 'note' which seems to indiscriminately group any element after (instead of just the p elements).

The other approach I'm considering is to simply add an attribute to each note and the p that immediately follow the note, and using that to group later, but how can I do that?

Thanks for any answers

Upvotes: 1

Views: 317

Answers (2)

Ian Roberts
Ian Roberts

Reputation: 122414

I think you can do this by being a bit creative with group-starting-with, essentially you want to start a new group whenever you see an element that is not one that belongs in a notegroup. In your example this would generate two groups - note+p+p and other+p+p, the trick is to only wrap a notegroup around groups where the initial item is a note, and to simply output groups that are not headed by a note unchanged

<xsl:for-each-group select="*" group-starting-with="*[not(self::p)]">
  <xsl:choose>
    <xsl:when test="self::note">
      <notegroup>
        <xsl:copy-of select="current-group()"/>
      </notegroup>
    </xsl:when>
    <xsl:otherwise>
      <xsl:copy-of select="current-group()"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:for-each-group>

This will wrap all note elements in a notegroup even if they don't actually have any following p elements. If you don't want to wrap a "bare" note then make it <xsl:when test="self::note and current-group()[2]"> to trigger the wrapping only when the current group has more than one member.

If you have more than one element name that can be part of a notegroup then you could either list them all in the predicate

<xsl:for-each-group select="*" group-starting-with="*[not(self::p|self::ul)]">

or declare a variable holding the node names that can be part of a notegroup:

<xsl:variable name="notegroupMembers" select="(xs:QName('p'), xs:QName('ul'))" />

and then say

<xsl:for-each-group select="*"
    group-starting-with="*[not(node-name(.) = $notegroupMembers)]">

taking advantage of the fact that an = comparison where one side is a sequence succeeds if any of the items in the sequence match.

Upvotes: 1

&#201;douard Lopez
&#201;douard Lopez

Reputation: 43421

Issue

You're group on either <note> or <p>. Hence the failing.

Hint

You can try by using group-starting-with="note",as describe in Grouping With XSLT 2.0 @ XML.com:

<xsl:template match="doc">
  <doc>
    <xsl:for-each-group select="*" group-starting-with="note">
      <notegroup>
      <xsl:for-each select="current-group()">
        <xsl:copy>
          <xsl:apply-templates select="self::note or self::p" />
        </xsl:copy>
      </xsl:for-each>
      </notegroup>
    </xsl:for-each-group>
  </doc>
</xsl:template>

You may need another <xsl:apply-templates /> around the end.

Upvotes: 0

Related Questions