This transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="EmptyElements" priority="5">
<xsl:copy>
<xsl:apply-templates mode="enumerate" select=
"../*[not(self::EmptyElements) and not(node())]" />
</xsl:copy>
</xsl:template>
<xsl:template match="Songs/Song/*" mode="enumerate">
<xsl:value-of select="substring(',', not(position() = 1), 1)"/>
<xsl:value-of select="name()"/>
</xsl:template>
<xsl:template match="Songs/Song/*[not(node())]"/>
</xsl:stylesheet>
when applied on the provided source XML document:
<Root>
<FirstName>Bob</FirstName>
<LastName>Marley</LastName>
<ID>BM1234</ID>
<Songs>
<Song>
<EmptyElements></EmptyElements>
<SongName>No woman no cry</SongName>
<Year>1974</Year>
<album></album>
<studio></studio>
<rating></rating>
</Song>
</Songs>
</Root>
produces the wanted, correct result:
<Root>
<FirstName>Bob</FirstName>
<LastName>Marley</LastName>
<ID>BM1234</ID>
<Songs>
<Song>
<EmptyElements>album,studio,rating</EmptyElements>
<SongName>No woman no cry</SongName>
<Year>1974</Year>
</Song>
</Songs>
</Root>
Explanation:
- The identity rule, when selected for execution, copies the matched node "as-is"
- The template matching
Songs/Song/*[not(node())]
, when selected for execution, does nothing, which results in "deleting" (not copying) the matched node in the output.
- The template matching
EmptyElements
has a higher priority specified than the "deleting" template mentioned above, so it is selected for execution on any EmptyElements
element.
- The matched
EmptyElements
element is shallow-copied to the output, and then its content (body) is produced by applying templates in mode enumerate
to all empty siblings-elements.
- Finally, the template in mode
enumerate
matches any child element of a Song
element that is a child of a Songs
element. It is selected for execution by the <xsl:apply-templates>
instruction in step 4.
above and is applied only on the empty-element siblings of the EmptyElements
element. This template does two things: a) output a comma, if this is not the first node in the node-list; b) output the name of the matched element. In this way all names of the empty siblings-elements of the EmptyElements
element are output, separated by commas.
Update:
Dear reader,
We have a companion answer to this question, which starts with Or simply:
And alludes that it is simpler than the code in this answer.
Instead of telling you that this answer is simpler than the Or simply:
-answer, I have summarized a few facts that are related to simplicity, and you can make your own conclusion. In the following table, the values in each left sub-column are for this, current solution. The values in each right sub-column are for the Or simply:
-solution:
In addition to this, the Or simply:
- solution also has a potential performance, and a certain streamability issue -- see this fragment:
<xsl:if test="position()!=last()">
<xsl:text>, </xsl:text>
</xsl:if>
Compare with what the current solution uses:
not(position() = 1)
See Dr. Michael Kay's recommendation, that the latter is "a much better way of coding this" than the former, and his explanation why:
"Why? Because however hard the optimizer works, the last() function is hard
work: it involves some kind of lookahead. With "position() ne last()" the
lookahead might be limited to one element, but it's still a lot more
complicated than testing whether the position is 1.
With streaming coming along, the latter formulation is also more likely to
be streamable (because lookahead is impossible with streaming)."
Conclusion:
Whenever someone tells us: "Or simply:
", it is good to take a few metrics before taking their statement for granted ...