tags:

views:

62

answers:

4

Using XSLT, how can I wrap siblings that share the same value for an attribute.

Lets say I need to wrap a one or more <amendment/> with the <chapter/> they belong too. From this:

<section>
      <heading>some heading text</heading>
      <amendment num='1' chapter='1'>
            <foo/>
      </amendment>
      <amendment num='2' chapter='1'>
            <bar/>
      </amendment>
      <amendment num='3' chapter='2'>
            <baz/>
      </amendment>
      <heading>some heading text</heading>
      <amendment num='4' chapter='3'>
            <baz/>
      </amendment>
</section>

into this:

<section>
      <heading>some heading text</heading>
      <chapter num="1">
            <amendment num='1'>
                  <foo/>
            </amendment>
            <amendment num='2'>
                  <bar/>
            </amendment>
      </chapter>
      <chapter num="2">
            <amendment num='3'>
                  <baz/>
            </amendment>
      </chapter>
      <heading>some heading text</heading>
      <chapter num="3">
            <amendment num='4'>
                  <baz/>
            </amendment>
      </chapter>
</section>

Note 1: Amendments are always listed sorted by chapter in the source XML.

Note 2: Im using PHP5 with XSLT 1.0

A: 

If you're using XSLT 1, you can use the Muenchian grouping method like this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:key name="chapter" use="@chapter" match="amendment" />

  <xsl:template match="section">
    <xsl:copy>
      <xsl:apply-templates select="heading | amendment[generate-id() = generate-id(key('chapter',@chapter)[1])]" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="amendment">
    <xsl:element name="chapter">
      <xsl:attribute name="num">
        <xsl:value-of select="@chapter" />
      </xsl:attribute>
      <xsl:apply-templates select="key('chapter', @chapter)" mode="withoutchapter"/>
    </xsl:element>
  </xsl:template>

  <xsl:template match="amendment" mode="withoutchapter">
    <xsl:copy>
      <xsl:apply-templates  select="@*[(name() != 'chapter')] | node()"/>
    </xsl:copy>
  </xsl:template>

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

There's two 'amendment' templates here- the first one (without the mode) is only invoked by the section template on amendments that are the first occurrence of an amended with that chapter. It creates a chapter element, and within it invokes the second template on each amendment tag with that chapter.

Two caveats here; First, any amendments without a chapter will be dropped from the output.

Secondly, where there is a heading in between two amendment tags, the amendment tags will still be grouped, and the heading will appear after the group.

So, if you do (abbreviated for clarity):

<amendment num='1' chapter='1' />
<heading>heading text</heading>
<amendment num='2' chapter='1' />

It'll output:

<chapter num='1'>
  <amendment num='1' />
  <amendment num='2' />
</chapter>
<heading>heading text</heading>
Flynn1179
+1  A: 

Thanks to @Alejandro and @Flynn1179 for noticing an error in the initial version of this solution -- now corrected!

This transformation:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:key name="kbyChapter" match="amendment"
     use="@chapter"/>

 <xsl:template match="node()" name="identity">
     <xsl:copy>
       <xsl:apply-templates select="@*"/>
       <xsl:apply-templates select="node()[1]"/>
     </xsl:copy>
     <xsl:apply-templates select="following-sibling::node()[1]"/>
 </xsl:template>

 <xsl:template match="amendment"/>
 <xsl:template match=
 "amendment[not(@chapter=preceding-sibling::amendment[1]/@chapter)]">
  <chapter num="{@chapter}">
   <xsl:apply-templates select="key('kbyChapter',@chapter)" mode="copy"/>
  </chapter>
  <xsl:apply-templates select=
    "key('kbyChapter',@chapter)[last()]/following-sibling::node()[1]"/>
 </xsl:template>

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

 <xsl:template match="@*">
  <xsl:copy-of select="."/>
 </xsl:template>

 <xsl:template match="@chapter"/>
</xsl:stylesheet>

