onzinsky
onzinsky

Reputation: 591

Flatten any XML using XSLT

I am trying to automatically flatten any XML file using XSLT. Is it achievable? I am guessing it is, but I cannot find a way to do it.

Example input

<person>
    <name>
        <first>John</first>
        <last>Doe</last>
    </name>
    <data>
        <address>
            <street>Main</street>
            <city>Los Angeles</city>
        </address>
    </data>
</person>

Expected output

<person>
    <name_first>John</name_first>
    <name_last>Doe</name_last>
    <data_address_street>Main</data_address_street>
    <data_address_city>Los Angeles</data_address_city>
</person>

I have tried many things but the closer I've got is extracted from this answer.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    exclude-result-prefixes="xs" version="2.0">

    <xsl:output method="xml" indent="yes"/>

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

    <xsl:template match="/*/*">
        <xsl:for-each select="*">
            <xsl:element name="{concat(name(..),'_',name())}">
                <xsl:apply-templates select="node()"/>
            </xsl:element>
        </xsl:for-each>
    </xsl:template>

</xsl:stylesheet>

As @Michael Kay comments, one example does not constitute a specification. So I wanted to point out any comments, processing instructions, mixed content, and everything not in the example should be ignored.

Upvotes: 1

Views: 634

Answers (1)

Martin Honnen
Martin Honnen

Reputation: 167401

You can do it with string-join:

  <xsl:template match="/*">
      <xsl:copy>
          <xsl:apply-templates select="descendant::*[not(*)]"/>
      </xsl:copy>
  </xsl:template>
  
  <xsl:template match="*">
      <xsl:element name="{string-join(ancestor-or-self::*[position() ne last()]/name(), '_')}">
          <xsl:value-of select="."/>
      </xsl:element>
  </xsl:template>

With huge documents and XSLT 3 and streaming (e.g. Saxon EE) you can do

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">
    
    <xsl:mode streamable="yes"/>
    
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates select="descendant::text()"/>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="text()">
        <xsl:element name="{string-join(ancestor::*[position() lt last()]/name(), '_')}">
            <xsl:value-of select="."/>
        </xsl:element>
    </xsl:template>
    
</xsl:stylesheet>

Upvotes: 2

Related Questions