DannyBoy
DannyBoy

Reputation: 107

XSLT transform siblings to children

I am trying to do something tricky in xslt I have a flat xml which contains a large list of siblings and depending on a name I want to transform them to children General rules for my transformation are:

  1. If tag/name is "BLOCK" then open a "BLOCK" element with tag/value as the attribute
  2. If tag/name is "BLOCK_END" close the "BLOCK" element (
  3. In all other cases create an element tag/name,put tag/value and immediatly close it

So for a following xml:

<message>
    <tag>
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag>
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag>
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
    </tag>
    <tag>
        <name>BLOCK_END</name>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag>
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
    </tag>
</message>

This is the result I am hoping for:

<message>
    <BLOCK id="first">
        <FOO>BAR</FOO>
        <BLOCK id="second">
            <FOO2>BAR2</FOO2>
        </BLOCK>
    </BLOCK>
    <BLOCK id="third">
        <FOO3>BAR3</FOO3>
    </BLOCK">    
</message>

I used the following xslt. This is working fine but sadly it finishes the execution after the encoutering the first BLOCK_END tag

<xsl:template match="/">
    <message>
        <xsl:apply-templates select="message/tag[1]" />
    </message>
</xsl:template>

<xsl:template match="tag">
    <xsl:variable name="tagName" select="name"/>
    <xsl:variable name="tagValue" select="value"/>
    <xsl:choose>
        <xsl:when test="$tagName = 'BLOCK'">
            <xsl:element name="{$tagName}">
                <xsl:attribute name="id">
                    <xsl:value-of select="$tagValue"/>
                </xsl:attribute>
                <xsl:apply-templates select="./following-sibling::*[1]" />
            </xsl:element>
        </xsl:when>
        <xsl:when test="$tagName = 'BLOCK_END'">
            <!-- DO NOTHING-->
        </xsl:when>
        <xsl:otherwise>
            <xsl:element name="{$tagName}">
                <xsl:value-of select="$tagValue"/>
            </xsl:element>
            <xsl:apply-templates select="./following-sibling::*[1]" />
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

UPDATE: Thanks to BitTickler I am getting closer but still not quite there.

Upvotes: 0

Views: 995

Answers (3)

DannyBoy
DannyBoy

Reputation: 107

The end solution was an extension upon BitTickler's idea:

I had to twick the original xml so that the end block tag also contains an identifier (we use it to search the corresponding END_BLOCK tag for the BLOCK tag)

<message>
    <tag>
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag>
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag>
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
        <value>second</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
        <value>first</value>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag>
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
        <value>third</value>
    </tag>
</message>

Then the trick was to do a recurrent call of the template for the END_BLOCK + 1 tag (see "magic" comment). There is no benefit of doing the "call-template" (just some leftover trial-and-error on my side) and I will revert it back to "apply-templates"

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

    <xsl:template match="/">
        <message>
            <xsl:call-template name="transformTag">
                <xsl:with-param name="tag" select="message/tag[1]"/>
            </xsl:call-template>
        </message>
    </xsl:template>

    <xsl:template name="transformTag">
        <xsl:param name="tag"/>
        <xsl:variable name="tagName" select="$tag/name"/>
        <xsl:variable name="tagValue" select="$tag/value"/>
        <xsl:choose>
            <xsl:when test="$tagName = 'BLOCK'">
                <!-- OPEN A BLOCK, RECURENTLY CALL FOR THE NEXT ELEMENT AND THEN MAKE A RECURENT CALL ONCE AGAIN FOR THE NEXT BLOCK-->
                <xsl:element name="{$tagName}">
                    <xsl:attribute name="id">
                        <xsl:value-of select="$tagValue"/>
                    </xsl:attribute>
                    <xsl:call-template name="transformTag">
                        <xsl:with-param name="tag" select="$tag/following-sibling::*[1]"/>
                    </xsl:call-template>
                </xsl:element>
                <!-- THIS IS WHERE THE MAGIC HAPPENS-->
                <xsl:variable  name="closingTag" select="$tag/following-sibling::*[name='END_BLOCK' and substring-before(value,'@@')=$tagValue][1]"/>

                <xsl:if test="$closingTag/name='END_BLOCK'">
                    <xsl:variable name="nextTag" select="$closingTag/following-sibling::*[1]"/>
                    <xsl:if test="$nextTag[name() = 'tag']">
                        <xsl:call-template name="transformTag">
                            <xsl:with-param name="tag" select="$nextTag"/>
                        </xsl:call-template>
                    </xsl:if>
                </xsl:if>
            </xsl:when>
            <xsl:when test="$tagName = 'END_BLOCK'">
                <!-- DO NOTHING AND EXIT THE RECURENT CALL (THIS CLOSES THE TAG)-->
            </xsl:when>
            <xsl:otherwise>
                <!-- PRINT THE REGULAR TAG AND RECURENTLY CALL FOR THE NEXT TAG-->
                <xsl:element name="_{$tagName}">
                    <xsl:value-of select="$tagValue"/>
                </xsl:element>
                <xsl:call-template name="transformTag">
                    <xsl:with-param name="tag" select="$tag/following-sibling::*[1]"/>
                </xsl:call-template>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 116959

There may be a shorter way, but this still seems to me to be the most straightforward approach:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:key name="child-item" match="tag[not(name='BLOCK')]" use="generate-id(preceding-sibling::tag[name='BLOCK'][@level=current()/@level][1])" />

<xsl:key name="child-block" match="tag[name='BLOCK']" use="generate-id(preceding-sibling::tag[name='BLOCK'][@level=current()/@level - 1][1])" />

<xsl:template match="/message">
    <xsl:variable name="first-pass-rtf">
        <xsl:apply-templates select="tag[1]" mode="first-pass" />
    </xsl:variable>
    <xsl:variable name="first-pass" select="exsl:node-set($first-pass-rtf)/tag" />
    <!-- output -->
    <message>
        <xsl:apply-templates select="$first-pass[name='BLOCK'][@level=1]"/>
    </message>
</xsl:template>

<!-- first-pass templates -->
<xsl:template match="tag[name='BLOCK']" mode="first-pass">
    <xsl:param name="level" select="0"/>
    <tag level="{$level + 1}">
        <xsl:copy-of select="*"/>
    </tag>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level + 1"/>
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="tag" mode="first-pass">
    <xsl:param name="level"/>
    <tag level="{$level}">
        <xsl:copy-of select="*"/>
    </tag>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level"/>
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="tag[name='BLOCK_END']" mode="first-pass">
    <xsl:param name="level"/>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level - 1"/>
    </xsl:apply-templates>
</xsl:template>

<!-- output templates -->
<xsl:template match="tag[name='BLOCK']">
    <BLOCK id="{value}">
        <xsl:apply-templates select="key('child-item', generate-id()) | key('child-block', generate-id())" />
    </BLOCK>
</xsl:template>

<xsl:template match="tag">
    <xsl:element name="{name}">
        <xsl:value-of select="value"/>
    </xsl:element>
</xsl:template>

</xsl:stylesheet>

This will start by rewriting you input as:

<xsl:variable name="first-pass-rtf">
    <tag level="1">
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag level="1">
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag level="2">
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag level="2">
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag level="1">
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag level="1">
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
</xsl:variable>

Then it becomes simple(r) to associate each tag to its parent block.

Upvotes: 2

BitTickler
BitTickler

Reputation: 11875

The problem comes from the fact, that the recursive template calls serve 2 purposes (1 too many):

  1. Proceed the cursor into the input tag elements by one towards the end.
  2. Handle the nesting level for the output.

For that to work, it is necessary to "return" both the current output state and the "iteration" state from the recursive function (template).

In a functional language, this can be demonstrated, e.g. with the following short code, emulating the situation.

type Node = 
    | Simple of string * string
    | Nested of string * string * Node list

let input =
    [ 
        Simple ("BLOCK","first")
        Simple ("FOO","BAR")
        Simple ("BLOCK","second")
        Simple ("FOO2","BAR2")
        Simple ("BLOCK_END","")
        Simple ("FOO3","BAR3")
        Simple ("BLOCK_END","")
    ]

let rec transform (result,remaining) =
    match remaining with
    | [] -> result,remaining
    | x::xs -> 
        match x with
        | Simple (n,v) when n = "BLOCK" ->
            let below,remaining' = transform ([],xs)
            transform (result @ [Nested(n,v,below)],remaining')
        | Simple (n,v) when n = "BLOCK_END" ->
            result,xs
        | Simple (n,v) ->
            transform (result @[x],xs)

transform ([],input)

Now that there is 1 solution strategy which works, the only question remaining is, how to apply this strategy to xslt transformations.

To kick start the whole thing, probably the first <tag> element should be transformed. And within its transformation the recursion happens.

The BLOCK_END should somehow return from the recursion such, that the current position is known, so the BLOCK section can resume at that point later on.

My best guess so far looks like this:

<xsl:template match="/">
  <xsl:element name="message">
    <xsl:apply-templates select="/message/tag[1]" />
  </xsl:element>
</xsl:template>

<xsl:template name="nest" match="tag">
  <xsl:variable name="tagName" select="name"/>
  <xsl:variable name="tagValue" select="value"/>

  <xsl:choose>
    <xsl:when test="./name='BLOCK'">
      <xsl:element name="{$tagName}">
        <xsl:attribute name="id">
          <xsl:value-of select="$tagValue"/>
        </xsl:attribute>
        <xsl:apply-templates select="./following-sibling::tag[1]" />
      </xsl:element>
      <!--TODO: We must continue here with the remaining nodes. But we do not know  how many 
      Nodes the block contained... Our cursor (.) is unaffected by previous recursion. -->
      <!--<xsl:apply-templates select="./following-sibling::tag[1]" />-->
    </xsl:when>
    <xsl:when test="./name='BLOCK_END'">
      <!--No nothing-->
    </xsl:when>
    <xsl:otherwise>
      <xsl:element name="{$tagName}">
        <xsl:value-of select="$tagValue"/>
      </xsl:element>
      <xsl:apply-templates select="./following-sibling::tag[1]" />
    </xsl:otherwise>
  </xsl:choose>

</xsl:template>

Producing the output:

<message>
   <BLOCK id="first">
      <FOO>BAR</FOO>
      <BLOCK id="second">
         <FOO2>BAR2</FOO2>
      </BLOCK>
   </BLOCK>
</message>

Upvotes: 1

Related Questions