Recursively combine identical sibling elements in XSLT

How can I combine all sibling elements with the same name and the same attributes into one element using XSLT? The transformation should also be applied recursively to child elements that merge. This is the source document:

<?xml version="1.0"?> <Root> <Element id="UniqueId1"> <SubElement1/> <SubElement2> <LeafElement1/> </SubElement2> </Element> <Element id="UniqueId1"> <SubElement2> <LeafElement1/> <LeafElement2/> </SubElement2> <SubElement3/> </Element> <Element id="UniqueId2"> <SubElement1/> <SubElement4/> </Element> </Root> 

It should be converted to:

 <?xml version="1.0"?> <Root> <Element id="UniqueId1"> <SubElement1/> <SubElement2> <LeafElement1/> <LeafElement2/> </SubElement2> <SubElement3/> </Element> <Element id="UniqueId2"> <SubElement1/> <SubElement4/> </Element> </Root> 

Any elements with the same names and attributes are combined into one element. Then their children are checked. If one of them has the same name and the same attributes, they are combined. This transformation is applied recursively to all elements.

Edit: To clarify, all of these conditions must be true to combine the two elements.

  • They have the same element name.
  • They have the same attributes.
  • The values โ€‹โ€‹of each corresponding attribute are the same.
  • They are brothers and sisters (applied recursively, so any identical parent elements are merged and merged before their children are considered)

These elements are identical and must be combined:

  • <Item/> and <Item/> (same name, same attributes)
  • <Item Attr="foo"/> and <Item Attr="foo"/> (same name, same attributes)

These elements are not identical and should not be combined:

  • <Item/> and <SubItem/> (another name)
  • <Item Attr="foo"/> and <Item/> (different attributes)
  • <Item Attr="foo"/> and <Item Attr="bar"/> (different attribute values)
+4
source share
3 answers

This should do the job:

 <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" indent="yes"/> <xsl:key name="atts-by-name" match="@*" use="name()"/> <xsl:template match="Root"> <xsl:copy> <xsl:call-template name="merge"> <xsl:with-param name="elements" select="*"/> </xsl:call-template> </xsl:copy> </xsl:template> <xsl:template name="merge"> <xsl:param name="elements"/> <xsl:for-each select="$elements"> <xsl:variable name="same-elements" select="$elements[name()=name(current()) and count(@*)=count(current()/@*) and count(@*[. = key('atts-by-name',name())[generate-id(..)=generate-id(current())]])=count(@*)]"/> <xsl:if test="generate-id($same-elements[1]) = generate-id()"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:call-template name="merge"> <xsl:with-param name="elements" select="$same-elements/*"/> </xsl:call-template> </xsl:copy> </xsl:if> </xsl:for-each> </xsl:template> </xsl:stylesheet> 

The hard part is identifying the same elements; indexing attributes by name is required to verify that all attributes are uniform.

+3
source

The easiest way I can come up with is to parse all elements with the same identifier when the first element meets this identifier. That would look something like this:

 <xsl:variable name="curID" select="@id"/> <xsl:if test="count(preceding-sibling::*[@id=$curID])=0"> <xsl:copy> <xsl:apply-templates select="@*"/> <xsl:for-each select="following-sibling::*[@id=$curID]"> <xsl:apply-templates select="@*"/> </xsl:for-each> <xsl:apply-templates select="node()"/> <xsl:for-each select="following-sibling::*[@id=$curID]"> <xsl:apply-templates select="node()"/> </xsl:for-each> </xsl:copy> </xsl:if> 

This is not OK, so a little tweaking may be required.

To make this work recursively is a bit more of a problem. You will need to parse SubElement2 in the second element when you process SubElement2 in the tag of the first element. This will be confusing enough to handle arbitrary depths.
I do not know your specific use case, but the simplified answer may be to re-execute the above conversion until the result is the same as the input.

Extending the if expression for fire for elements of the same name should be simple.

+3
source

If you are using XSLT2, you can use the grouping object. Here is an early tutorial:

 http://www.xml.com/pub/a/2003/11/05/tr.html 

Here is a later one written by a group that produces great lessons:

 http://www.zvon.org/xxl/XSL-Ref/Tutorials/index.html 

If you are limited to XSLT1, this is possible, but more difficult.

If you're still stuck, try the Dave Pawson FAQ: http://www.dpawson.co.uk/xsl/sect2/N4486.html

+1
source

All Articles