Reputation: 484
I'm improving some XML I have inherited by using XSLT to clean things up, but I'm struggling with one section. Which looks like this:
<rules>
<if condition="equals" arg1="somevar" arg2="1"/>
<elseif condition="equals" arg1="somevar" arg2="2"/>
<elseif condition="equals" arg1="somevar" arg2="3"/>
<else/>
<if condition="equals" arg1="somevar" arg2="4"/>
<else/>
</rules>
This looks to be difficult to validate with XSD, so I'd like to transform it into something like this -- ideas?
<rules>
<conditionSet>
<if condition="equals" arg1="somevar" arg2="1"/>
<elseif condition="equals" arg1="somevar" arg2="2"/>
<elseif condition="equals" arg1="somevar" arg2="3"/>
<else/>
</conditionSet>
<conditionSet>
<if condition="equals" arg1="somevar" arg2="4"/>
<else/>
</conditionSet>
</rules>
Upvotes: 4
Views: 356
Reputation: 25034
It's an interesting XSLT challenge. But, uh, why are you changing the XML again? The pattern in the input can easily be defined by a regular expression, namely
(if, elseif*, else)*
and for that reason it is easy to validate with XSD.
It may be worth while to change -- a veteran vocabulary designer (Lynne A. Price) once told me that any repetition operator on group was automatically suspect and often meant that the group should be replaced by an element. She would, I guess, approve of your change. But to make sense, the rationale for the change has to be greater ease of processing, not easier validation.
Upvotes: 1
Reputation: 243449
I. Much simpler and shorter XSLT 1.0 solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kFollowing" match="elseif|else"
use="generate-id(preceding-sibling::if[1])"/>
<xsl:template match="/*">
<rules>
<xsl:apply-templates select="if"/>
</rules>
</xsl:template>
<xsl:template match="if">
<conditionSet>
<xsl:copy-of select=".|key('kFollowing', generate-id())"/>
</conditionSet>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<rules>
<if condition="equals" arg1="somevar" arg2="1"/>
<elseif condition="equals" arg1="somevar" arg2="2"/>
<elseif condition="equals" arg1="somevar" arg2="3"/>
<else/>
<if condition="equals" arg1="somevar" arg2="4"/>
<else/>
</rules>
the wanted, correct result is produced:
<rules>
<conditionSet>
<if condition="equals" arg1="somevar" arg2="1"/>
<elseif condition="equals" arg1="somevar" arg2="2"/>
<elseif condition="equals" arg1="somevar" arg2="3"/>
<else/>
</conditionSet>
<conditionSet>
<if condition="equals" arg1="somevar" arg2="4"/>
<else/>
</conditionSet>
</rules>
II. Even simpler and shorter 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:template match="/*">
<rules>
<xsl:for-each-group select="*" group-starting-with="if">
<conditionSet>
<xsl:sequence select="current-group()"/>
</conditionSet>
</xsl:for-each-group>
</rules>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the same XML document (above), the same correct result is produced:
<rules>
<conditionSet>
<if condition="equals" arg1="somevar" arg2="1"/>
<elseif condition="equals" arg1="somevar" arg2="2"/>
<elseif condition="equals" arg1="somevar" arg2="3"/>
<else/>
</conditionSet>
<conditionSet>
<if condition="equals" arg1="somevar" arg2="4"/>
<else/>
</conditionSet>
</rules>
Upvotes: 0
Reputation: 56162
One more:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/rules">
<xsl:copy>
<xsl:apply-templates select="if"/>
</xsl:copy>
</xsl:template>
<xsl:template match="if">
<conditionSet>
<xsl:copy-of select="."/>
<xsl:apply-templates select="
following-sibling::*[not(self::if)
and generate-id(preceding-sibling::if[1])
= generate-id(current())]
"/>
</conditionSet>
</xsl:template>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Upvotes: 0
Reputation: 60414
Group elseif
and else
elements by their immediately preceding if
element:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="block" match="elseif|else"
use="generate-id(preceding-sibling::if[1])"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="rules">
<xsl:copy>
<xsl:apply-templates select="@*|
node()[not(self::elseif or self::else)]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="if">
<conditionSet>
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
<xsl:apply-templates select="key('block', generate-id())"/>
</conditionSet>
</xsl:template>
</xsl:stylesheet>
This stylesheet produces the requested output.
Explanation: The xsl:key
associates each if
element with its following, related elements, so that, later, when we match an if
, we can simply wrap and copy the entire set.
Upvotes: 1