Anders Rabo Thorbeck
Anders Rabo Thorbeck

Reputation: 1206

XSLT 2: How to wrap all content between first and last occurrence of a XML-tag?

I am transforming XML using XSLT 2.0. It is important that all the text nodes from the input XML are included in the resulting XML, and in the same order that they occurred in the input. For the element nodes, in most cases I only want to change the name of the tag, or add some hierarchy in terms of wrapping certain nodes in a new node.

For this question, I want to know how I can treat as one "unit" all the content from (inclusive) the first child occurrence of a certain tag up until (inclusive) the last child occurrence of the same tag, including text and other tags in between. At the same time, I wish to also be able to treat all the children preceding this selection as a separate "unit", and all the children succeeding this selection as another separate "unit".

I have included a dummy example of what I want. Assume that the "current node" is <c>, e.g. if we are in an <xsl:template match="//c">. I would like to wrap everything (under <c>) from the first <e> node to the last <e> node (inclusive), including the <f> node, in a node <es>. I would furthermore (still only in the context <c>) like to leave everything before as-is, but wrap everything after in a node <after-es>. I want this done without any side effects outside of <c>, such as moving contents into or out of the <c> node.

Input XML:

<a>
  alfred
  <b>bob</b>
  charlie
  <c>
    dover
    <d>elon</d>
    fabio
    <e>grant</e>
    hugh
    <f>illinois</f>
    jacob
    <e>kathy</e>
    lombard
    <e>
      moby
      <g>narwhal</g>
      obi-wan
    </e>
    pontiac
    <h>quantas</h>
    rhino
  </c>
  xenu
  <z>yoga</z>
  zombie
</a>

Expected output XML:

<a>
  alfred
  <b>bob</b>
  charlie
  <c>
    dover
    <d>elon</d>
    fabio
    <es>
      <e>grant</e>
      hugh
      <f>illinois</f>
      jacob
      <e>kathy</e>
      lombard
      <e>
        moby
        <g>narwhal</g>
        obi-wan
      </e>
    </es>
    <after-es>
      pontiac
      <h>quantas</h>
      rhino
    </after-es>
  </c>
  xenu
  <z>yoga</z>
  zombie
</a>

How can this be done? Preferably using XSLT 2.0. The neater the solution, the better.

Upvotes: 0

Views: 621

Answers (2)

Anders Rabo Thorbeck
Anders Rabo Thorbeck

Reputation: 1206

My own take to solving this, started before I saw the answer by @michael.hor257k .

<xsl:stylesheet
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="2.0">

  <xsl:output omit-xml-declaration="yes" />

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

  <xsl:template match="c">
    <xsl:variable name="first-e" select="count(e[     1]/preceding-sibling::node()) + 1"/>
    <xsl:variable name="last-e"  select="count(e[last()]/preceding-sibling::node()) + 1"/>

    <xsl:copy>
      <xsl:apply-templates select="e[1]/preceding-sibling::node()"/>
      <es>
        <xsl:apply-templates select="node()[$first-e &lt;= position() and position() &lt;= $last-e]"/>
      </es>
      <after-es>
        <xsl:apply-templates select="e[last()]/following-sibling::node()"/>
      </after-es>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 117018

Here's one way you could look at it:

XSLT 2.0

<xsl:stylesheet version="2.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="*"/>

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

<xsl:template match="c">
    <xsl:variable name="nodes-before" select="node()[. &lt;&lt; ../e[1]]"/>
    <xsl:variable name="nodes-after" select="node()[. >> ../e[last()]]"/>
    <xsl:copy>
        <xsl:apply-templates select="$nodes-before"/>
        <es>
            <xsl:apply-templates select="node() except ($nodes-before|$nodes-after)"/>
        </es>
        <xsl:if test="$nodes-after">
            <after-es>
                <xsl:apply-templates select="$nodes-after"/>
            </after-es>
        </xsl:if>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Note that there is an underlying assumption here that c has at least one e child.

Upvotes: 2

Related Questions