Modifying a XSLT for converting XML to tab delimited text file

user605179 picture user605179 · Feb 6, 2011 · Viewed 7.4k times · Source

A system that I am using currently allows me to export selected record data in XML format. The system also provides an option to use XSLT to transform the XML. I like to use it to convert the XML to tab delimited text so that I can import the data to another application.

Here is a XML sample with three records exported. Four data fields are selected to be exported with each record.

<?xml version="1.1" encoding="UTF-8"?>
<!DOCTYPE Export>
<ns:Export xmlns:ns="http://www.canto.com/ns/Export/1.0">
<ns:Layout tableName="AssetRecords">
    <ns:Fields>
        <ns:Field uid="{5bbb974b-d50a-45de-bb40-f6d7fb36bfd8}" type="2" valueInterpretation="5">
            <ns:Name>Date Taken / Scanned</ns:Name>
        </ns:Field>
        <ns:Field uid="{af4b2e43-5f6a-11d2-8f20-0000c0e166dc}" type="0" valueInterpretation="0">
            <ns:Name>Title</ns:Name>
        </ns:Field>
        <ns:Field uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}" type="2" valueInterpretation="0">
            <ns:Name>ID</ns:Name>
        </ns:Field>
        <ns:Field uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}" type="0" valueInterpretation="0">
            <ns:Name>Categories</ns:Name>
        </ns:Field>
    </ns:Fields>
</ns:Layout>
<ns:Items>
    <ns:Item catalogid="4" id="1">
        <ns:FieldValue uid="{5bbb974b-d50a-45de-bb40-f6d7fb36bfd8}" displayValue="Oct 10, 2010">2049029450</ns:FieldValue>
        <ns:FieldValue uid="{af4b2e43-5f6a-11d2-8f20-0000c0e166dc}">teapot</ns:FieldValue>
        <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">1</ns:FieldValue>
        <ns:FieldValue uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}">
            <ns:CategoryValue>Artifacts:Daily Life Item</ns:CategoryValue>
            <ns:CategoryValue>Images:Objects</ns:CategoryValue>
        </ns:FieldValue>
    </ns:Item>
    <ns:Item catalogid="4" id="2">
        <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">2</ns:FieldValue>
        <ns:FieldValue uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}">
            <ns:CategoryValue>Artifacts:Personal Artefact</ns:CategoryValue>
            <ns:CategoryValue>Artifacts:Daily Life Item</ns:CategoryValue>
            <ns:CategoryValue>Images:Objects</ns:CategoryValue>
        </ns:FieldValue>
    </ns:Item>
    <ns:Item catalogid="4" id="10">
        <ns:FieldValue uid="{af4b2e43-5f6a-11d2-8f20-0000c0e166dc}">Product Overview</ns:FieldValue>
        <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">10</ns:FieldValue>
        <ns:FieldValue uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}">
            <ns:CategoryValue>Documents:Paper Records</ns:CategoryValue>
        </ns:FieldValue>
    </ns:Item>
</ns:Items>
</ns:Export>

My desired tab delimited output text is like the following table when open in Excel

https://lh5.googleusercontent.com/_Wsib_NwEZrA/TU6vzqbEGDI/AAAAAAAAAU0/V1SfGJyXJhQ/s800/2011-02-06_185321.png

I was given a XSLT file to do the conversion. However, it can only handle simple, straight-forward XML structure.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:ns="http://www.canto.com/ns/Export/1.0"
    xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="text" indent="yes" encoding="utf-8" standalone="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/">
    <Root>
        <xsl:apply-templates select="/ns:Export/ns:Items/ns:Item"/>
    </Root>
</xsl:template>

<xsl:template match="ns:Item">
    <xsl:for-each select="ns:FieldValue">
        <xsl:value-of select="normalize-space(text())"/>
        <xsl:text>&#09;</xsl:text>
    </xsl:for-each>
    <xsl:text>
</xsl:text>
</xsl:template>

</xsl:stylesheet>

I like to have an improved version so that

  • field names are shown as column headers
  • for date : the display value is used, not the numbers
  • for categories: all the categories displayed in separate columns
  • for empty data: empty cell is shown

I have limited XML knowledge. Hope someone can assist. Thanks :-)

Edited on 25 April 2011

The XSLT provided has worked very well until I tested on some new fields. Below is the XML exported from the system. It consists of two records.

