Reputation: 33
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
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
Reputation: 43421
You're group on either <note>
or <p>
. Hence the failing.
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