XML > 2 > CSV

I was faced with the possibility of writing this from scratch and while I love a challenge, sometimes it’s easier to just post it:)

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

  <xsl:output method="text"/>

  <xsl:variable name="fields" 
                select="distinct-values(//file/*[not(*)]/name(.))"/>

  <xsl:template match="/">
    <!--header row-->
    <xsl:value-of select="$fields" separator=","/>

    <!--body-->
    <xsl:apply-templates select="*"/>

    <!--final line terminator-->
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>

  <!--elements only process elements, not text-->
  <xsl:template match="*">
    <xsl:apply-templates select="*"/>
  </xsl:template>

  <!--these elements are CSV fields-->
  <xsl:template match="file/*[not(*)]">
    <!--replicate ancestors if necessary-->
    <xsl:if test="position()=1 and ../preceding-sibling::file">
      <xsl:for-each select="ancestor::file[position()>1]/*[not(*)]">
        <xsl:call-template name="doThisField"/>
      </xsl:for-each>
    </xsl:if>
    <xsl:call-template name="doThisField"/>
  </xsl:template>

  <!--put out a field ending the previous field and escaping content-->
  <xsl:template name="doThisField">
    <xsl:choose>
      <xsl:when test="name(.)=$fields[1]">
        <!--previous line terminator-->
        <xsl:text>&#xa;</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <!--previous field terminator-->
        <xsl:text>,</xsl:text>
      </xsl:otherwise>
    </xsl:choose>
    <!--field value escaped per RFC4180-->
    <xsl:choose>
      <xsl:when test="contains(.,'&#x22;') or 
                      contains(.,',') or
                      contains(.,'&#xa;')">
        <xsl:text>"</xsl:text>
        <xsl:value-of select="replace(.,'&#x22;','&#x22;&#x22;')"/>
        <xsl:text>"</xsl:text>
      </xsl:when>
      <xsl:otherwise><xsl:value-of select="."/></xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  </xsl:stylesheet>

And as a special added bonus. Here is the XSL to eliminate any extraneous information from the pipeline.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
	<xsl:output method="xml" encoding="UTF-8" indent="yes" omit-xml-declaration="yes"/>
	<!-- ======================================= -->
	<!-- Identity template -->
	<!-- ======================================= -->
	<xsl:template match="@*|node()">
		<xsl:copy>
			<xsl:apply-templates select="@*|node()"/>
		</xsl:copy>
	</xsl:template>
	<!-- ======================================= -->
	<!-- Exclude the file path attribute. -->
	<!-- ======================================= -->
	<xsl:template match="file">
		<xsl:copy>
			<xsl:apply-templates/>
		</xsl:copy>
	</xsl:template>
	<!-- ======================================= -->
	<!-- Exclude the content element. -->
	<!-- ======================================= -->
	<xsl:template match="file/content"/>
</xsl:stylesheet>

Leave a Reply