Reputation: 591
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
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