0

My goal is to group the nodes first by <RowBreak>, then in each <RowBreak> group, I want to group by <ColumnBreak>.

Here is my XML.

<?xml version="1.0" encoding="utf-8" ?>
<Tree>
  <Item>
    <Label>Item 1</Label>
  </Item>
  <Item>
    <Label>Item 2</Label>
  </Item>
  <ColumnBreak />
  <Item>
    <Label>Item 3</Label>
  </Item>
  <Item>
    <Label>Item 4</Label>
  </Item>
  <Item>
    <Label>Item 5</Label>
  </Item>
  <RowBreak />
  <Item>
    <Label>Item 6</Label>
  </Item>
  <Item>
    <Label>Item 7</Label>
  </Item>
  <ColumnBreak />
  <Item>
    <Label>Item 8</Label>
  </Item>
  <RowBreak />
  <Item>
    <Label>Item 9</Label>
  </Item>
  <Item>
    <Label>Item 10</Label>
  </Item>
</Tree>

The output should be:

Item 1  Item 3
Item 2  Item 4
        Item 5

Item 6  Item 8
Item 7

Item 9
Item 10

My current XSLT is like this:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="html" indent="yes"/>
  <xsl:key name="rowGroups" match="Tree/*[not(self::RowBreak)]" use="count(preceding-sibling::RowBreak)" />

  <xsl:template match="Tree">
    <xsl:variable name="rowGroupings" select="*[not(self::RowBreak)][generate-id() = generate-id(key('rowGroups', count(preceding-sibling::RowBreak))[1])]" />
    <xsl:variable name="position" select="position()" />
    <table>
      <xsl:for-each select="$rowGroupings">
        <xsl:variable name="rowId" select="generate-id()"/>
        <xsl:variable name="colGroupings" select="*[not(self::ColumnBreak)][generate-id()=$rowId][1]" />
        <tr>
            <xsl:for-each select="$colGroupings">
              <!--Do logic here to group by ColumnBreak-->
            </xsl:for-each>
        </tr>
      </xsl:for-each>
    </table>
  </xsl:template>
</xsl:stylesheet>

However, I'm getting problem with extracting the <ColumnBreak> groups in every <RowBreak> (see colGroupings variable). I want to create a <key> for every <RowBreak> in the loop (similar to rowGroups), but as per my understanding of <xsl:key> element, it has to be declared top-level and the match should work on actual nodes, not on variables.

mark uy
  • 521
  • 1
  • 6
  • 17
  • 1
    If you group by `RowBreak`, then by `ColumnBreak`, the output will not be what you show. Plus you have the problem of missing the first (or the last) break. FWIW, I don't think it can be done in one pass. Well, maybe - but using sibling recursion, not Muenchian grouping. – michael.hor257k Dec 20 '18 at 06:20

1 Answers1

2

This is what I would do as the first pass:

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:strip-space elements="*"/>

<xsl:template match="/Tree">
    <cells>
        <xsl:apply-templates select="Item[1]" mode="sibling">
            <xsl:with-param name="row" select="1"/>
            <xsl:with-param name="col" select="1"/>
        </xsl:apply-templates>  
    </cells>
</xsl:template>

<xsl:template match="Item" mode="sibling">
    <xsl:param name="row"/>
    <xsl:param name="col"/>
    <cell row="{$row}" col="{$col}">
        <xsl:value-of select="Label"/>
    </cell>
    <xsl:apply-templates select="following-sibling::*[1]" mode="sibling">
        <xsl:with-param name="row" select="$row"/>
        <xsl:with-param name="col" select="$col"/>
    </xsl:apply-templates>  
</xsl:template>

<xsl:template match="ColumnBreak" mode="sibling">
    <xsl:param name="row"/>
    <xsl:param name="col"/>
    <xsl:apply-templates select="following-sibling::*[1]" mode="sibling">
        <xsl:with-param name="row" select="$row"/>
        <xsl:with-param name="col" select="$col + 1"/>
    </xsl:apply-templates>  
</xsl:template>

<xsl:template match="RowBreak" mode="sibling">
    <xsl:param name="row"/>
    <xsl:param name="col"/>
    <xsl:apply-templates select="following-sibling::*[1]" mode="sibling">
        <xsl:with-param name="row" select="$row + 1"/>
        <xsl:with-param name="col" select="1"/>
    </xsl:apply-templates>  
</xsl:template>

</xsl:stylesheet>

Applied to your example input, this would produce:

Result

<?xml version="1.0" encoding="UTF-8"?>
<cells>
  <cell row="1" col="1">Item 1</cell>
  <cell row="1" col="1">Item 2</cell>
  <cell row="1" col="2">Item 3</cell>
  <cell row="1" col="2">Item 4</cell>
  <cell row="1" col="2">Item 5</cell>
  <cell row="2" col="1">Item 6</cell>
  <cell row="2" col="1">Item 7</cell>
  <cell row="2" col="2">Item 8</cell>
  <cell row="3" col="1">Item 9</cell>
  <cell row="3" col="1">Item 10</cell>
