McTrafik
McTrafik

Reputation: 2938

Combine space-separated attributes in XSLT

The transform I'm working on mergers two templates that has attributes that are space-separated.

An example would be:

<document template_id="1">
  <header class="class1 class2" />
</document>
<document template_id="2">
  <header class="class3 class4" />
</document>

And after the transform I want it to be like this:

<document>
  <header class="class1 class2 class3 class4" />
</document>

How to achieve this?

I have tried (writing from memory):

<xsl:template match="/">
  <header>
    <xsl:attribute name="class">
      <xsl:for-each select=".//header">
        <xsl:value-of select="@class"/>
      </xsl:for-each>
    </xsl:attribute>
  </header>
</xsl:template>

But that appends them all together, but I need them separated... and would be awesome if uniqued as well.

Thank you

Upvotes: 0

Views: 166

Answers (2)

Valdi_Bo
Valdi_Bo

Reputation: 30991

If you want your classes without repetitions, then use the following XSLT:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
  extension-element-prefixes="exsl">
  <xsl:output method="xml" encoding="UTF-8" indent="yes" />

  <xsl:template match="/">
    <document>
      <xsl:variable name="cls1">
        <xsl:for-each select=".//header/@class">
          <xsl:call-template name="tokenize">
            <xsl:with-param name="txt" select="."/>
          </xsl:call-template>  
        </xsl:for-each>
      </xsl:variable>
      <xsl:variable name="cls" select="exsl:node-set($cls1)"/>
      <header>
        <xsl:attribute name="class">
          <xsl:for-each select="$cls/*[not(preceding-sibling::* = .)]">
            <xsl:value-of select="."/>
            <xsl:if test="position() &lt; last()">
              <xsl:text> </xsl:text>
            </xsl:if>
          </xsl:for-each>
        </xsl:attribute>
      </header>
    </document>
  </xsl:template>

  <xsl:template name="tokenize">
    <xsl:param name="txt"/>
    <xsl:if test="$txt">
      <xsl:if test="contains($txt, ' ')">
        <cls>
          <xsl:value-of select="substring-before($txt, ' ')"/>
        </cls>
        <xsl:call-template name="tokenize">
          <xsl:with-param name="txt" select="substring-after($txt, ' ')"/>
        </xsl:call-template>
      </xsl:if>
      <xsl:if test="not(contains($txt, ' '))">
        <cls>
          <xsl:value-of select="$txt"/>
        </cls>
      </xsl:if>
    </xsl:if>
  </xsl:template>
</xsl:transform>

In XSLT 1.0 it is much more difficult than in XSLT 2.0, but as you see, it is possible.

Edit

A revised version using Muenchian grouping (inspired by comment from Michael):

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
  extension-element-prefixes="exsl">
  <xsl:output method="xml" encoding="UTF-8" indent="yes" />

  <xsl:variable name="cls1">
    <xsl:for-each select=".//header/@class">
      <xsl:call-template name="tokenize">
        <xsl:with-param name="txt" select="."/>
      </xsl:call-template>
    </xsl:for-each>
  </xsl:variable>
  <xsl:variable name="cls2" select="exsl:node-set($cls1)"/>
  <xsl:key name="classKey" match="cls" use="."/>

  <xsl:template match="/">
    <document>
      <header>
        <xsl:attribute name="class">
          <xsl:for-each select="$cls2/*[generate-id() = generate-id(key('classKey', .)[1])]">
            <xsl:value-of select="."/>
            <xsl:if test="position() &lt; last()">
              <xsl:text> </xsl:text>
            </xsl:if>
          </xsl:for-each>
        </xsl:attribute>
      </header>
    </document>
  </xsl:template>

  <xsl:template name="tokenize">
    <xsl:param name="txt"/>
    <xsl:if test="$txt">
      <xsl:if test="contains($txt, ' ')">
        <cls>
          <xsl:value-of select="substring-before($txt, ' ')"/>
        </cls>
        <xsl:call-template name="tokenize">
          <xsl:with-param name="txt" select="substring-after($txt, ' ')"/>
        </xsl:call-template>
      </xsl:if>
      <xsl:if test="not(contains($txt, ' '))">
        <cls>
          <xsl:value-of select="$txt"/>
        </cls>
      </xsl:if>
    </xsl:if>
  </xsl:template>
</xsl:transform>

Upvotes: 0

Tim C
Tim C

Reputation: 70618

Try this template, which simply adds a space before all the headers, apart from the first

<xsl:template match="/">
  <header>
    <xsl:attribute name="class">
      <xsl:for-each select=".//header">
        <xsl:if test="position() > 1">
            <xsl:text> </xsl:text>
        </xsl:if>
        <xsl:value-of select="@class"/>
      </xsl:for-each>
    </xsl:attribute>
  </header>
</xsl:template>

Upvotes: 1

Related Questions