I'm trying to convert some existing XML files to a JSON structure using XSL. Depending on their values, I need to put some elements into an JSON Array. To Produce a JSON String with correct syntax, I need to dynamically decide whether to put a comma behind (or in front) an object, since JSON doesn't allow trailing commas in arrays.
The XML files look somewhat like the following:
<list>
<type_a>
<active>0</active>
<data_a>don't put me to JSON...</data_a>
</type_a>
<type_b>
<active>1</active>
<data_b>put me to JSON</data_b>
</type_b>
<type_c>
<active>1</active>
<data_c>me too...</data_c>
</type_c>
</list>
The XSL to convert this looks like the following:
<xsl:template match="/list">
[
<xsl:if test="type_a/active != 0">
{ "type": "type_a",
"data": <xsl:value-of select="type_a/data_a" /> }
</xsl:if>
<xsl:if test="type_b/active != 0">
{ "type": "type_b",
"data": <xsl:value-of select="type_b/data_b" /> }
</xsl:if>
<xsl:if test="type_c/active != 0">
{ "type": "type_c",
"data": <xsl:value-of select="type_c/data_c" /> }
</xsl:if>
]
</xsl:template>
The Problem is, that I need to put commas between the different { } objects, but not after the last active one. The only two solutions I see are, either to check if any of the preceding objects where active, before putting an object (<xsl-if test="type_a/active != 0 or type_b/active != 0">, </xsl-if>; in front of the third object) or to transfer the XML into some, less odd, intermediate XML first. Particular the first option would be extremely ugly, since in reality I have to check for far more than 3 object types. The XML Format is produced by some legacy application and can't be changed.
Should I expect any further trouble because of using XSL to transfer the XML Structure to some none XML output?
One way is to check the preceding sibling to see if it is exists and is active and if so insert a ,. I have edited your template to reflect this, but I would write a function to just pass the node to, to make you life easier instead of repeating the template for each sibling.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:func="http://example.com/function"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output encoding="UTF-8" indent="yes" method="xml"/>
<xsl:template match="/list">
[
<xsl:sequence select="func:processing(type_a)"/>
<xsl:sequence select="func:processing(type_b)"/>
<xsl:sequence select="func:processing(type_c)"/>
]
</xsl:template>
<xsl:function name="func:processing">
<xsl:param name="type"/>
<xsl:for-each select="$type">
<xsl:if test="active != 0">
{ "type": <xsl:value-of select="local-name()" />,
"data": <xsl:value-of select="data_a" /> }
<xsl:if test="following-sibling::*[1] and following-sibling::*[1]/active!=0">
,
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:function>
</xsl:stylesheet>
Related
I am trying to convert XML to json using XSLT 3.0, lowercasing all keys and moving the first attribute, if any, as a JSON child. So given following (dummy) input XML:
<FOO id="1">
<BAR xy="2">
<SPAM>N</SPAM>
</BAR>
</FOO>
I am expecting
{
"foo" : {
"id" : "1",
"bar" : {
"xy" : "2",
"spam" : "N"
}
}
}
Using this XSLT:
<?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"
xmlns="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:output method="xml" indent='true' omit-xml-declaration='yes'/>
<xsl:template match="dummy">
<xsl:variable name="json-xml">
<xsl:apply-templates/>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
<!--no children-->
<xsl:template match="*[not(*)]">
<string key="{lower-case(local-name())}">{.}</string>
</xsl:template>
<xsl:template match="*[*]">
<xsl:param name="key" as="xs:boolean" select="true()"/>
<map>
<xsl:if test="$key">
<xsl:attribute name="key" select="lower-case(local-name())"/>
</xsl:if>
<xsl:for-each-group select="*" group-by="node-name()">
<xsl:choose>
<xsl:when test="current-group()[2]">
<array key="{lower-case(local-name())}">
<xsl:apply-templates select="current-group()">
<xsl:with-param name="key" select="false()"/>
</xsl:apply-templates>
</array>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()">
<xsl:with-param name="key" select="true()"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</map>
</xsl:template>
</xsl:stylesheet>
I get (note the dummy pattern to skip json conversion),
<map xmlns="http://www.w3.org/2005/xpath-functions" key="foo">
<map key="bar">
<string key="spam">N</string>
</map>
</map>
Looks good to me but when I invoke the JSON conversion (by replacing dummy with /), I get:
{ "bar" :
{ "spam" : "N" } }
-> the foo node is gone.
I haven't figured out how to "move" the first attribute (arbitrary, could have any name) as a child node - if someone knows, appreciate a little snippet.
Lastly, not a big-deal but I am lowercasing keys in each template. Is it possible to do the transformation at once, either before in the source XML, or after templating, in the Result XML (before jsonifying) ?
See - https://xsltfiddle.liberty-development.net/jyfAiDC/2
(thanks btw to #Martin Honnen for this very useful tool !!)
You can use
<xsl:template match="/">
<xsl:variable name="json-xml">
<map>
<xsl:apply-templates/>
</map>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
perhaps to get closer to what you might want. But I haven't understood what you want to do with attributes in the XML.
The key attribute only make sense for an element that represents an entry in a map, so there's no point including it in an element unless that element has a parent named map. You want another layer of map in your structure.
Thanks to Martin & Michael for the map wrapper tip. Agreed though it is an unnecessary level in tree.
For rendering a XML attribute as a child node - assuming there is one only -,
I added the following after the first test (conditional map attribute) in the template:
<xsl:if test="#*[1]">
<string key="{name(#*[1])}">{#*[1]}</string>
</xsl:if>
Lastly, for converting to lowercase all intermediary key attributes in one-go instead of individually in multiple templates, it would require I think parsing the result tree before passing on to the xml-to-json function.
Not worth it... but it would be a nice featured option in XSLT 4.0 (?) i.e. a new xml-to-json option force-key-case = lower/upper
I would like to use XSLT to transform some XML into JSON.
The XML looks like the following:
<DATA_DS>
<G_1>
<ORGANIZATION_NAME>My Company 1</ORGANIZATION_NAME>
<ORGANIZATIONID>901</ORGANIZATIONID>
<ITEMNUMBER>20001</ITEMNUMBER>
<ITEMDESCRIPTION>Item Description 1</ITEMDESCRIPTION>
</G_1>
<G_1>
<ORGANIZATION_NAME>My Company 1</ORGANIZATION_NAME>
<ORGANIZATIONID>901</ORGANIZATIONID>
<ITEMNUMBER>20002</ITEMNUMBER>
<ITEMDESCRIPTION>Item Description 2</ITEMDESCRIPTION>
</G_1>
<G_1>
<ORGANIZATION_NAME>My Company 1</ORGANIZATION_NAME>
<ORGANIZATIONID>901</ORGANIZATIONID>
<ITEMNUMBER>20003</ITEMNUMBER>
<ITEMDESCRIPTION>Item Description 3</ITEMDESCRIPTION>
</G_1>
</DATA_DS>
I expect the JSON to look like the following:
[
{
"Item_Number":"20001",
"Item_Description":"Item Description 1"
},
{
"Item_Number":"20002",
"Item_Description":"Item Description 2"
},
{
"Item_Number":"20003",
"Item_Description":"Item Description 3"
}
]
What is the recommended way to do this?
I am considering two approaches:
Try using the fn:xml-to-json function, as defined at https://www.w3.org/TR/xpath-functions-31/#func-xml-to-json. But as I understand, the input XML must follow a specific format defined at: https://www.w3.org/TR/xpath-functions-31/schema-for-json.xsd. And I also need the field names in the output JSON to be specifically "Item_Number" and "Item_Description".
Manually code the bracket and brace characters, "[", "]", "{", and "}", along with the field names "Item_Number" and "Item_Description". Then use a standard function to list the values and ensure that any special characters are handled properly. For example, the "&" character should appear normally in the JSON output.
What is the recommended way to do this, or is there a better way that I have not considered?
I would take the first approach, but start with transforming the given input to the XML format expected by the xml-to-json() function. This could be something like:
XSLT 3.0
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2005/xpath-functions">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/G_1">
<!-- CONVERT INPUT TO XML FOR JSON -->
<xsl:variable name="xml">
<array>
<xsl:for-each-group select="*" group-starting-with="ORGANIZATION_NAME">
<map>
<string key="Item_Number">
<xsl:value-of select="current-group()[self::ITEMNUMBER]"/>
</string>
<string key="Item_Description">
<xsl:value-of select="current-group()[self::ITEMDESCRIPTION]"/>
</string>
</map>
</xsl:for-each-group>
</array>
</xsl:variable>
<!-- OUTPUT -->
<xsl:value-of select="xml-to-json($xml)"/>
</xsl:template>
</xsl:stylesheet>
Demo: https://xsltfiddle.liberty-development.net/bFWR5DQ
For simple mappings like that you can also directly construct XPath 3.1 arrays and maps i.e. in this case an array of maps:
<xsl:template match="DATA_DS">
<xsl:sequence select="array { G_1 ! map { 'Item_Number' : string(ITEMNUMBER), 'Item_Description' : string(ITEMDESCRIPTION) } }"/>
</xsl:template>
Then serialize as JSON with <xsl:output method="json" indent="yes"/>: https://xsltfiddle.liberty-development.net/ejivdGS
The main disadvantage is that maps have no order so you can't control the order of the items in a map, for instance for that example and the used Saxon version Item_Description is output before Item_Number.
But in general transforming to the format for xml-to-json provides more flexibility and also allows you to control the order as the function preserves the order in the XML representation of JSON.
This is the result of taking the solution posted by michael.hor257k and applying it to my revised input XML:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2005/xpath-functions">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/DATA_DS">
<!-- CONVERT INPUT TO XML FOR JSON -->
<xsl:variable name="xml">
<array>
<xsl:for-each select="G_1">
<map>
<string key="Item_Number">
<xsl:value-of select="ITEMNUMBER"/>
</string>
<string key="Item_Description">
<xsl:value-of select="ITEMDESCRIPTION"/>
</string>
</map>
</xsl:for-each>
</array>
</xsl:variable>
<!-- OUTPUT -->
<xsl:value-of select="xml-to-json($xml)"/>
</xsl:template>
</xsl:stylesheet>
I am processing various XML files with XSLT. In one XML I found a wrapped JSON list:
<list><![CDATA[[
{
"title": "Title 1",
"value": "Value 1",
"order": 1
},
{
"title": "Title 2",
"value": "Value 2",
"order": 2
}
]]]>
</list>
My problem is that I need to iterate over the list. For example:
<xsl:variable name="listVar">
<!-- do something with list -->
</xsl:variable>
<xsl:for-each select="$listVar">
<!-- do something with objects in list e.g. -->
<xsl:value-of select="title"/>
<xsl:value-of select="value"/>
</xsl:for-each>
How to do this with XSLT? I use XSLT 3.0 and Saxon engine, version 9.8 HE.
Considered solutions:
1.
Use parse-json function:
But then I cannot iterate over the result because of XPathException: "Required item type of the context item for the child axis is node(); supplied value (.) has item type array(function(*))" or "Maps cannot be atomized". I found that there are
functions that probably I should take into account like map:get, map:entry, but I've failed to use them in my case so far.
2.
Addidiotnal transform before the one mentioned above:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">
<xsl:output method="xml" encoding="UTF-8" indent="no"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="list">
<list>
<xsl:copy-of select="json-to-xml(.)"/>
</list>
</xsl:template>
</xsl:stylesheet>
And then:
<xsl:variable name="listVar" select="list/array/map"/>
But it does not work - probably due to added namespace
<list>
<array xmlns="http://www.w3.org/2005/xpath-functions">
<map>
...
Your JSON structure when parsed with parse-json gives you on the XSLT/XPath side an array of maps and the most straightforward way to process the individual array items is with the ?* lookup operator, then you can use for-each or even apply-templates:
<?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"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
exclude-result-prefixes="xs math"
version="3.0">
<xsl:template match="list">
<xsl:apply-templates select="parse-json(.)?*"/>
</xsl:template>
<xsl:template match=".[. instance of map(xs:string, item())]">
<xsl:value-of select="?title, ?value"/>
</xsl:template>
</xsl:stylesheet>
where to access the map values you can again use ?foo as shown above.
As for working with the XML returned by json-to-xml, it returns elements in the XPath function namespace so to select them, as with any other elements in a namespace you need to make sure you set up a namespace with e.g. xpath-default-namespace for the section you want to process elements from that namespace or you can use the namespace wild card e.g. *:array/*:map.
I'm having the "text" tag for separate parent tag, in that some times for that text tag content will be empty, that time i want that tag with separate output and content element "text" needs to be in separate tags:
My Input XML is:
<introText>
<text/>
</introText>
<directionText>
<text>CLICK ON EACH CATEGORY TO GET STARTED, AND WHEN YOU ARE FINISHED, EVALUATE YOUR LISTS. WHICH LIST IS LONGER?</text>
</directionText>
XSL I used as:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:json="http://json.org/" xmlns:mf="http://example.com/mf" exclude-result-prefixes="#all">
<xsl:template match="introText">
"introText": {
<xsl:apply-templates/>
},
</xsl:template>
<xsl:template match="directionText">
"directionText": {
<xsl:apply-templates/>
}
</xsl:template>
<xsl:param name="length" as="xs:integer" select="100"/>
<xsl:param name="pattern" as="xs:string" select="concat('((.{1,', $length, '})( |$))')"/>
<xsl:param name="sep" as="xs:string" select="' +
'"/>
<xsl:function name="mf:break" as="xs:string">
<xsl:param name="input" as="xs:string"/>
<xsl:variable name="result">
<xsl:analyze-string select="$input" regex="{$pattern}">
<xsl:matching-substring>
<xsl:value-of select="concat('"', regex-group(2), ' ', '"')"/>
<xsl:if test="position() ne last()">
<xsl:value-of select="$sep"/>
</xsl:if>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:variable>
<xsl:sequence select="$result"/>
</xsl:function>
<xsl:template match="text">
"text": <xsl:sequence select="mf:break(normalize-space(string-join(node()/serialize(., $ser-params), '')))"/>,
</xsl:template>
</xsl:stylesheet>
My getting the output as:
introText: {
"text": ""
},
directionText: {
"text": "CLICK ON EACH CATEGORY TO GET STARTED, AND WHEN YOU ARE" +
"FINISHED, EVALUATE YOUR LISTS. WHICH LIST IS LONGER?",
}
But i want the empty element "text" have to be like below output file
Expected Output file:
introText: {
"text":' "" '
},
directionText: {
"text": "CLICK ON EACH CATEGORY TO GET STARTED, AND WHEN YOU ARE" +
"FINISHED, EVALUATE YOUR LISTS. WHICH LIST IS LONGER?",
}
The extra single quote need to cover that double quotes if that text is empty, Likewise lot of "text" tag coming in article, So i want to write the XSL for "text" template alone. Thanks in advance
If you want to handle an empty text element separately, simply add a new template to match such an element, like so:
<xsl:template match="text[not(normalize-space())]">
"text": ' "" ',
</xsl:template>
The normalize-space() will mean it will also pick up text nodes that contain nothing but whitespace.
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 9 months ago.
Improve this question
TL;DR - Problem: In a nested for-each I am unable to get commas to print correctly. There are four cases which keep repeating. There are other ways that I can think of to get the job done, but I really want to find out why this is behaving the way it is.
Question: Why are pipes working, but not commas and why does the last element print out by itself sometimes?
Overview: I am trying to get an XML document, which contains node relationships, into a JSON format so that I can do graph visualization. There are a couple of ways to solve this problem and I chose the start by generating the list of nodes. So, I have an XSL file which is transforming an XML document into a JSON tree structure of nodes.
Here is a snippet of the XML file:
<?xml version="1.0" ?>
<knowledge_base
xmlns="http://protege.stanford.edu/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://protege.stanford.edu/xml http://www.enterprise-architecture.org/xml/essentialreportxml.xsd">
<simple_instance>
<name>Class181</name>
<type>Business_Capability</type>
<own_slot_value>
<slot_reference>business_capability_level</slot_reference>
<value value_type="string">3</value>
</own_slot_value>
<own_slot_value>
<slot_reference>realised_by_business_processes</slot_reference>
<value value_type="simple_instance">Class40041</value>
</own_slot_value>
<own_slot_value>
<slot_reference>supports_business_capabilities</slot_reference>
<value value_type="simple_instance">Class180</value>
<value value_type="simple_instance">Class20022</value>
<value value_type="simple_instance">Class182</value>
</own_slot_value>
<own_slot_value>
<slot_reference>name</slot_reference>
<value value_type="string">Help Desk Service</value>
</own_slot_value>
</simple_instance>
</knowledge_base>
There are multiple Business Capabilities and they can have Business capabilities as parents and others as children. Here is the bulk of my XSL doc:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xpath-default-namespace="http://protege.stanford.edu/xml" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt" xmlns:pro="http://protege.stanford.edu/xml" xmlns:eas="http://www.enterprise-architecture.org/essential" xmlns:functx="http://www.functx.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ess="http://www.enterprise-architecture.org/essential/errorview">
<xsl:template match="knowledge_base">
<xsl:call-template name="BuildPage"/>
</xsl:template>
<xsl:template name="BuildPage">
<xsl:param name="pageLabel">Business Capability Graph</xsl:param>
<html>
<head>
<title><xsl:value-of select="$pageLabel"></xsl:value-of></title>
<script src="js/d3.v3.min.js"></script>
<script type="text/javascript">
<xsl:text>
function tree()
{
var json=</xsl:text><xsl:call-template name="getJSON" /><xsl:text>;
console.log(JSON.stringify(json));
}
</xsl:text>
</script>
</head>
<body onload="tree();">
<div id="main"></div>
</body>
</html>
</xsl:template>
<xsl:template name="getJSON">
<xsl:text>{ "nodes" : [</xsl:text>
<!-- Get Business Caps -->
<xsl:call-template name="getNodes">
<xsl:with-param name="nodes" select="/node()/simple_instance[type='Business_Capability']" />
</xsl:call-template>
<xsl:text>]}</xsl:text>
</xsl:template>
<xsl:template name="getNodes">
<xsl:param name="nodes" />
<xsl:for-each select="$nodes">
<xsl:variable name="name" select="own_slot_value[slot_reference='name']/value" />
<xsl:variable name="id" select="current()/name" />
<xsl:variable name="level" select="own_slot_value[slot_reference='business_capability_level']/value" />
<xsl:text>{"name":"</xsl:text><xsl:value-of select="$name" />
<xsl:text>", "id":"</xsl:text><xsl:value-of select="$id" />
<xsl:text>", "level":"</xsl:text><xsl:value-of select="$level" />
<xsl:text>", "data":{</xsl:text>
<xsl:variable name="pcCap_list" select="own_slot_value[slot_reference='supports_business_capabilities']" />
<xsl:text>"pcCap":{</xsl:text>
<xsl:for-each select="/node()/simple_instance[name=$pcCap_list/value]" >
<xsl:variable name="pos" select="position()" />
<xsl:text>"id" : "</xsl:text>
<xsl:value-of select="name" />
<xsl:text> - pos </xsl:text><xsl:value-of select="$pos" />
<xsl:text> - count </xsl:text><xsl:value-of select="count($pcCap_list/value)" />
<xsl:text>"</xsl:text>
</xsl:for-each>
<xsl:text>}}}</xsl:text>
<xsl:if test="position() != count($nodes)">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Throughout the document, I have successfully used the xsl:if test=position() != count() to properly add commas (1, 2, 3), EXCEPT for within the inner for-each in the getNodes template. I have tried many different variations, from using position(), count(), last(), !=, <, >, =, not(position() = count()), count()-1, count()+1, etc., along with xsl:choose with xsl:when and xsl:otherwise statements. I keep seeing the same four patterns below (along with XSL syntax errors, and empty objects).
Here is what I have tried. All were placed After the <xsl:text>"</xsl:text> and </xsl:for-each> of the inner for-each in the getNodes template. First is the XSL input, my summary of what happened, & the related JSON output.
Test 1:
<xsl:if test="position() != count($pcCap_list/value)" >
<xsl:text>, </xsl:text>
</xsl:if>
No XSL error. No JS error. But only **last element** is printed.
{
"name": "Help Desk Service",
"id": "Class181",
"level": "3",
"data": {
"pcCap": {
"id": "Class20022 - pos 3 - count 3"
}
}
}
Test 2:
<xsl:if test="position() = count($pcCap_list/value)" >
<xsl:text>, </xsl:text>
</xsl:if>
No XSL error. JS errors = "Uncaught SyntaxError: Unexpected string report:8" && "Uncaught ReferenceError: tree is not defined report:125". All three are printed as expected with the comma after the third element.
{
"name": "Help Desk Service",
"id": "Class181",
"level": "3",
"data": {
"pcCap": {
"id": "Class180 - pos 1 - count 3""id": "Class182 - pos 2 - count 3""id": "Class20022 - pos 3 - count 3",
}
}
}
Test 3:
<xsl:if test="position() < count($pcCap_list/value)" >
<xsl:text>, </xsl:text>
</xsl:if>
No XSL error. No JS error. Again, only last item is shown.
{
"name": "Help Desk Service",
"id": "Class181",
"level": "3",
"data": {
"pcCap": {
"id": "Class20022 - pos 3 - count 3"
}
}
}
Test 4:
<xsl:if test="position() < count($pcCap_list/value)" > <!-- happens with both < and != -->
<xsl:text>| </xsl:text>
</xsl:if>
No XSL error. JS errors = "Uncaught SyntaxError: Unexpected token : report:8" && "Uncaught ReferenceError: tree is not defined report:125". Prints out as expected, with pipes between the elements.
{
"name": "Help Desk Service",
"id": "Class181",
"level": "3",
"data": {
"pcCap": {
"id": "Class180 - pos 1 - count 3"|"id": "Class182 - pos 2 - count 3"|"id": "Class20022 - pos 3 - count 3"
}
}
}
So what is going on that causes pipes to work but not commas? And why does the third element sometimes print by itself?
Your originally posted transform works on my XSL transformer. I think your problem may be with the transformer itself. I just copied and pasted your XML and XSL and got the desired output from XSLT.