</cells>

which is something that can be actually worked with.


Added:

Here's a complete stylesheet that processes the input in two passes:

  • the first pass uses sibling recursion as described above;
  • the second pass does Muenchian grouping by rows and by columns in order to produce the table described by the original document:

XSLT 1.0 (with EXSLT node-set() extension function)

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:strip-space elements="*"/>
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:key name="cell-by-row" match="cell" use="@row" />
<xsl:key name="cell-by-col" match="cell" use="concat(@row, '|', @col)" />

<xsl:template match="/Tree">
    <!-- first-pass -->
    <xsl:variable name="cells">
        <xsl:apply-templates select="Item[1]" mode="sibling">
            <xsl:with-param name="row" select="1"/>
            <xsl:with-param name="col" select="1"/>
        </xsl:apply-templates>  
    </xsl:variable>
    <!-- output -->
    <table border = "1">
        <!-- for each distinct row -->
        <xsl:for-each select="exsl:node-set($cells)/cell[count(. | key('cell-by-row', @row)[1]) = 1]">
            <tr>
                <!-- for each distinct cell in the current row -->
                <xsl:for-each select="key('cell-by-row', @row)[count(. | key('cell-by-col', concat(@row, '|', @col))[1]) = 1]">
                    <td>
                        <!-- get the values in the current cell -->
                        <xsl:for-each select="key('cell-by-col', concat(@row, '|', @col))">
                            <xsl:value-of select="."/>
                            <br/>
                        </xsl:for-each>
                    </td>
                </xsl:for-each>
            </tr>
        </xsl:for-each>
    </table>
</xsl:template>

<xsl:template match="Item" mode="sibling">
    <xsl:param name="row"/>
    <xsl:param name="col"/>
    <cell row="{$row}" col="{$col}">
        <xsl:value-of select="Label"/>
    </cell>
    <xsl:apply-templates select="following-sibling::*[1]" mode="sibling">
        <xsl:with-param name="row" select="$row"/>
        <xsl:with-param name="col" select="$col"/>
    </xsl:apply-templates>  
</xsl:template>

<xsl:template match="ColumnBreak" mode="sibling">
    <xsl:param name="row"/>
    <xsl:param name="col"/>
    <xsl:apply-templates select="following-sibling::*[1]" mode="sibling">
        <xsl:with-param name="row" select="$row"/>
        <xsl:with-param name="col" select="$col + 1"/>
    </xsl:apply-templates>  
</xsl:template>

<xsl:template match="RowBreak" mode="sibling">
    <xsl:param name="row"/>
    <xsl:param name="col"/>
    <xsl:apply-templates select="following-sibling::*[1]" mode="sibling">
        <xsl:with-param name="row" select="$row + 1"/>
        <xsl:with-param name="col" select="1"/>
    </xsl:apply-templates>  
</xsl:template>

</xsl:stylesheet>

Result

<?xml version="1.0" encoding="utf-8"?>
<table border="1">
  <tr>
    <td>Item 1<br/>Item 2<br/></td>
    <td>Item 3<br/>Item 4<br/>Item 5<br/></td>
  </tr>
  <tr>
    <td>Item 6<br/>Item 7<br/></td>
    <td>Item 8<br/></td>
  </tr>
  <tr>
    <td>Item 9<br/>Item 10<br/></td>
  </tr>
</table>

Rendered

enter image description here

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Thanks a lot, @michael.hor257k! This works for my scenarios. – mark uy Dec 20 '18 at 09:11
  • This is the first time I've seen the use of cells for grouping. If I want to get the number of columns per row, is it correct to use the count of `key('cell-by-row', @row)[count(. | key('cell-by-col', concat(@row, '|', @col))[1]) = 1]`? – mark uy Dec 20 '18 at 09:44
  • 1
    The number of columns in a given row is equal to the column number of the last cell in the row - which can be retrieved using: `` (where `$row` is the given row number). – michael.hor257k Dec 20 '18 at 09:55
  • Hello michael.hor257k I have a follow-up question. There may be other nodes in my XML which is not of type ``. Let's say the node is ``. If I want to exclude this from the sibling recursion, I'd call ``. Is there a way to make this select condition reusable such that Item, RowBreak, and ColumnBreak templates can use it without repetitive coding? I'm afraid I have handling to exclude some items by node type and attributes, so keeping my code shorter would be ideal. Thanks! – mark uy Dec 21 '18 at 02:06
  • 1
    Yes and no. You could define a global variable selecting the nodes to exclude (or, perhaps preferably, the three nodes to include), then write the predicate to use the variable. But it wouldn't be any shorter, on the contrary. OTOH, it would have the advantage of having the blacklist (or the whitelist) in a single place. – michael.hor257k Dec 21 '18 at 07:33