2

I have a XML file where elements B are inside elements A and I want to move them up. From:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <A>
    <C>Text</C>
    Text again

    More text
    <D>Other text</D>
    <B>Text again</B>
    <C>No</C>
    <D>May be</D>
    <B>What?</B>
  </A>
  <A>
    Something
    <B>Nothing</B>
    <D>Again</D>
    <B>Content</B>
    End
  </A>
</root>

I would like to have:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <A>
    <C>Text</C>
    Text again

    More text
    <D>Other text</D>
  </A>
  <B>Text again</B>
  <A>
    <C>No</C>
    <D>May be</D>
  </A>
  <B>What?</B>
  <A>
    Something
  </A>
  <B>Nothing</B>
  <A>
    <D>Again</D>
  </A>
  <B>Content</B>
  <A>
    End
  </A>
</root>

The closest XSLT program I have is this:

<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    exclude-result-prefixes="xs" version="1.0">

    <xsl:output method="xml" indent="yes"/>

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

    <xsl:template match="A">
      <xsl:for-each select="*">
    <xsl:choose>
      <xsl:when test="name()='B'">
            <xsl:apply-templates select="."/>
      </xsl:when>
      <xsl:otherwise>
                  <xsl:element name="A">
                      <xsl:apply-templates select="."/>
          </xsl:element>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:for-each>
    </xsl:template>

</xsl:stylesheet>

It has two problems: it ignores text nodes (this is probably just a matter of adding |text() to the select="*") but, more important, it creates a element for each node while I would like them to stay together under one . For instance, the above stylesheet makes:

<A><C>No</C></A>
<A><D>May be</D></A>

where I want:

<A><C>No</C>
   <D>May be</D></A>

In my XML files, are always direct children of , and there is no or nesting.

The main use case is producing HTML where UL and OL cannot be inside a P.

This question is related but not identical to xslt flattening out child elements in a DocBook para element (and may be also to Flatten xml hierarchy using XSLT )

bortzmeyer
  • 34,164
  • 12
  • 67
  • 91
  • Your output shows more (much more) than just moving `B` elements up to the `root` element. You also split the `A` elements at that point, which is going to be more (much more) difficult (at least in XSLT 1.0). How important is this part? – michael.hor257k Jan 02 '19 at 18:33
  • @michael.hor257k Yes, it is very important that B moves upward and in the process, splits its former parent A in two. – bortzmeyer Jan 02 '19 at 19:25

3 Answers3

2

As I said in the comment to your question, this is not about moving elements up in hierarchy. It is about grouping nodes, and creating a new parent A element for each group determined by the dividing B element.

In XSLT 1.0 this can be achieved using a so-called sibling recursion:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/root">
    <xsl:copy>
        <xsl:apply-templates select="A"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="A">
    <xsl:copy>
        <xsl:apply-templates select="node()[1][not(self::B)]" mode="sibling"/>
    </xsl:copy>
    <xsl:apply-templates select="B[1]" mode="sibling"/>
</xsl:template>

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

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

</xsl:stylesheet>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
1

An XSLT-1.0 solution - which is quite ugly - is the following. The output is as desired, but only for this simple MCVE. A general solution would be far more complicated as @michael.hor257k mentioned in the comments. Without more data it is unlikely to create a better solution in XSLT-1.0. Solutions for XSLT-2.0 and above may simplify this.

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

    <xsl:template match="/root">
        <xsl:copy>
            <xsl:for-each select="A">
                <xsl:if test="normalize-space(text()[1])">
                    <A>
                        <xsl:copy-of select="text()[1]" />
                    </A>
                </xsl:if>
                <xsl:if test="preceding::*">
                    <xsl:copy-of select="B[1]" />
                </xsl:if>
                <A>
                    <xsl:copy-of select="C[1] | C[1]/following-sibling::text()[1] | D[1]" />
                </A>
                <xsl:if test="not(preceding::*)">
                    <xsl:copy-of select="B[1]" />
                </xsl:if>
                <A>
                    <xsl:copy-of select="C[2] | C[2]/following-sibling::text()[1]" />
                    <xsl:if test="D[2]">
                        <xsl:copy-of select="D[2]" />
                    </xsl:if>
                </A>
                <xsl:copy-of select="B[2]" />
                <xsl:if test="normalize-space(text()[last()])">
                    <A>
                        <xsl:copy-of select="text()[last()]" />
                    </A>
                </xsl:if>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>         

</xsl:stylesheet>

Concerning the situation of

<A><C>No</C></A>
<A><D>May be</D></A>

It is handled appropriately in the above code. So its output is

<A>
    <C>No</C>
    <D>May be</D>
</A>
zx485
  • 28,498
  • 28
  • 50
  • 59
0

Easy in XSLT 2 or 3 with group-adjacent=". instance of element(B)" or group-adjacent="boolean(self::B)", here is an XSLT 3 example (XSLT 3 is supported by Saxon 9.8 or 9.9 on Java and .NET (https://sourceforge.net/projects/saxon/files/Saxon-HE/) and by Altova since 2017 releases):

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:mode on-no-match="shallow-copy"/>

  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="A">
      <xsl:for-each-group select="node()" group-adjacent=". instance of element(B)">
          <xsl:choose>
              <xsl:when test="current-grouping-key()">
                  <xsl:apply-templates select="current-group()"/>
              </xsl:when>
              <xsl:otherwise>
                  <xsl:copy select="..">
                      <xsl:apply-templates select="current-group()"/>
                  </xsl:copy>
              </xsl:otherwise>
          </xsl:choose>
      </xsl:for-each-group>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/gWmuiKv

In XSLT 2 you need to spell out the <xsl:mode on-no-match="shallow-copy"/> as the identity transformation template and use xsl:element instead xsl:copy:

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

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

  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="A">
      <xsl:for-each-group select="node()" group-adjacent=". instance of element(B)">
          <xsl:choose>
              <xsl:when test="current-grouping-key()">
                  <xsl:apply-templates select="current-group()"/>
              </xsl:when>
              <xsl:otherwise>
                  <xsl:element name="{name(..)}" namespace="{namespace-uri(..)}">
                      <xsl:apply-templates select="current-group()"/>
                  </xsl:element>
              </xsl:otherwise>
          </xsl:choose>
      </xsl:for-each-group>
  </xsl:template>

</xsl:transform>

http://xsltransform.hikmatu.com/pPqsHT2

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
  • No XSLT v3 processor for my Debian machine, it seems (the most recent is XSLT 2, with Saxon B) – bortzmeyer Jan 02 '19 at 19:34
  • Saxon 9.8 or 9.9 HE are available on Sourceforge to allow anyone with a recent Java JRE (I think 1.6 for 9.8 and 1.8 for 9.9) to run it. Not sure why Saxon B is "available" for Debian but Saxon 9.8 or 9.9 not. I have added an XSLT 2 transcription of the previous XSLT 3 solution. – Martin Honnen Jan 02 '19 at 19:39