Matching nodes between current node and next with a certain tag-name

别说谁变了你拦得住时间么 提交于 2020-01-05 02:46:07

问题


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?


回答1:


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.




回答2:


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>


来源:https://stackoverflow.com/questions/23476401/matching-nodes-between-current-node-and-next-with-a-certain-tag-name

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!