user1820768
user1820768

Reputation:

XSLT Comma separating list, without grouping

I'm trying to convert the following XML document:

<method>
    <desc_signature>
        <desc_name>
            Some method name
        </desc_name>
    </desc_signature>
    <desc_content>
        <paragraph>Paragraph1</paragraph>
        <image>Image1</image>
        <paragraph>Paragraph2</paragraph>
        <literal_block>Codesnippet1</literal_block>
        <image>Image2</image>
        <paragraph>Paragraph3</paragraph>
        <image>Image3</image>
        <literal_block>Codesnippet2</literal_block>
    </desc_content>
</method>

To the following JSON format:

{
    "title":"Some method name",
    "elements":[
        "Paragraph1",
        "Paragraph2",
        "Codesnippet1",
        "Paragraph3",
        "Codesnippet2",
    ]
}

What I basically want to achieve, is to comma separate a list, but only include paragraph and literal_blocks, and still keeping the original order.

My first attempt was an XSLT that looks like this, with this union selector:

<xsl:for-each select="desc_content/paragraph|desc_content/literal_block">

Like this:

<!-- Method template -->
<xsl:template name="method">
    {
        "title":"<xsl:value-of select="normalize-space(desc_signature/desc_name)" />",
        "elements":[
            <xsl:for-each select="desc_content/paragraph|desc_content/literal_block">
                <xsl:choose>
                    <xsl:when test="self::paragraph">
                        <xsl:call-template name="paragraph"/>
                    </xsl:when>
                    <xsl:when test="self::literal_block">
                        <xsl:call-template name="code"/>
                    </xsl:when>
                </xsl:choose>
                <xsl:if test="position()!=last()">
                    <xsl:text>,</xsl:text>
                </xsl:if>
            </xsl:for-each>
        ]
    }
</xsl:template>

However, it seems like it is grouping paragraphs and then literal_blocks:

{
    "title":"Some method name",
    "elements":[
        "Paragraph1",
        "Paragraph2",
        "Paragraph3",
        "Codesnippet1",
        "Codesnippet2",
    ]
}

Another approach was to select all:

<xsl:for-each select="desc_content/*">

That, however, yields too many commas, as position() accounts for all elements, even if not used:

{
    "title":"Some method name",
    "elements":[
        "Paragraph1",,
        "Paragraph2",
        "Codesnippet1",,
        "Paragraph3",,
        "Codesnippet2",
    ]
}

How would I be able to achieve the desired behaviour?

Thanks for the help!

EDIT - XSLT 1.0 solution:

I've solved the issue with the solution from @Christian Mosz, using apply-templates and checking following-sibling:

<!-- Method template -->
<xsl:template name="method">
    {
        "title":"<xsl:value-of select="normalize-space(desc_signature/desc_name)" />",
        "elements":[
            <xsl:apply-templates select="desc_content/*"/>
        ]
    }
</xsl:template>

<!-- Paragraph template -->
<xsl:template match="paragraph">
{"paragraph":"<xsl:value-of select="normalize-space()"/>"}
<xsl:if test="following-sibling::paragraph|following-sibling::literal_block">,</xsl:if>
</xsl:template>

<!-- Code snippet template -->
<xsl:template match="literal_block">
{"code":"<xsl:call-template name="code"/>"}
<xsl:if test="following-sibling::paragraph|following-sibling::literal_block">,</xsl:if>
</xsl:template>

For XSLT 3.0, please check approved answer.

Upvotes: 1

Views: 52

Answers (1)

Martin Honnen
Martin Honnen

Reputation: 167506

Using XSLT 3 (e.g. with Saxon 9.8 or later or AltovaXML 2017 R3 or later) you have two options, you can either construct an XPath 3.1 map:

        map { 
              'title' : normalize-space(desc_signature/desc_name),
              'elements' : array { 
                 data(desc_content/(paragraph | literal_block))
              }
            }

i.e.

<?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"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:output method="json" indent="yes" />

  <xsl:template match="method">
      <xsl:sequence
        select="map { 
                  'title' : normalize-space(desc_signature/desc_name),
                  'elements' : array { 
                     data(desc_content/(paragraph | literal_block))
                  }
                }"/>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/pNmC4HJ

or you transform to the XML representation of JSON the xml-to-json functions supports:

<?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"
    expand-text="yes"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:output method="text" indent="yes" />

  <xsl:template match="/">
      <xsl:variable name="json-xml">
          <xsl:apply-templates/>
      </xsl:variable>
      <xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
  </xsl:template>

  <xsl:template match="method">
      <map>
          <string key="title">{normalize-space(desc_signature/desc_name)}</string>
          <array key="elements">
              <xsl:apply-templates select="desc_content/(paragraph | literal_block)"/>
          </array>
      </map>
  </xsl:template>

  <xsl:template match="desc_content/*">
      <string>{.}</string>
  </xsl:template>

</xsl:stylesheet>

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

Upvotes: 1

Related Questions