haagel
haagel

Reputation: 2728

Iterate through siblings until a specific element type

I have a flat XML structure looking like this:

<root>
    <header>First header</header>
    <type1>Element 1:1</type1>
    <type2>Element 1:2</type2>

    <header>Second header</header>
    <type1>Element 2:1</type1>
    <type3>Element 3:1</type3>

    <header>Third header</header>
    <type1>Element 3:1</type1>
    <type2>Element 3:2</type2>
    <type1>Element 3:3</type1>
    <type2>Element 3:4</type2>
</root>

Essentialy there is an unknown number of headers. Under each header there is an unknown number of elements (tree different types). There can be zero to many elements of each type under each header. I do not have control over this structure so I can't change/improve it.

What I'm trying to generate is this HTML:

<h2>First header</h2>
<table>
    <tr>
        <th>Type 1</th>
        <td>Element 1:1</td>
    </tr>
    <tr>
        <th>Type 2</th>
        <td>Element 1:2</td>
    </tr>
</table>

<h2>Second header</h2>
<table>
    <tr>
        <th>Type 1</th>
        <td>Element 2:1</td>
    </tr>
    <tr>
        <th>Type 3</th>
        <td>Element 2:2</td>
    </tr>
</table>

<h2>third header</h2>
<table>
    <tr>
        <th>Type 1</th>
        <td>Element 3:1</td>
    </tr>
    <tr>
        <th>Type 2</th>
        <td>Element 3:2</td>
    </tr>
    <tr>
        <th>Type 1</th>
        <td>Element 3:3</td>
    </tr>
    <tr>
        <th>Type 2</th>
        <td>Element 3:4</td>
    </tr>
</table>

Each header will be an HTML header (level 2) and then I want all the other elements until the next header to be shown in a table.

My first idea is to make a template that matches the header elements:

<xsl:template match="header">
    <h2><xsl:value-of select="text()" /></h2>
    <table>
        ???
    </table>
</xsl:template>

I'm thinking I could replace "???" with code iterating through all the following siblings until the next header element and turn them into table rows.

Is that a good idea?

If it is, how do I do it? If it isn't, what's a better solution?

I'm using XSLT 1.0.

Upvotes: 0

Views: 858

Answers (1)

Tim C
Tim C

Reputation: 70638

One way to achieve this to use a key, to group the non-header elements by their first preceding header element

<xsl:key name="type" match="*[not(self::header)]" use="generate-id(preceding-sibling::header[1])" />

You would then start off by selecting just header elements

<xsl:apply-templates select="header" />

And within the template that matches this header element, you can then get all the type elements that correspond to the header by using the key

<xsl:for-each select="key('type', generate-id())">

Try this XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="html" indent="yes" />
    <xsl:key name="type" match="*[not(self::header)]" use="generate-id(preceding-sibling::header[1])" />

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

    <xsl:template match="header">
        <h2><xsl:value-of select="." /></h2>
        <table>
            <xsl:for-each select="key('type', generate-id())">
                <tr>
                    <th><xsl:value-of select="local-name()" /></th>
                    <th><xsl:value-of select="." /></th>
                </tr>
            </xsl:for-each>
        </table>
    </xsl:template>
</xsl:stylesheet>

Note, this doesn't include the issue of converting the node name type1 to Type 1, but I will leave that exercise for you....

Upvotes: 2

Related Questions