1
votes

With the following XML

<page>
  <chunk id="1001" sequence="1">
    <meta>Inline</meta>
    <body>Inline chunk 1</body>
  </chunk>
  <chunk id="1002" sequence="2">
    <meta>Tabs</meta>
    <body>Tab chunk 1</body>
  </chunk>
  <chunk id="1054" sequence="3">
    <meta>Tabs</meta>
    <body>Tab chunk 2</body>
  </chunk>
  <chunk id="1023" sequence="4">
    <meta>Inline</meta>
    <body>Inline chunk 2</body>
  </chunk>
  <chunk id="1013" sequence="5">
    <meta>Tabs</meta>
    <body>Tab chunk 3</body>
  </chunk>
  <chunk id="1072" sequence="6">
    <meta>Tabs</meta>
    <body>Tab chunk 4</body>
  </chunk>
</page>

I would like to apply an XSL template to output this:

<main>
  <section class="Inline>
    <div>Inline chunk 1</div>
  </section>
  <section class="Tabs>
    <div>Tab chunk 1</div>
    <div>Tab chunk 2</div>
  </section>
  <section class="Inline>
    <div>Inline chunk 2</div>
  </section>
  <section class="Tabs>
    <div>Tab chunk 3</div>
    <div>Tab chunk 4</div>
  </section>
</main>

Basically I would like to output the chunk in the order of their ./@sequence, but group them based on their ./meta value when possible. By "possible", I mean only group them if they have the same ./meta value and next to each other in sequence.

Using Muenchian Method, I could group the chunk by ./meta, but only get the following result:

<main>
  <section class="Inline>
    <div>Inline chunk 1</div>
    <div>Inline chunk 2</div>
  </section>
  <section class="Tabs>
    <div>Tab chunk 1</div>
    <div>Tab chunk 2</div>
    <div>Tab chunk 3</div>
    <div>Tab chunk 4</div>
  </section>
</main>

This is the XSLT I use:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:key name="kBucketByLabel" match="/page/chunk" use="./meta" />

  <!-- Then we override/define a template to match "/SAM" and write our body HTML here. -->
  <xsl:template match="/page" mode="body">
    <main>
      <xsl:apply-templates select="chunk[generate-id() = generate-id( key('kBucketByLabel', ./meta)[1] )]" mode="bucket">
        <xsl:sort select="@sequence" data-type="number" />
      </xsl:apply-templates>
    </main>

  </xsl:template>

  <xsl:template match="chunk" mode="bucket">
    <xsl:variable name="bucket" select="./meta" />
    <section class="{$bucket}">
      <xsl:for-each select="key('kBucketByLabel', $bucket)">
        <div><xsl:value-of select="./body"/></div>
      </xsl:for-each>
    </section>
  </xsl:template>

</xsl:stylesheet>
1
can you please append your XSLT too in your questionnawazlj
The output is not well-formed as it has multiple root elements.Jim Garrison
can you use xsl 2.0?Joel M. Lamsen
Unfortunately I can only use 1.0.Sơn Trần-Nguyễn

1 Answers

2
votes

One way to do this is through a technique known as "sibling recursion":

XSLT 1.0

<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:template match="/page">
    <main>
        <xsl:apply-templates select="chunk[not(meta = preceding-sibling::chunk[1]/meta)]"/>
    </main>
</xsl:template>

<xsl:template match="chunk">
    <section class="{meta}">
        <xsl:apply-templates select="." mode="collect"/>
    </section>
</xsl:template>

<xsl:template match="chunk" mode="collect">
    <div>
        <xsl:value-of select="body"/>
    </div>
    <xsl:apply-templates select="following-sibling::chunk[1][meta = current()/meta]" mode="collect"/>
</xsl:template> 

</xsl:stylesheet>

Added:

Does this work with an unsorted chunks? In my sample data above, I intentionally sorted them by @sequence for ease of visualization, but my actual data are unsorted.
...
I managed to have them sorted by first looping through the chunks with <xsl:for-each>, sort them by @sequence and store copy of them to a RTF. I had to use extension to convert the RTF into node-set and apply your technique. It works, but I am hoping not to do that if possible.

Actually, that's exactly what I would do: sort the data first, convert it a node-set, then apply the above method in a second pass.

If you don't want to that, you could use a key to refer to the previous/next chunk in sequence - provided that the sequence is contiguous:

XSLT 1.0

<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:key name="chunk" match="chunk" use="@sequence" />

<xsl:template match="/page">
    <main>
        <xsl:apply-templates select="chunk[not(meta = key('chunk', @sequence - 1)/meta)]">
            <xsl:sort select="@sequence" data-type="number" order="ascending"/>
        </xsl:apply-templates>
    </main>
</xsl:template>

<xsl:template match="chunk">
    <section class="{meta}">
        <xsl:apply-templates select="." mode="collect"/>
    </section>
</xsl:template>

<xsl:template match="chunk" mode="collect">
    <div>
        <xsl:value-of select="body"/>
    </div>
    <xsl:apply-templates select="key('chunk', @sequence + 1)[meta = current()/meta]" mode="collect"/>
</xsl:template> 

</xsl:stylesheet>