tags:

views:

409

answers:

3

Is it possible to merge every sequence of nodes of the same specified type? ('aaa' in this case) (not just first occurrence of sequence)

Here is my XML input:

<block>
    <aaa>text1</aaa>
    <aaa>text2</aaa>
    <aaa><xxx>text3</xxx></aaa>
    <bbb>text4</bbb>
    <aaa>text5</aaa>
    <bbb><yyy>text6</yyy></bbb>
    <bbb>text7</bbb>
    <aaa>text8</aaa>
    <aaa><zzz>text9</zzz></aaa>
    <aaa>texta</aaa>
</block>

And I want following output:

<block>
    <aaa>text1text2<xxx>text3</xxx></aaa>
    <bbb>text4</bbb>
    <aaa>text5</aaa>
    <bbb><yyy>text6</yyy></bbb>
    <bbb>text7</bbb>
    <aaa>text8<zzz>text9</zzz>texta</aaa>
</block>

Any help appreciated

A: 

Assuming you only have one block, Muenchian method is the most optimized way to do this:

<!-- group nodes by name -->
<xsl:key name="block-children-by-name" match="block/*" use="name()"/>

<!-- for nodes that aren't first in their group, no output -->
<xsl:template match="block/*" />

<!-- for nodes that are first in their group, combine group children and output -->
<xsl:template match="block/*[generate-id() =
                             generate-id(key('block-children-by-name', name())[1])]">
   <xsl:copy>
     <xsl:copy-of select="key('block-children-by-name', name())/*"/>
   </xsl:copy>
</xsl:template>

Note that this only merges the child nodes, and not e.g. any attributes that may occur on aaa and bbb themselves.

Pavel Minaev
A: 

Here is another way to do this.

First, match on all the child nodes of the block element

<xsl:template match="block/child::*">

Next, check if the element's most direct sibling has a different name, indicating this is the first of one or more adjacent elements:

<xsl:if test="local-name(preceding-sibling::*[position()=1]) != $name">

If so, you can copy that node. Then, you need to copy of following siblings with the same name. I did this by recursively calling a template on each immediately following sibling with the same name

<xsl:apply-templates select="following-sibling::*[1][local-name()=$name]" mode="next"/>

Putting this all together gives

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
   <xsl:output method="xml" indent="yes"/>

   <!-- Match children of the block element -->
   <xsl:template match="block/child::*">
      <xsl:variable name="name" select="local-name()"/>

      <!-- Is this the first element in a sequence? -->
      <xsl:if test="local-name(preceding-sibling::*[position()=1]) != $name">
         <xsl:copy>
            <xsl:apply-templates />

            <!-- Match the next sibling if it has the same name -->
            <xsl:apply-templates select="following-sibling::*[1][local-name()=$name]" mode="next"/>
         </xsl:copy>
      </xsl:if>
   </xsl:template>

   <!-- Recursive template used to match the next sibling if it has the same name -->
   <xsl:template match="block/child::*" mode="next">
      <xsl:variable name="name" select="local-name()"/>
         <xsl:apply-templates />
      <xsl:apply-templates select="following-sibling::*[1][local-name()=$name]" mode="next"/>
   </xsl:template>

   <!-- Template used to copy a generic node -->
   <xsl:template match="@* | node()">
         <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
         </xsl:copy>
   </xsl:template>
</xsl:stylesheet>
Tim C
For solution of my problem i only needed to merge 'aaa' nodes, therefore I had to slightly adjust your solution by changing 'match="int:block/child::*"' to 'match="int:block/child::aaa"' otherwise it works flawlessly.
Peter
A: 

Here's another approach, without using recursive templates.

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

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

  <xsl:template match="aaa">
    <xsl:if test="not(preceding-sibling::*[1]/self::aaa)">
      <xsl:variable name="following" 
                    select="following-sibling::aaa[
                              not(preceding-sibling::*[
                                not(self::aaa) and
                                not(following-sibling::aaa = current())
                              ])
                            ]"/>
      <xsl:copy>
        <xsl:apply-templates select="$following/@*"/>
        <xsl:apply-templates select="@*"/>
        <xsl:apply-templates select="node()"/>
        <xsl:apply-templates select="$following/node()"/>
      </xsl:copy>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

The rather convoluted XPath expression for selecting the following sibling aaa nodes which are merged with the current one:

following-sibling::aaa[                       # following 'aaa' siblings
  not(preceding-sibling::*[                   #   if they are not preceded by
    not(self::aaa) and                        #     a non-'aaa' node
    not(following-sibling::aaa = current())   #     after the current node
  ])
]
Jukka Matilainen