Frage

I have XML structured in the following way:

<content>
    <sh1>A</sh1>
    <sh2>A1</sh2>
    <sh2>A2</sh2>
    <sh1>B</sh1>
    <sh2>B1</sh2>
    <sh2>B2</sh2>
    <sh2>B3</sh2>
    <sh1>C</sh1>
    <sh2>C1</sh2>
    <sh2>C2</sh2>
    <sh1>D</sh1>
    <sh2>D1</sh2>
    <sh2>D2</sh2>
    <sh2>D3</sh2>
    <sh2>D4</sh2>
</content>

As you can see there are two tag names of concern here: sh1 and sh2. These represent headers and sub-headers, and I need to organize them as such in the output XML by nesting their contents (you can see example output here). My approach so far has been to match each sh1, and then try to match each sh2 between it and the next sh1.

I found this super helpful question which has gotten me (I hope) most of the way there. I've thrown together the following XSLT (1.0):

<xsl:template match="content">
    <ul>
        <xsl:apply-templates select="//sh1" />
    </ul>
</xsl:template>

<xsl:template match="sh1">
    <li>
        <h1><xsl:value-of select="." /></h1>
        <ul>
            <xsl:apply-templates select="./following-sibling::sh1[1]/preceding-sibling::sh2[preceding-sibling::.]" />
        </ul>
    </li>
</xsl:template>

<xsl:template match="sh2">
    <li><xsl:value-of select="." /></li>
</xsl:template>

The problem (predictably) is that I can't have the XPath ./following-sibling::sh1[1]/preceding-sibling::sh2[preceding-sibling::.] using . signify that the sh2 should have the current node as a preceding sibling. Is there some other keyword I could use? Or perhaps another approach entirely to get the nested structure I'm going for?

War es hilfreich?

Lösung 2

In XSLT 2.0 I would approach the problem using for-each-group:

<xsl:template match="content">
    <ul>
        <xsl:for-each-group select="*" group-starting-with="sh1">
            <li>
                <h1><xsl:value-of select="." /></h1>
                <xsl:if test="current-group()[2]">
                    <ul>
                        <xsl:apply-templates select="current-group() except ." />
                    </ul>
                </xsl:if>
            </li>
        </xsl:for-each-group>
    </ul>
</xsl:template>

<xsl:template match="sh2">
    <li><xsl:value-of select="." /></li>
</xsl:template>

If you're restricted to 1.0 then a key based approach as suggested by michael.hor257k is probably the most efficient, but the simplest fix to your existing approach would be something like

<xsl:apply-templates select="following-sibling::sh2[
     generate-id(preceding-sibling::sh1[1]) = generate-id(current())]" />

preceding-sibling::sh1[1] is the nearest preceding sh1 to the node we're filtering at the time, and current() is the sh1 element that the template is operating on. Comparing their generate-id values is the XSLT 1.0 way to determine whether the two expressions select the same node (if you just compared the values rather than their generated IDs then you risk false positives as two different sh1 elements with the same text would be considered equal). In 2.0 you could just use the is operator to do this (preceding-sibling::sh1[1] is current()).

Alternatively, you can simulate a "while loop" using a technique I've heard referred to as "sibling recursion": apply templates to just the first sh2 under this heading, and have that template recurse to the next sibling, and so on until we run out of appropriate nodes:

<ul>
    <xsl:apply-templates select="following-sibling::*[1][self::sh2]" />
</ul>

and the template:

<xsl:template match="sh2">
    <li><xsl:value-of select="." /></li>
    <xsl:apply-templates select="following-sibling::*[1][self::sh2]" />
</xsl:template>

The "loop" will terminate as soon as following-sibling::*[1] is not an sh2 element.

Andere Tipps

I believe using a key would be the most convenient approach here:

XSLT 1.0

<?xml version="1.0" encoding="UTF-8"?>
<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="sh2" match="sh2" use="generate-id(preceding-sibling::sh1[1])" />

<xsl:template match="/content">
    <ul>
        <xsl:apply-templates select="sh1" />
    </ul>
</xsl:template>

<xsl:template match="sh1">
    <li>
        <h1><xsl:value-of select="." /></h1>
        <ul>
            <xsl:apply-templates select="key('sh2', generate-id())" />
        </ul>
    </li>
</xsl:template>

<xsl:template match="sh2">
    <li><xsl:value-of select="." /></li>
</xsl:template>

</xsl:stylesheet>
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top