Reputation: 107
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:
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
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
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
Reputation: 11875
The problem comes from the fact, that the recursive template calls serve 2 purposes (1 too many):
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