<?xml version="1.1" encoding="UTF-8"?>
<!DOCTYPE Export>
<ns:Export xmlns:ns="http://www.canto.com/ns/Export/1.0">
<ns:Layout tableName="AssetRecords">
    <ns:Fields>
        <ns:Field uid="{00a3b951-4c7b-4751-90c0-c88cf0eb4983}" type="10" valueInterpretation="0">
            <ns:Name>Test Table Field</ns:Name>
            <ns:Layout tableName="AssetRecords/{00a3b951-4c7b-4751-90c0-c88cf0eb4983}">
                <ns:Fields>
                    <ns:Field uid="{86b3bf92-e7cc-4150-bd74-191ce4bf9374}" type="0" valueInterpretation="0">
                        <ns:Name>Accession Number</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}" type="2" valueInterpretation="0">
                        <ns:Name>Host Item ID</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}" type="2" valueInterpretation="0">
                        <ns:Name>ID</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{8322d5e0-edc9-4c2a-9991-702c376d9edc}" type="0" valueInterpretation="0">
                        <ns:Name>Description / Synopsis</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{5bbb974b-d50a-45de-bb40-f6d7fb36bfd8}" type="2" valueInterpretation="5">
                        <ns:Name>Date Taken / Scanned</ns:Name>
                    </ns:Field>
                </ns:Fields>
            </ns:Layout>
        </ns:Field>
        <ns:Field uid="{04a4080f-fec0-4e01-822f-b9125c0b5ac9}" type="10" valueInterpretation="0">
            <ns:Name>User Comment Thread</ns:Name>
            <ns:Layout tableName="AssetRecords/{04a4080f-fec0-4e01-822f-b9125c0b5ac9}">
                <ns:Fields>
                    <ns:Field uid="{dd48a259-8a55-46a6-b7cd-95a0335197e3}" type="5" valueInterpretation="0">
                        <ns:Name>User Comment Coordinates</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{03504c8d-c809-461c-a62e-55a4bd6c1adf}" type="10" valueInterpretation="0">
                        <ns:Name>User Comment</ns:Name>
                        <ns:Layout tableName="AssetRecords/{04a4080f-fec0-4e01-822f-b9125c0b5ac9}/{03504c8d-c809-461c-a62e-55a4bd6c1adf}">
                            <ns:Fields>
                                <ns:Field uid="{af4b2e46-5f6a-11d2-8f20-0000c0e166dc}" type="0" valueInterpretation="0">
                                    <ns:Name>Comments</ns:Name>
                                </ns:Field>
                                <ns:Field uid="{340f81ff-bef4-4967-b5eb-da1db00b73c6}" type="4" valueInterpretation="0">
                                    <ns:Name>Modification Date</ns:Name>
                                </ns:Field>
                                <ns:Field uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}" type="2" valueInterpretation="0">
                                    <ns:Name>Host Item ID</ns:Name>
                                </ns:Field>
                                <ns:Field uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}" type="2" valueInterpretation="0">
                                    <ns:Name>ID</ns:Name>
                                </ns:Field>
                                <ns:Field uid="{d332e5f9-980a-4685-bdd1-235635ac7f2f}" type="2" valueInterpretation="0">
                                    <ns:Name>Replied On</ns:Name>
                                </ns:Field>
                                <ns:Field uid="{da7fd65c-ea26-49f8-b7b8-a6871727d0bb}" type="0" valueInterpretation="10">
                                    <ns:Name>User</ns:Name>
                                </ns:Field>
                            </ns:Fields>
                        </ns:Layout>
                    </ns:Field>
                    <ns:Field uid="{228051ee-4983-462b-84e8-3644a14c0d52}" type="2" valueInterpretation="0">
                        <ns:Name>Page Number</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{d9bbc910-bae3-4862-97aa-b3ddb8bebb06}" type="4" valueInterpretation="0">
                        <ns:Name>Creation Date</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}" type="2" valueInterpretation="0">
                        <ns:Name>ID</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}" type="2" valueInterpretation="0">
                        <ns:Name>Host Item ID</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{da7fd65c-ea26-49f8-b7b8-a6871727d0bb}" type="0" valueInterpretation="10">
                        <ns:Name>User</ns:Name>
                    </ns:Field>
                    <ns:Field uid="{cf84d248-60b6-4fe1-a8f1-a0d91c307b0a}" type="7" valueInterpretation="0">
                        <ns:Name>User Comment Type</ns:Name>
                    </ns:Field>
                </ns:Fields>
            </ns:Layout>
        </ns:Field>
        <ns:Field uid="{af4b2e00-5f6a-11d2-8f20-0000c0e166dc}" type="0" valueInterpretation="0">
            <ns:Name>Record Name</ns:Name>
        </ns:Field>
        <ns:Field uid="{6060d669-a2ff-4284-af2e-4c6762139ea1}" type="7" valueInterpretation="7">
            <ns:Name>Test multi-select string list</ns:Name>
        </ns:Field>
        <ns:Field uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}" type="0" valueInterpretation="0">
            <ns:Name>Categories</ns:Name>
        </ns:Field>
    </ns:Fields>