when applied on the provided XML document (with a correction on the last amendment):

<section>
      <heading>some heading text</heading>
      <amendment num='1' chapter='1'>
            <foo/>
      </amendment>
      <amendment num='2' chapter='1'>
            <bar/>
      </amendment>
      <amendment num='3' chapter='2'>
            <baz/>
      </amendment>
      <heading>some heading text</heading>
      <amendment num='4' chapter='3'>
            <baz/>
      </amendment>
</section>

produces the wanted, correct result:

<section>
    <heading>some heading text</heading>
    <chapter num="1">
        <amendment num="1">
            <foo/>
        </amendment>
        <amendment num="2">
            <bar/>
        </amendment>
    </chapter>
    <chapter num="2">
        <amendment num="3">
            <baz/>
        </amendment>
    </chapter>
    <heading>some heading text</heading>
    <chapter num="3">
        <amendment num="4">
            <baz/>
        </amendment>
    </chapter>
</section>
Dimitre Novatchev
@Dimitre, should those chapter elements be nesting like that?
Flynn1179
@Dimitre: I think Flynn1179 is right. For fine grained traversal you need the close rule, something like `<xsl:template match="*[last()]" mode="copy">`
Alejandro
@Alejandro, @Flynn1179: Thanks for noticing this. Fixed now. When I was solving this problem I was almost late for an appointment -- and it showed :)
Dimitre Novatchev
+1 Because I like the expression to get the following outside the group.
Alejandro
A: 

Because your amendments are sorted by chapter, you can get away with not using a key, an instead just looking at the immediately following and preceding elements. The following should work:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

<xsl:template match="amendment[not(@chapter = preceding-sibling::amendment[1]/@chapter)]">
  <chapter num="{@chapter}">
    <xsl:variable name="chapter" select="@chapter"/>
      <amendment num="{@num}">
        <xsl:apply-templates/>
      </amendment>
    <xsl:apply-templates select="following-sibling::amendment[1][@chapter = $chapter]">
      <xsl:with-param name="chapter" select="@chapter"/>
    </xsl:apply-templates>
  </chapter>
</xsl:template>

<xsl:template match="amendment">
  <xsl:param name="chapter"/>
  <xsl:if test="$chapter">
      <amendment num="{@num}">
        <xsl:apply-templates/>
      </amendment>
      <xsl:apply-templates select="following-sibling::amendment[1][@chapter = $chapter]">
        <xsl:with-param name="chapter" select="$chapter"/>
      </xsl:apply-templates>
  </xsl:if>
</xsl:template>

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

</xsl:stylesheet>
Nick Jones
+1  A: 

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:key name="kAmendementByChapter" match="amendment" use="@chapter"/>
    <xsl:template match="node()|@*" name="identity">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="amendment[count(.|key('kAmendementByChapter',
                                               @chapter)[1])=1]">
        <chapter num="{@chapter}">
            <xsl:apply-templates select="key('kAmendementByChapter',@chapter)"
                                 mode="copy"/>
        </chapter>
    </xsl:template>
    <xsl:template match="amendment"/>
    <xsl:template match="amendment" mode="copy">
        <xsl:call-template name="identity"/>
    </xsl:template>
    <xsl:template match="@chapter"/>
</xsl:stylesheet>

Output:

<section>
    <heading>some heading text</heading>
    <chapter num="1">
        <amendment num="1">
            <foo></foo>
        </amendment>
        <amendment num="2">
            <bar></bar>
        </amendment>
    </chapter>
    <chapter num="2">
        <amendment num="3">
            <baz></baz>
        </amendment>
    </chapter>
    <heading>some heading text</heading>
    <chapter num="3">
        <amendment num="4">
            <baz></baz>
        </amendment>
    </chapter>
</section>

Note: Copy all (indentity rule), grouping on @chapter.

Alejandro
@Alejandro: +1 for the best answer!
Dimitre Novatchev