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>
</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>
</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(.,'"') or
contains(.,',') or
contains(.,'
')">
<xsl:text>"</xsl:text>
<xsl:value-of select="replace(.,'"','""')"/>
<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>