</ns:Layout>
<ns:Items>
    <ns:Item catalogid="3" id="1">
        <ns:FieldValue uid="{00a3b951-4c7b-4751-90c0-c88cf0eb4983}">
            <ns:Items>
                <ns:Item catalogid="3" id="1">
                    <ns:FieldValue uid="{86b3bf92-e7cc-4150-bd74-191ce4bf9374}">table access number test</ns:FieldValue>
                    <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">1</ns:FieldValue>
                    <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">1</ns:FieldValue>
                    <ns:FieldValue uid="{8322d5e0-edc9-4c2a-9991-702c376d9edc}">table decription test</ns:FieldValue>
                    <ns:FieldValue uid="{5bbb974b-d50a-45de-bb40-f6d7fb36bfd8}" displayValue="12-Jan-1967">2049007148</ns:FieldValue>
                </ns:Item>
                <ns:Item catalogid="3" id="12">
                    <ns:FieldValue uid="{86b3bf92-e7cc-4150-bd74-191ce4bf9374}">789-123</ns:FieldValue>
                    <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">1</ns:FieldValue>
                    <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">12</ns:FieldValue>
                    <ns:FieldValue uid="{8322d5e0-edc9-4c2a-9991-702c376d9edc}">data entered using Web Client in IE7</ns:FieldValue>
                    <ns:FieldValue uid="{5bbb974b-d50a-45de-bb40-f6d7fb36bfd8}" displayValue="31-Oct-1977">2049012575</ns:FieldValue>
                </ns:Item>
            </ns:Items>
        </ns:FieldValue>
        <ns:FieldValue uid="{04a4080f-fec0-4e01-822f-b9125c0b5ac9}">
            <ns:Items>
                <ns:Item catalogid="3" id="6">
                    <ns:FieldValue uid="{dd48a259-8a55-46a6-b7cd-95a0335197e3}">7265636f00000008000000084154505944626c653fc6d5ef2c783013000000084154505844626c653fb3d4bb7e327a97000000084142505844626c653fd33ae45b57bcb2000000084142505944626c653fcd12073615a24000000004455450584c6f6e670000002800000004455450594c6f6e670000012600000004454250584c6f6e67000001ee00000004454250594c6f6e6700000122</ns:FieldValue>
                    <ns:FieldValue uid="{03504c8d-c809-461c-a62e-55a4bd6c1adf}">
                        <ns:Items>
                            <ns:Item catalogid="3" id="6">
                                <ns:FieldValue uid="{af4b2e46-5f6a-11d2-8f20-0000c0e166dc}">Crop this part</ns:FieldValue>
                                <ns:FieldValue uid="{340f81ff-bef4-4967-b5eb-da1db00b73c6}" displayValue="2011-04-25T20:03:51+08:00">1303733031000</ns:FieldValue>
                                <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">6</ns:FieldValue>
                                <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">6</ns:FieldValue>
                                <ns:FieldValue uid="{da7fd65c-ea26-49f8-b7b8-a6871727d0bb}">U:{98ca1422-0171-4d1d-8b84-8cdda4ab70ae}:testsuperadmin</ns:FieldValue>
                            </ns:Item>
                        </ns:Items>
                    </ns:FieldValue>
                    <ns:FieldValue uid="{d9bbc910-bae3-4862-97aa-b3ddb8bebb06}" displayValue="2011-04-25T20:03:40+08:00">1303733020000</ns:FieldValue>
                    <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">6</ns:FieldValue>
                    <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">1</ns:FieldValue>
                    <ns:FieldValue uid="{da7fd65c-ea26-49f8-b7b8-a6871727d0bb}">U:{98ca1422-0171-4d1d-8b84-8cdda4ab70ae}:testsuperadmin</ns:FieldValue>
                    <ns:FieldValue uid="{cf84d248-60b6-4fe1-a8f1-a0d91c307b0a}">
                        <ns:EnumValue id="1">Rectangle</ns:EnumValue>
                    </ns:FieldValue>
                </ns:Item>
                <ns:Item catalogid="3" id="7">
                    <ns:FieldValue uid="{dd48a259-8a55-46a6-b7cd-95a0335197e3}">7265636f00000006000000084154505944626c653fe1a70f9fd98a37000000084154505844626c653fdf6fc64f52edf900000004455450584c6f6e67000000be00000004455450594c6f6e670000024d00000004454250584c6f6e67000002f800000004454250594c6f6e67000000fc</ns:FieldValue>
                    <ns:FieldValue uid="{03504c8d-c809-461c-a62e-55a4bd6c1adf}">
                        <ns:Items>
                            <ns:Item catalogid="3" id="7">
                                <ns:FieldValue uid="{af4b2e46-5f6a-11d2-8f20-0000c0e166dc}">Remove this spot</ns:FieldValue>
                                <ns:FieldValue uid="{340f81ff-bef4-4967-b5eb-da1db00b73c6}" displayValue="2011-04-25T20:04:34+08:00">1303733074000</ns:FieldValue>
                                <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">7</ns:FieldValue>
                                <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">7</ns:FieldValue>
                                <ns:FieldValue uid="{da7fd65c-ea26-49f8-b7b8-a6871727d0bb}">U:{98ca1422-0171-4d1d-8b84-8cdda4ab70ae}:testsuperadmin</ns:FieldValue>
                            </ns:Item>
                        </ns:Items>
                    </ns:FieldValue>
                    <ns:FieldValue uid="{d9bbc910-bae3-4862-97aa-b3ddb8bebb06}" displayValue="2011-04-25T20:04:09+08:00">1303733049000</ns:FieldValue>
                    <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">7</ns:FieldValue>
                    <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">1</ns:FieldValue>
                    <ns:FieldValue uid="{da7fd65c-ea26-49f8-b7b8-a6871727d0bb}">U:{98ca1422-0171-4d1d-8b84-8cdda4ab70ae}:testsuperadmin</ns:FieldValue>
                    <ns:FieldValue uid="{cf84d248-60b6-4fe1-a8f1-a0d91c307b0a}">
                        <ns:EnumValue id="0">Icon</ns:EnumValue>
                    </ns:FieldValue>
                </ns:Item>
            </ns:Items>
        </ns:FieldValue>
        <ns:FieldValue uid="{af4b2e00-5f6a-11d2-8f20-0000c0e166dc}">890-ppm.jpg</ns:FieldValue>
        <ns:FieldValue uid="{6060d669-a2ff-4284-af2e-4c6762139ea1}">
            <ns:EnumValue id="0">Item A</ns:EnumValue>
            <ns:EnumValue id="1">Item B</ns:EnumValue>
            <ns:EnumValue id="3">Item D</ns:EnumValue>
        </ns:FieldValue>
        <ns:FieldValue uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}">
            <ns:CategoryValue>$Categories:Artifacts:Daily Life Item</ns:CategoryValue>
            <ns:CategoryValue>$Categories:Images:Objects</ns:CategoryValue>
        </ns:FieldValue>
    </ns:Item>
    <ns:Item catalogid="3" id="2">
        <ns:FieldValue uid="{00a3b951-4c7b-4751-90c0-c88cf0eb4983}">
            <ns:Items>
                <ns:Item catalogid="3" id="13">
                    <ns:FieldValue uid="{86b3bf92-e7cc-4150-bd74-191ce4bf9374}">12345-6789</ns:FieldValue>
                    <ns:FieldValue uid="{e709c047-7fca-4081-9c4b-41fb70bf31f4}">2</ns:FieldValue>
                    <ns:FieldValue uid="{c02adb31-5c2c-4014-b86a-a53cf83f7e6c}">13</ns:FieldValue>
                    <ns:FieldValue uid="{8322d5e0-edc9-4c2a-9991-702c376d9edc}">Test 2nd level description field</ns:FieldValue>
                    <ns:FieldValue uid="{5bbb974b-d50a-45de-bb40-f6d7fb36bfd8}" displayValue="31-Mar-2011">2049029759</ns:FieldValue>
                </ns:Item>
            </ns:Items>
        </ns:FieldValue>
        <ns:FieldValue uid="{af4b2e00-5f6a-11d2-8f20-0000c0e166dc}">2002-4-scrimshaw2001-128-10.jpg</ns:FieldValue>
        <ns:FieldValue uid="{6060d669-a2ff-4284-af2e-4c6762139ea1}">
            <ns:EnumValue id="4">Item E</ns:EnumValue>
            <ns:EnumValue id="0">Item A</ns:EnumValue>
            <ns:EnumValue id="2">Item C</ns:EnumValue>
        </ns:FieldValue>
        <ns:FieldValue uid="{af4b2e0c-5f6a-11d2-8f20-0000c0e166dc}">
            <ns:CategoryValue>$Categories:Artifacts:Personal Artefact</ns:CategoryValue>
            <ns:CategoryValue>$Categories:Artifacts:Daily Life Item</ns:CategoryValue>
            <ns:CategoryValue>$Categories:Images:Objects</ns:CategoryValue>
        </ns:FieldValue>
    </ns:Item>
