1

I'm pretty new to XSLT and I've been struggling to replicate the solution mentioned here XSL for-each: how to detect last node? for longer than I'm willing to admit :(

I've setup this fiddle. https://xsltfiddle.liberty-development.net/naZXVFi

I was hoping I could use just the value-of + separator, vs choose / when xslt tools, as it did seem more idiomatic.

  • I can't get the separator to show up;
  • nor can I select just the child of skill, I always get the descendants too. That's to say, I shouldn't see any detail in the output.
  • bonus: not sure why that meta tag is not self closing (warning in the html section)

Desired output: skill1, skill2, skill3, skill4, skill5 (no comma space for the last one)

Any help would be greatly appreciated. Thanks.

EDIT: including the code here too: xml: (need to add ref to xslt):

<?xml version="1.0" encoding="utf-8" ?>
<?xml-stylesheet type="text/xsl" href="test.xsl"?> <!-- not in fiddle -->

<skills>
    <skill>skill1</skill>
    <skill>skill2</skill>
    <skill>skill3
        <details>
            <detail>detail1</detail>
            <detail>detail2</detail>
        </details>
    </skill>
    <skill>skill4</skill>
    <skill>skill5</skill>
</skills>

And test.xsl:

<?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"
    xmlns:map="http://www.w3.org/2005/xpath-functions/map"
    xmlns:array="http://www.w3.org/2005/xpath-functions/array"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:mode on-no-match="shallow-copy"/>

  <xsl:output method="html" indent="yes" html-version="5"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>.NET XSLT Fiddle Example</title>
      </head>
      <body>
         <xsl:for-each select="/skills/skill">
            <xsl:value-of select="." separator=", "/>
        </xsl:for-each>
      </body>
    </html>


  </xsl:template>

</xsl:stylesheet>
ILoveCoding
  • 866
  • 1
  • 9
  • 18

2 Answers2

1

In general, with XSLT 2/3 to output a sequence separated by some separator string, you simply use xsl:value-of select="$sequence" with the appropriate separator string in the separator attribute (and no for-each):

  <xsl:template match="skills">
    <xsl:value-of select="skill/text()[normalize-space()]/normalize-space()" separator=", "/>
  </xsl:template>

https://xsltfiddle.liberty-development.net/naZXVFi/1

In most cases you would just need select="skill" separator=", " but given your descendants and the white space you seem to want to eliminate the select expression above is a bit more complicated.

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
  • 1
    Thanks. It works in the fiddle, but I can't get it to transpose to my setup for now... Any pointers regarding what `text()[normalize-space()]/normalize-space()` does please? XSLT feels like black magic to me at this point... After just removing the spaces in the xml and changing the core xsl to , I loose the separator again ( results in skill1skill2...) – ILoveCoding May 25 '20 at 19:04
  • 1
    The path `skill` selects all `skill` child elements, the path `skill/text()` all text child nodes of all `skill` child elements. In square brackets you put predicates/conditions to filter out nodes `text()[normalize-space()]` filters out the text nodes where the normalize-space call returns a non-empty string. A last step in XPath 2.0 or later can call functions returning atomic values like strings and not simply select nodes, so the last step `/normalize-space()` returns the result of calling that function on each text node so that we get a string with leading and trailing whitespace stripped – Martin Honnen May 25 '20 at 19:35
  • 1
    Consider reading an XPath tutorial like https://www.altova.com/training/xpath3 if such simple path expressions are not known to you. – Martin Honnen May 25 '20 at 19:41
  • Great explanation, thanks. Any idea why I'm loosing the separator in the case I menitoned? (I'm surprised that I manage to get all the values but the separator doesn't make it...) – ILoveCoding May 25 '20 at 21:08
  • 1
    The spec for https://www.w3.org/TR/xslt-30/#element-value-of says "If the separator attribute is present, then the effective value of this attribute is used to separate adjacent items in the result sequence, as described in 5.7.2 Constructing Simple Content.". That says in https://www.w3.org/TR/xslt-30/#constructing-simple-content that "Adjacent text nodes in the sequence are merged into a single text node.". So that is the reason for the result you see, you could use `select="skill/text()/string()"` to prevent the merge and have the separator applied. – Martin Honnen May 25 '20 at 21:40
1

Martin has given you the detailed work-through to get the final result including getting rid of the extra spaces etc, but at a high level, here's how to use xsl:value-of with separator correctly.

You have:

  <body>
     <xsl:for-each select="/skills/skill">
        <xsl:value-of select="." separator=", "/>
    </xsl:for-each>
  </body>

This says that for each skill node, take the content of that node and display it. Notably, the value-of only sees one skill at a time, so there is nothing to join with the comma separator.

The answer which would get you what you want is:

  <body>
        <xsl:value-of select="/skills/skill" separator=", "/>
  </body>

This says to take the set of skill nodes and display them joined by comma separators. You can see the output at https://xsltfiddle.liberty-development.net/naZXVFi/4

Stobor
  • 44,246
  • 6
  • 66
  • 69