davidb
davidb

Reputation: 367

Concatenating the content of tags that share an attribute value with XSLT

As will become very apparent, XSLT is not my thing.

Hpwever I have some XHTML content, and I wish to concatenate the contents of some custom tags it contains, but only if they share a given attribute value.

So for example I would like to turn this:

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
   <head>
      <title />
   </head>
   <body>
      <p>
         <mytag myid="0">Don't </mytag>
         <mytag myid="1">concatenate. </mytag>
         Other text
         <mytag myid="2">Text </mytag>
         <mytag myid="2">to </mytag>
         <mytag myid="2">concatenate. </mytag>
         More text
      </p>
   </body>
</html>

into this:

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
   <head>
      <title />
   </head>
   <body>
      <p>
         <mytag myid="0">Don't </mytag>
         <mytag myid="1">concatenate. </mytag>
         Other text
         <mytag myid="2">Text to concatenate. </mytag>
         More text
      </p>
   </body>
</html>

Here's as far as I've got, with my XSLT:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<xsl:stylesheet xmlns:xhtml="http://www.w3.org/1999/xhtml" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                exclude-result-prefixes="xhtml"
                version="1.0">

<xsl:template match="*|@*">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<!-- Concatenate the contents of custom tags 'mytag'-->
<!-- that share an attribute myid value-->


<xsl:template match="//xhtml:mytag">

    <mytag>

        <xsl:variable name="this-id" select="@myid"/> 

        <xsl:attribute name="myid">
            <xsl:value-of select="$this-id"/>
        </xsl:attribute>

        <xsl:value-of select="."/>

        <xsl:for-each  select="following-sibling::xhtml:mytag[@myid=$this-id]">
            <xsl:value-of select="."/>
        </xsl:for-each>

    </mytag>

</xsl:template>

</xsl:stylesheet>

However try as I might, I can't work out how to stop the tags that have already been processed with the 'following-sibling' select being processed again. Consequently my output currently looks rather tragically like this:

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
   <head>
      <title/>
   </head>
   <body>
      <p>
         <mytag myid="0">Don't </mytag>
         <mytag myid="1">concatenate. </mytag>
         Other text
         <mytag myid="2">Text to concatenate. </mytag>
         <mytag myid="2">to concatenate. </mytag>
         <mytag myid="2">concatenate. </mytag>
         More text
      </p>
   </body>
</html>

If feels like this question, should answer my problem too - but I am too stupid to see it.

Help?

Upvotes: 0

Views: 438

Answers (2)

Tim C
Tim C

Reputation: 70648

You can tweak your current template to include an xsl:if that checks that there is not a preceding sibling with the same id, to prevent the mytag being output multiple times.

<xsl:stylesheet xmlns:xhtml="http://www.w3.org/1999/xhtml" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                exclude-result-prefixes="xhtml"
                version="1.0">

<xsl:strip-space elements="*" />

<xsl:template match="*|@*">
   <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
   </xsl:copy>
</xsl:template>

<xsl:template match="xhtml:mytag">
   <xsl:variable name="this-id" select="@myid"/> 
   <xsl:if test="not(preceding-sibling::xhtml:mytag[@myid = $this-id])">
      <xsl:copy>
         <xsl:attribute name="myid">
            <xsl:value-of select="$this-id"/>
         </xsl:attribute>
         <xsl:value-of select="."/>
         <xsl:for-each select="following-sibling::xhtml:mytag[@myid=$this-id]">
            <xsl:value-of select="."/>
         </xsl:for-each>
      </xsl:copy>
   </xsl:if>
</xsl:template>
</xsl:stylesheet>

However, depending on your requirements, this may "fail" if your XML looked like this

  <p>
     <mytag myid="0">Don't </mytag>
     <mytag myid="1">concatenate. </mytag>
     Other text
     <mytag myid="2">Text </mytag>
     <mytag myid="2">to </mytag>
     <mytag myid="2">concatenate. </mytag>
     More text
     <mytag myid="1">Concatenate separately. </mytag>
  </p>

The given XSLT would produce this...

<body xmlns="http://www.w3.org/1999/xhtml">
   <p>
      <mytag myid="0">Don't </mytag>
      <mytag myid="1">concatenate. Concatenate separately. </mytag>
         Other text
         <mytag myid="2">Text to concatenate. </mytag>
         More text
         </p>
</body>

This is because the following-sibling axis gets all following siblings, not just the one the immediately follows the current node.

If you actually wanted this instead....

<body xmlns="http://www.w3.org/1999/xhtml">
<p>
<mytag myid="0">Don't </mytag>
<mytag myid="1">concatenate. </mytag>
         Other text
         <mytag myid="2">Text to concatenate. </mytag>
         More text
         <mytag myid="1">Concatenate separately. </mytag>
</p>
</body>

Then you would have to look at the following siblings one at a time. Try this XSLT instead

<xsl:stylesheet xmlns:xhtml="http://www.w3.org/1999/xhtml" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                exclude-result-prefixes="xhtml"
                version="1.0">

<xsl:strip-space elements="*" />

<xsl:template match="*|@*">
   <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
   </xsl:copy>
</xsl:template>

<xsl:template match="xhtml:mytag">
   <xsl:variable name="this-id" select="@myid"/> 
   <xsl:if test="not(preceding-sibling::*[1][self::xhtml:mytag[@myid = $this-id]])">
      <xsl:copy>
         <xsl:attribute name="myid">
            <xsl:value-of select="$this-id"/>
         </xsl:attribute>
         <xsl:apply-templates select="self::*" mode="concatenate" />
      </xsl:copy>
   </xsl:if>
</xsl:template>

<xsl:template match="xhtml:mytag" mode="concatenate">
  <xsl:variable name="this-id" select="@myid"/> 
  <xsl:value-of select="." />
  <xsl:apply-templates select="following-sibling::*[1][self::xhtml:mytag[@myid = $this-id]]" mode="concatenate" />
</xsl:template>
</xsl:stylesheet>

Upvotes: 1

Michael Kay
Michael Kay

Reputation: 163625

This can be achieved very simply in XSLT 2.0 using

<xsl:template match="p">
  <xsl:for-each-group select="mytag" group-adjacent="@myid">
    <mytag myid="{current-grouping-key()}">
      <xsl:value-of select="string-join(current-group(), '')"/>
    </mytag>
  </xsl:for-each-group>
</xsl:template>

Upvotes: 0

Related Questions