</ns:Items>
</ns:Export>

There are three areas that I like to improve.

  1. Previously, only the Category field has multi values. Now, other field can have multi values too. In this example, the field name is Test multi-select string list

  2. Instead of breaking the multi values into different columns, insert separators between the values e.g. use | or some other characters that we can define

  3. As can be seen, there are two multi level fields: Test Table Field and User Comment Thread. Hope that the improved XSLT can handle this type of filed

Edit on 26 Apr 2011

  1. I have solved this successfully by duplicating the last 3 templates and renaming ns:CategoryValue to ns:EnumValue

  2. Your solution works. Thanks!

  3. Sorry that I did not explain clearly. I just need all the multi-level fields and data to be transformed like the rest of the fields. That is, one column for one field. I do not care about their nested relationship

Answer

Flynn1179 picture Flynn1179 · Feb 6, 2011

Try this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:ns="http://www.canto.com/ns/Export/1.0"
    xmlns:fn="http://www.w3.org/2005/xpath-functions">
  <xsl:output method="text" indent="yes" encoding="utf-8" standalone="yes"/>
  <xsl:strip-space elements="*"/>

  <!-- Parameterized separator/end of line characters for flexibility -->
  <xsl:param name="sep" select="'&#09;'" />
  <xsl:param name="eol" select="'&#10;'" />
  <xsl:param name="listsep" select="'|'" />

  <!-- On matching the root node, output a list of field names, followed by the items -->
  <xsl:template match="/ns:Export">
    <xsl:apply-templates select="ns:Layout/ns:Fields/ns:Field" />
    <xsl:apply-templates select="ns:Items/ns:Item"/>
  </xsl:template>

  <!-- On matching all but the last field name, output the name followed by separator -->
  <xsl:template match="ns:Field[position()!=last()]">
    <xsl:value-of select="concat(normalize-space(ns:Name),$sep)" />
  </xsl:template>

  <!-- On matching the last field name, output the name followed by a newline -->
  <xsl:template match="ns:Field[position()=last()]">
    <xsl:value-of select="concat(normalize-space(ns:Name),$eol)" />
  </xsl:template>

  <!-- On matching an item, iterate through each field, applying templates to any 'ns:FieldValue' nodes that share the same value of @uid -->
  <xsl:template match="ns:Item">
    <xsl:variable name="item" select="." />
    <xsl:for-each select="/ns:Export/ns:Layout/ns:Fields/ns:Field/@uid">
      <xsl:apply-templates select="$item/ns:FieldValue[@uid=current()]" />
      <xsl:if test="position()!=last()">
        <xsl:value-of select="$sep" />
      </xsl:if>
    </xsl:for-each>
    <xsl:value-of select="$eol" />
  </xsl:template>

  <!-- On matching a field value, output the content. -->
  <xsl:template match="ns:FieldValue">
    <xsl:value-of select="normalize-space(.)" />
  </xsl:template>

  <!-- on matching a field value with a @displayValue attribute, output the value of that attribute -->
  <xsl:template match="ns:FieldValue[@displayValue]">
    <xsl:value-of select="normalize-space(@displayValue)" />
  </xsl:template>

  <!-- On matching a field value with ns:CategoryValue children, apply templates on those children. -->
  <xsl:template match="ns:FieldValue[ns:CategoryValue]">
    <xsl:apply-templates select="ns:CategoryValue" />
  </xsl:template>

  <!-- On matching a category value, output it's content, plus a separator. -->
  <xsl:template match="ns:CategoryValue">
    <xsl:value-of select="concat(normalize-space(.),$listsep)" />
  </xsl:template>

  <!-- On matching the last category value, output it's content without a separator. -->
  <xsl:template match="ns:CategoryValue[position()=last()]">
    <xsl:value-of select="normalize-space(.)" />
  </xsl:template>
</xsl:stylesheet>

This probably isn't the most efficient way of using XSLT, but it's relatively simple. As you can probably see, I've embedded basic descriptions of the templates in comments. I'm not sure what you're using as a newline character (CR+LF or just LF?), but I've parameterised it, using LF at the moment. If you need to use CR+LF, change the <xsl:param name="eol" line to:

<xsl:param name="eol" select="'&#13;&#10;'" />

The ns:item template's a little tricky; let me know if you need an explanation of it.

EDIT: I've amended it very slightly to allow a different separator for a list of values in a single field, just adding a third parameter, and using it in the second last template in place of the existing $sep parameter.