morsor
morsor

Reputation: 1323

XSL: 'Moving' following-siblings without duplicating

Given the XML:

<root>
    <txto>Header text<txto>
    <txt>more paragraph text</txt>
    <txt>even more paragraph text</txt>
    <txto>Another header<txto>
    <txt> … </txt>
    <txt> … </txt>
</root>

The desired output is having the first header define a section, while preserving the second:

<root>
    <n1>
        <o>Header text</o>
        <txt>more paragraph text</txt>
        <txt>even more paragraph text</txt>
    </n1>
    <txto>Another header<txto>
    <txt> … </txt>
    <txt> … </txt>
</root>

Using the XSL:

<xsl:template match=“//txto[starts-with(., ‘Header’)]”>
        
    <n1>
        <o><xsl:value-of select="."/></o>
        <xsl:apply-templates select="following-sibling::txt" />
    
    </n1>
    
</xsl:template>
    
    

However, the result is 'duplication' of the txt elements:

<root>
    <n1>
        <o>Header text</o>
        <txt>more paragraph text</txt>
        <txt>even more paragraph text</txt>
    </n1>
    <txt>more paragraph text</txt>       <-- 'duplicated'
    <txt>even more paragraph text</txt>  <-- 'duplicated'
    <txto>Another header<txto>
    <txt> … </txt>
    <txt> … </txt>
</root>

Which probably has to do with the Identity transform already having copied all txt nodes to the result tree:

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

How do I avoid copying the two 'duplicated' txt nodes to the result tree? I only want them inside the n1 node and not immediately after.

Upvotes: 1

Views: 48

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 116992

In XSLT 1.0 you could do something like:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:key name="following-txt" match="txt" use="generate-id(preceding-sibling::txto[1])" />

<xsl:template match="/root">
    <xsl:copy>
        <xsl:apply-templates select="txto"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="txto[starts-with(., 'Header')]">
    <n1>
        <o>
            <xsl:value-of select="." />
        </o>
        <xsl:copy-of select="key('following-txt', generate-id())"/>
    </n1>
</xsl:template>

<xsl:template match="txto">
    <xsl:copy-of select=". | key('following-txt', generate-id())"/>
</xsl:template>

</xsl:stylesheet>

Upvotes: 1

Martin Honnen
Martin Honnen

Reputation: 167516

In XSLT 2 or 3:

  <xsl:template match="root">
      <xsl:copy>
          <xsl:for-each-group select="*" group-starting-with="txto[starts-with(., 'Header')]">
              <xsl:choose>
                  <xsl:when test="position() = 1">
                      <n1>
                          <xsl:apply-templates select="current-group()"/>
                      </n1>
                  </xsl:when>
                  <xsl:otherwise>
                      <xsl:apply-templates select="current-group()"/>
                  </xsl:otherwise>
              </xsl:choose>
          </xsl:for-each-group>
      </xsl:copy>
  </xsl:template>
  
  <xsl:template match="txto[starts-with(., 'Header')][1]">
      <o>
          <xsl:apply-templates/>
      </o>
  </xsl:template>

That assumes there are several txto elements starting with Header, it is not quite sure why your code checks for that but your sample for the "second" header has an element <txto>Another header<txto> that does not meet that condition.

Based on your comment, although I still think your requirement is clear, you might simply want a pattern group-starting-with="txto" for the for-each-group and a pattern match="txto[1]" for the transformation template below.

In XSLT 1 an approach like

  <xsl:key name="sibling-group" 
           match="root/*[not(self::txto)]" 
           use="generate-id(preceding-sibling::txto[1])"/>
           
  <xsl:template match="@* | node()" name="identity">
      <xsl:copy>
          <xsl:apply-templates select="@* | node()"/>
      </xsl:copy>
  </xsl:template>
           
  <xsl:template match="root">
      <xsl:copy>
          <xsl:apply-templates select="txto"/>
      </xsl:copy>
  </xsl:template>

  <xsl:template match="txto[1]">
      <n1>
          <o>
              <xsl:apply-templates/>
          </o>
          <xsl:apply-templates select="key('sibling-group', generate-id())"/>
      </n1>
  </xsl:template>
  
  <xsl:template match="txto[not(position() = 1)]">
      <xsl:call-template name="identity"/>
      <xsl:apply-templates select="key('sibling-group', generate-id())"/>
  </xsl:template>

could work, again, the conditions might be adapted or enhanced to precisely select what a "header" is.

Upvotes: 2

Related Questions