Markus L
Markus L

Reputation: 1014

Convert XML elements to XML attributes respecting existing attributes with XSLT

I have the following XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<cars filter="yes">
  <car>
    <brand>Volkswagen</brand>
    <make>Golf</make>
    <wheels>4</wheels>
    <extras hifi="yes" ac="no"/>
  </car>
</cars>

I want to flatten the element <car> so that it only has attributes - no more child elements!

So far I've produced this XSLT:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>

  <xsl:template match="cars">
    <cars>
      <xsl:apply-templates/>
    </cars>
  </xsl:template>

  <xsl:template match="car">
    <car>
      <xsl:for-each select="*">
        <xsl:attribute name="{name()}">
          <xsl:value-of select="text()"/>
        </xsl:attribute>
      </xsl:for-each>
    </car>
  </xsl:template>

</xsl:stylesheet>

This results in:

<cars>
  <car brand="Volkswagen" make="Golf" wheels="4" extras=""/>
</cars>

Problems:

Expected result:

<cars filter="yes">
  <car brand="Volkswagen" make="Golf" wheels="4" hifi="yes" ac="no"/>
</cars>

Upvotes: 0

Views: 42

Answers (1)

Tim C
Tim C

Reputation: 70598

For the first issue, with the filter attribute going missing, you can solve this by using the identity template instead of a specific template for cars

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

For extra appearing as an attribute, can the select statement to only select elements with text in

<xsl:for-each select="*[normalize-space()]">

And finally, for the attributes of extras, add another for-each to pick these up.

  <xsl:for-each select="*/@*">
    <xsl:attribute name="{name()}">
      <xsl:value-of select="."/>
    </xsl:attribute>
  </xsl:for-each>

Try this XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

  <xsl:template match="car">
    <car>
      <xsl:for-each select="*[normalize-space()]">
        <xsl:attribute name="{name()}">
          <xsl:value-of select="text()"/>
        </xsl:attribute>
      </xsl:for-each>
      <xsl:for-each select="*/@*">
        <xsl:attribute name="{name()}">
          <xsl:value-of select="."/>
        </xsl:attribute>
      </xsl:for-each>
    </car>
  </xsl:template>
</xsl:stylesheet>

In fact, the two xsl:for-each statements could be combined here

<xsl:template match="car">
<car>
  <xsl:for-each select="*[normalize-space()]|*/@*">
    <xsl:attribute name="{name()}">
      <xsl:value-of select="."/>
    </xsl:attribute>
  </xsl:for-each>
</car>
</xsl:template>

Note that you this assumes two different child elements of car don't have the same attribute names.

Upvotes: 1

Related Questions