如何对节点之间的节点进行分组?

How to group nodes between nodes?

我知道之前已经回答过这个问题,但我很好奇如何在没有密钥的情况下实现这一点。

我有这个 XML,我必须获取 {%tab%} 和 {%endtab%} 之间的节点。

<element>
  <hello>{%tab%}</hello>
  <hello>yes</hello>
  <hello>{%endtab%}</hello>
  <hello>no</hello>
  <hello>no</hello>
  <hello>{%tab%}</hello>
  <hello>yes</hello>
  <hello>{%endtab%}</hello>
</element>

这是我得到的:

  <xsl:template match="hello[preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')]]
                     [following-sibling::hello[not(normalize-space(text())!='{%endtab%}')]]
                     [
                     (preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')])[1]/(following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
                     = (following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
                     ]">
    <strong><xsl:value-of select="." /></strong>
  </xsl:template>

这将选择后跟 {%endtab%} 和前 {%tab%} 个节点的所有节点。

hello[preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')]]
                         [following-sibling::hello[not(normalize-space(text())!='{%endtab%}')]]

问题在于 "no" 节点也被选中,因为它们也在这些节点之间。因此,我必须确保特定节点(第一次出现)之后的 {%endtab%} 与前面的 {%tab%} 之后的相同,我用这个 XPath:

[
(preceding-sibling::hello[not(normalize-space(text())!='{%tab%}')])[1]
/(following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
= (following-sibling::hello[not(normalize-space(text())!='{%endtab%}')])[1]
]

但是,这并没有按预期过滤 "no" 节点。

忽略后面的兄弟,算前面的tab和前面的endtab。如果它们不相等,那么你要么是endtab,要么是yes。排除您是 endtab.

的情况

请允许我省略 normalize-space() 以使示例更清楚。

<xsl:template match="hello[
    (. != '{%endtab%}')
    and
    (
        count(preceding-sibling::hello[. = '{%tab%}'])
        != count(preceding-sibling::hello[. = '{%endtab%}'])
    )
    ]">

编辑: 一开始我没有注意到 标签;这个答案可能对原发布者没有用

使用 XSLT 2.0 相同的巧妙技巧,您可以将 "tab" 中的元素组合在一起,这将允许同时做一些事情整个组和每个 hello 元素:

XSLT 2.0

    ...
    <xsl:for-each-group select="hello" group-by="my:tabId(.)">
        <!-- do something with a whole run -->
        <strong>
            <!-- do something with the single elements -->
            <xsl:apply-templates select="current-group()"/>
        </strong>
    </xsl:for-each-group>
    ...

<xsl:function name="my:tabId" as="xs:integer?">
    <xsl:param name="e" as="element()"/>
    <xsl:variable name="tabStartCount" 
        select="count($e/preceding-sibling::hello[. = '{%tab%}'])"/>
    <xsl:variable name="tabEndCount" 
        select="count($e/preceding-sibling::hello[. = '{%endtab%}'])"/>
    <xsl:sequence select="
        if ($e != '{%endtab%}' and ($tabStartCount > $tabEndCount)) then
            $tabStartCount
        else
            ()
    "/>
</xsl:function>

这个 XPath 表达式:

 /*/*[not(. = '{%tab%}' or . = '{%endtab%}') 
    and preceding-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%tab%}'
    and following-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%endtab%}'
     ]

选择作为顶级元素的子元素且介于具有字符串值 {%tab%} 的兄弟元素和具有字符串值 {%endtab%}

的兄弟元素之间的任何元素

这是一个 运行 证明,带有简单的 XSLT 转换:

<xsl:stylesheet version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <xsl:template match="/">
    <xsl:copy-of select=
    "/*/*[not(. = '{%tab%}' or . = '{%endtab%}') 
        and preceding-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%tab%}'
        and following-sibling::*[. = '{%tab%}' or . = '{%endtab%}'][1] = '{%endtab%}'
         ]"/>
  </xsl:template>
</xsl:stylesheet>

当此转换应用于提供的源 XML 文档时:

<element>
  <hello>{%tab%}</hello>
  <hello>yes</hello>
  <hello>{%endtab%}</hello>
  <hello>no</hello>
  <hello>no</hello>
  <hello>{%tab%}</hello>
  <hello>yes</hello>
  <hello>{%endtab%}</hello>
</element>

产生了想要的、正确的结果:

<hello>yes</hello>
<hello>yes</hello>

两个普遍感兴趣的笔记:

1.

I know that this has been answered before, but I am curious about how to achieve this without keys.

A link 使用 key 的解决方案将按顺序排列:

2.

由于之前已经建议了 XSLT 2.0 解决方案,我会建议另一个我认为更简单的解决方案:

XSLT 2.0

<xsl:stylesheet version="2.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="/element">
    <xsl:copy>
        <xsl:for-each-group select="hello" group-starting-with="hello[.='{%tab%}']">
            <xsl:for-each-group select="current-group()" group-ending-with="hello[.='{%endtab%}']">
                <xsl:if test="current-group()[self::hello[.='{%tab%}']]">
                    <xsl:for-each select="current-group()[not(position()=1 or position()=last())]">
                        <strong><xsl:value-of select="." /></strong>
                    </xsl:for-each>
                </xsl:if>
            </xsl:for-each-group>
        </xsl:for-each-group>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>