0

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

McTrafik
  • 2,843
  • 3
  • 29
  • 31

2 Answers2

1

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>
Tim C
  • 70,053
  • 14
  • 74
  • 93
0

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>
Valdi_Bo
  • 30,023
  • 4
  • 23
  • 41
  • `*[not(preceding-sibling::* = .)]` is not a good method to remove duplicates. Use [Muenchian grouping](http://www.jenitennison.com/xslt/grouping/muenchian.html) instead. – michael.hor257k Mar 18 '17 at 14:49
  • Muenchian grouping involves generating a key at the top level of XSLT. This key must be generated based on elements, that **already exist** in the source XML, specified by *match* clause. Here it is impossible, because the *class* tokens (individual words) are obtained from a loop and they are held in a variable. Or maybe there is a possibility to generate a key **from a variable**? – Valdi_Bo Mar 19 '17 at 10:14
  • Keys operate in the context of current document. A variable created by the `node-set()` function is a document - all you have to do is switch the context to it before using the key. – michael.hor257k Mar 19 '17 at 12:40