XSLT - Sub-grouping in mixed nodes

M

MRe

Hi,

Is it possible using XSLT to transform this..

<test>
<b>0</b>
<a>1</a>
<a>2</a>
<b>3</b>
<b>4</b>
5
<b>6</b>
<b>7</b>
<b>8</b>
</test>

..into this (it's basically just wrapping groups of <b>s in a <c>)..

<test>
<c>
<b>0</b>
</c>
<a>1</a>
<a>2</a>
<c>
<b>3</b>
<b>4</b>
</c>
5
<c>
<b>6</b>
<b>7</b>
<b>8</b>
</c>
</test>

..that is, treat, in a mix of nodes, a specified group of nodes
(nodes that appear one-after-the-other, having no node outside that
group (except ignored empty text() nodes) between them) as a block?
(in the example above, <b> is the only chosen node in this group)

The closest I've come to getting this is to call a recursive
template, passing child::*[1] initially, and following-sibling::*[1]
for each recursive step, and also passing a 'block' parameter that, if
false and <b> is encountered, puts a <c> in and sets block to true,
and if true and <a> is encountered, puts a </c> in and sets to false.
However, this won't work as it won't put a </c> in if the last element
is a <b>.

All other attempts I've made at a solution I'd prefer to keep to
myself [embarrassed]

Thank you,
Kind regards,
Eliott
 
M

Martin Honnen

MRe said:
Is it possible using XSLT to transform this..

<test>
<b>0</b>
<a>1</a>
<a>2</a>
<b>3</b>
<b>4</b>
5
<b>6</b>
<b>7</b>
<b>8</b>
</test>

..into this (it's basically just wrapping groups of <b>s in a <c>)..

<test>
<c>
<b>0</b>
</c>
<a>1</a>
<a>2</a>
<c>
<b>3</b>
<b>4</b>
</c>
5
<c>
<b>6</b>
<b>7</b>
<b>8</b>
</c>
</test>

With XSLT 2.0 (as supported by Saxon http://saxon.sourceforge.net/,
Gestalt http://gestalt.sourceforge.net/, and Altova
http://www.altova.com/altovaxml.html) you can solve that as follows:

<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">

<xsl:eek:utput method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/test">
<xsl:copy>
<xsl:for-each-group select="node()"
group-adjacent="boolean(self::b)">
<xsl:choose>
<xsl:when test="current-grouping-key()">
<c>
<xsl:copy-of select="current-group()"/>
</c>
</xsl:when>
<xsl:eek:therwise>
<xsl:copy-of select="current-group()"/>
</xsl:eek:therwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>


Or do you need an XSLT 1.0 solution?
 
M

Martin Honnen

MRe said:
The closest I've come to getting this is to call a recursive
template, passing child::*[1] initially, and following-sibling::*[1]
for each recursive step, and also passing a 'block' parameter that, if
false and <b> is encountered, puts a <c> in and sets block to true,
and if true and <a> is encountered, puts a </c> in and sets to false.
However, this won't work as it won't put a </c> in if the last element
is a <b>.

With XSLT 1.0 you can solve such problems by processing sibling by
sibling, here is a sample stylesheet:

<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">

<xsl:eek:utput method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/test">
<xsl:copy>
<xsl:apply-templates select="node()[1]" mode="group"/>
</xsl:copy>
</xsl:template>

<xsl:template match="test/b" mode="group">
<c>
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1][self::b]"/>
</c>
<xsl:apply-templates
select="following-sibling::node()[not(self::b)][1]" mode="group"/>
</xsl:template>

<xsl:template match="test/b">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1][self::b]"/>
</xsl:template>

<xsl:template match="test/node()[not(self::b)]" mode="group">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="group"/>
</xsl:template>

</xsl:stylesheet>
 
M

MRe

With XSLT 1.0 you can solve such problems by processing sibling by
sibling, here is a sample stylesheet:

Ha, wow, very cool - this works prefect.

Thank you so much,
Plus extra thank you for the working sample,
And sorry for not stating it, XSLT 1.0 was what I needed

Thanks again,
Kind regards,
Eliott
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">

<xsl:eek:utput method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/test">
<xsl:copy>
<xsl:apply-templates select="node()[1]" mode="group"/>
</xsl:copy>
</xsl:template>

<xsl:template match="test/b" mode="group">
<c>
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1][self::b]"/>
</c>
<xsl:apply-templates
select="following-sibling::node()[not(self::b)][1]" mode="group"/>
</xsl:template>

<xsl:template match="test/b">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1][self::b]"/>
</xsl:template>

<xsl:template match="test/node()[not(self::b)]" mode="group">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="group"/>
</xsl:template>

</xsl:stylesheet>
 
D

Dimitre Novatchev

I will offer this solution, which uses just a key and the identity rule:

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

<xsl:key name="kAllChildren" match="b" use=
"generate-id(
(preceding-sibling::node()
[not(self::b)][1]
|
parent::*[
*[1][self::b]
]
)
[last()]
)"/>

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

<xsl:template match="node()
[not(self::b)
and
following-sibling::node()[1][self::b]
]">
<xsl:copy-of select="."/>
<c>
<xsl:copy-of select="key('kAllChildren', generate-id())"/>
</c>
</xsl:template>

<xsl:template match="*[*[1][self::b]]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<c>
<xsl:copy-of select="key('kAllChildren', generate-id())"/>
</c>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>

<xsl:template match="b"/>
</xsl:stylesheet>

It produces the correct result even with source xml like this:

<test>
<b>0</b>
<a>1</a>
<a>2</a>
<b>3</b>
<b>4</b>
5
<b>6</b>
<b>7</b>
<b>8</b>
9
<d>
<e>10</e>
<b>11</b>
<e>12</e>
<f>13</f>
<b>14</b>
<b>15</b>
16
<b>17</b>
<b>18</b>
<b>19</b>
20
</d>
21
<b>22</b>
23
</test>

Cheers,
Dimitre Novatchev

Martin Honnen said:
MRe said:
The closest I've come to getting this is to call a recursive
template, passing child::*[1] initially, and following-sibling::*[1]
for each recursive step, and also passing a 'block' parameter that, if
false and <b> is encountered, puts a <c> in and sets block to true,
and if true and <a> is encountered, puts a </c> in and sets to false.
However, this won't work as it won't put a </c> in if the last element
is a <b>.

With XSLT 1.0 you can solve such problems by processing sibling by
sibling, here is a sample stylesheet:

<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">

<xsl:eek:utput method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/test">
<xsl:copy>
<xsl:apply-templates select="node()[1]" mode="group"/>
</xsl:copy>
</xsl:template>

<xsl:template match="test/b" mode="group">
<c>
<xsl:copy-of select="."/>
<xsl:apply-templates
select="following-sibling::node()[1][self::b]"/>
</c>
<xsl:apply-templates
select="following-sibling::node()[not(self::b)][1]" mode="group"/>
</xsl:template>

<xsl:template match="test/b">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1][self::b]"/>
</xsl:template>

<xsl:template match="test/node()[not(self::b)]" mode="group">
<xsl:copy-of select="."/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="group"/>
</xsl:template>

</xsl:stylesheet>
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,989
Messages
2,570,207
Members
46,783
Latest member
RickeyDort

Latest Threads

Top