Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

XSLT 1.0 Rearrange specific descendant / child notes to the same level of the parents / ancestors

Tags:

xml

xslt

I'm searching for a general approach to rearrange some specific descendant to the level of their ancestor nodes.

What is important:

  • I need a general template which works for p and x.
  • The order of nodes a b DEEPSPACENODE (from up to bottom) should not be changed as much as possible.

My input:

<root>
    <p>
        <a>1</a>
        <a>2</a>
        <a><DEEPSPACENODE/></a>
        <b>3</b>
        <b>4</b>
        <a>5</a>
        <a>6</a>
        <b>7</b>
    </p>

    <x>
        <a>8</a>
        <a>9</a>
        <b>10</b>
        <b>11</b>
        <a>12</a>
        <a>13</a>
        <b>14</b>
    </x>    
</root>

My desired output:

<root>
    <p>
        <a>1</a>
        <a>2</a>
        <a/>
    </p>
    <DEEPSPACENODE/>
    <b>3</b>
    <b>4</b>
    <p>
        <a>5</a>
        <a>6</a>
    </p>
    <b>7</b>

    <x>
        <a>8</a>
        <a>9</a>
    </x>
    <b>10</b>
    <b>11</b>
    <x>
        <a>12</a>
        <a>13</a>
    </x>
    <b>14</b>
</root>

Thank you for your help. I tried to solve it myself but I was not succesful.

like image 651
therealmarv Avatar asked Nov 17 '25 09:11

therealmarv


1 Answers

This is a more generic solution that can produce the wanted result without imposing any restrictions on the XML document -- we don't suppose any pre-defined level of nesting, or that an element named b is present:

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

  <xsl:key name="kFollowing"
   match="a[preceding-sibling::*[1]
                              [self::a]
           ]"
   use="concat(
         generate-id(
                preceding-sibling::*[not(self::a)][1]
                    ),
         generate-id(..)
               )
         "/>

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

 <xsl:template match="*[a]">
   <xsl:apply-templates/>
 </xsl:template>

 <xsl:template match=
   "a[not(preceding-sibling::*[1][self::a])]">

   <xsl:variable name="vGroup" select=
   ".|key('kFollowing',
             concat(generate-id(preceding-sibling::*[1]),
                    generate-id(..)
                   )
          )"/>

   <xsl:element name="{name(..)}">
     <xsl:apply-templates mode="group"
     select="$vGroup"/>
   </xsl:element>

   <xsl:apply-templates select="$vGroup/*"/>
 </xsl:template>

 <xsl:template match="a" mode="group">
  <a>
   <xsl:apply-templates select="node()[not(self::*)]"/>
  </a>
 </xsl:template>

 <xsl:template match=
  "a[preceding-sibling::*[1]
                       [self::a]
    ]"/>
</xsl:stylesheet>

When applied on the provided XML document:

<root>
    <p>
        <a>1</a>
        <a>2</a>
        <a>
            <DEEPSPACENODE/>
        </a>
        <b>3</b>
        <b>4</b>
        <a>5</a>
        <a>6</a>
        <b>7</b>
    </p>
    <x>
        <a>8</a>
        <a>9</a>
        <b>10</b>
        <b>11</b>
        <a>12</a>
        <a>13</a>
        <b>14</b>
    </x>
</root>

the wanted, correct result is produced:

<root>
   <p>
      <a>1</a>
      <a>2</a>
      <a/>
   </p>
   <DEEPSPACENODE/>
   <b>3</b>
   <b>4</b>
   <p>
      <a>5</a>
      <a>6</a>
   </p>
   <b>7</b>
   <x>
      <a>8</a>
      <a>9</a>
   </x>
   <b>10</b>
   <b>11</b>
   <x>
      <a>12</a>
      <a>13</a>
   </x>
   <b>14</b>
</root>

Now, with the following XML document this solution still produces the wanted result, while the solution by @empo chokes on it:

<root>
   <c>
    <p>
        <a>1</a>
        <a>2</a>
        <a>
            <DEEPSPACENODE/>
        </a>
        <z>3</z>
        <z>4</z>
        <a>5</a>
        <a>6</a>
        <b>7</b>
    </p>
    <x>
        <a>8</a>
        <a>9</a>
        <b>10</b>
        <z>11</z>
        <a>12</a>
        <a>13</a>
        <b>14</b>
    </x>
  </c>  
</root>

The same transformation, when applied on the XML document above, produces again the correct, wanted result:

<root>
   <c>
      <p>
         <a>1</a>
         <a>2</a>
         <a/>
      </p>
      <DEEPSPACENODE/>
      <z>3</z>
      <z>4</z>
      <p>
         <a>5</a>
         <a>6</a>
      </p>
      <b>7</b>
      <x>
         <a>8</a>
         <a>9</a>
      </x>
      <b>10</b>
      <z>11</z>
      <x>
         <a>12</a>
         <a>13</a>
      </x>
      <b>14</b>
   </c>
</root>

II. XSLT 2.0 solution:

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

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

 <xsl:template match="*[a]">
  <xsl:for-each-group select="node()"
                      group-adjacent="name() eq 'a'">
    <xsl:apply-templates select="current-group()"
                         mode="group">
      <xsl:with-param name="pGroup"
                      select="current-group()"/>
    </xsl:apply-templates>
    <xsl:apply-templates select="current-group()[self::a]/*"/>
   </xsl:for-each-group>
  </xsl:template>

 <xsl:template match="a" mode="group"/>

 <xsl:template mode="group"
          match="a[not(preceding-sibling::*[1][self::a])]" >
   <xsl:param name="pGroup" as="node()*"/>

   <xsl:element name="{name(..)}">
     <xsl:apply-templates select="$pGroup" mode="shallow"/>
   </xsl:element>
 </xsl:template>

  <xsl:template match="a" mode="shallow">
    <a>
      <xsl:apply-templates select="node()[not(self::*)]"/>
    </a>
   </xsl:template>
</xsl:stylesheet>
like image 151
Dimitre Novatchev Avatar answered Nov 21 '25 04:11

Dimitre Novatchev



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!