如何在XSLT输出中包含通过xpath参数选择的节点的祖先分支

8
After trying for over 8 hours, I am hoping someone can help me with this.
给定以下(简化的)图书XML:
<book>
    <section name="A">
        <chapter name="I">
            <paragraph name="1"/>
            <paragraph name="2"/>
        </chapter>
        <chapter name="II">
            <paragraph name="1"/>          
        </chapter>
    </section>
    <section name="B">
        <chapter name="III"/>
        <chapter name="IV"/>   
    </section>
</book>

我能够基于给定参数,使用以下XSL从书籍XML中提取任何部分(章节或段落):
<xsl:param name="subSelectionXPath" required="yes" as="node()"/>

<xsl:template match="/">
    <xsl:apply-templates select="$subSelectionXPath"/>
</xsl:template>

<xsl:template match="*">
    <!-- output node with all children -->
    <xsl:copy-of select="."/>
</xsl:template>

并使用值类似于

doc(filename)//chapter[@name='II']
的参数$subSelectionXPath

导致输出:

<chapter name="II">
    <paragraph name="1"/>          
</chapter>

除此之外,我想实现的是将所选的XML片段包含在其祖先XML分支中,即:

<book>
    <section name="A">
        <chapter name="II">
            <paragraph name="1"/>          
        </chapter>
    </section>    
</book>

我想象并尝试遍历XML树,并测试当前节点是否为祖先节点,类似于以下伪代码:
<xsl:if test="node() in $subSelectionXPath/ancestor::node()">
    <xsl:copy>
       <xsl:apply-templates/>
    </xsl:copy>
</xsl:if>

我也尝试过使用xsl:key,但我担心我的XSLT知识到此为止了。有什么想法吗?


所以,给定一个输入元素,您想要复制其祖先并输出带有祖先的元素,而不带任何兄弟姐妹,是吗? - Emiliano Poggi
1
好问题,+1。请查看我的答案,其中包含完整、简短、易于理解的XSLT 2.0解决方案。 - Dimitre Novatchev
1
我还添加了详细的解释。 - Dimitre Novatchev
我还添加了一个类似的XSLT 1.0解决方案,使用Kayessian公式来计算两个节点集的交集。 - Dimitre Novatchev
1
更有趣的是,你在这里得到了三个相当不错的答案,但你还没有接受其中任何一个。 :) - Emiliano Poggi
显示剩余3条评论
3个回答

6

从你的代码中可以看出,你正在使用XSLT 2.0。

这个XSLT 2.0转换

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

 <xsl:param name="subSelectionXPath"
  as="node()" select="//chapter[@name='II']"
  />

 <xsl:template match="*[descendant::node() intersect $subSelectionXPath]">
  <xsl:copy>
   <xsl:copy-of select="@*"/>
   <xsl:apply-templates select="*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="*[. intersect $subSelectionXPath]">
  <xsl:copy-of select="."/>
 </xsl:template>
</xsl:stylesheet>

当应用于提供的XML文档时

<book>
    <section name="A">
        <chapter name="I">
            <paragraph name="1"/>
            <paragraph name="2"/>
        </chapter>
        <chapter name="II">
            <paragraph name="1"/>
        </chapter>
    </section>
    <section name="B">
        <chapter name="III"/>
        <chapter name="IV"/>
    </section>
</book>

能够产生精确、正确的结果:

<book>
   <section name="A">
      <chapter name="II">
         <paragraph name="1"/>
      </chapter>
   </section>
</book>

解释: 我们只有两个模板:

  1. 匹配任何一个元素,其后代与 $subSelectionXPath 节点集有非空交集。在此处我们执行“浅拷贝”操作,并将模板应用于其子元素。

  2. 匹配属于 $subSelectionXPath 节点集的元素。在此处我们复制以该元素为根的整个子树。

  3. 请注意使用 XPath 2.0 的 intersect 运算符。

  4. 没有显式递归。

II. XSLT 1.0 解决方案:

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

 <xsl:param name="subSelectionXPath"
  select="//chapter[@name='II']"
  />

 <xsl:template match="*">
  <xsl:choose>
   <xsl:when test=
   "descendant::node()
        [count(.|$subSelectionXPath)
        =
         count($subSelectionXPath)
        ]
   ">
   <xsl:copy>
    <xsl:copy-of select="@*"/>
    <xsl:apply-templates select="*"/>
   </xsl:copy>
  </xsl:when>

   <xsl:when test=
   "count(.|$subSelectionXPath)
   =
    count($subSelectionXPath)
   ">
   <xsl:copy-of select="."/>
   </xsl:when>
  </xsl:choose>
 </xsl:template>
</xsl:stylesheet>

当将此转换应用于相同的XML文档(如上所示),将产生相同的期望和正确结果。
<book>
   <section name="A">
      <chapter name="II">
         <paragraph name="1"/>
      </chapter>
   </section>
</book>
解释:本质上,这是XSLT 2.0解决方案,其中XPath 2.0的intersect运算符使用两个节点集$ns1$ns2的著名Kayessian(用于@Michael Kay)公式转换为XPath 1.0的形式。
$ns1[count(.|$ns2) = count($ns2)]

@empo:你的赞赏对我来说非常重要。谢谢。 - Dimitre Novatchev

3
我认为实现你想要的功能的方法是使用递归模板:

我认为实现你想要的功能的方法是使用递归模板:

<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

 <xsl:template match="/">
    <xsl:call-template name="copyElementsOnAncestorAxis">
      <xsl:with-param name="nodeList"
                      select="//chapter[@name='I']/ancestor-or-self::*"/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="copyElementsOnAncestorAxis">
    <xsl:param name="nodeList"/>
    <xsl:choose>
      <!-- if the context node is the last node in the list, copy it entirely -->
      <xsl:when test=". = $nodeList[count($nodeList)]">
        <xsl:copy-of select="."/>
      </xsl:when>
      <!-- otherwise, just copy the element, its attributes, and any child element that 
           is also in the node list -->
      <xsl:otherwise>
        <xsl:copy>
          <xsl:copy-of select="@*"/>
          <xsl:for-each select="*[. = $nodeList]">
            <xsl:call-template name="copyElementsOnAncestorAxis">
              <xsl:with-param name="nodeList"
                              select="$nodeList"/>
            </xsl:call-template>
          </xsl:for-each>
        </xsl:copy>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>

应用于您提供的XML时,会产生以下结果:
<book>
  <section name="A">
    <chapter name="I">
      <paragraph name="1" />
      <paragraph name="2" />
    </chapter>
  </section>
</book>

2

这是另一种基于递归的解决方案。解释如下:

  • 首先将 apply-templates 应用于最后一个祖先节点
  • 然后,如果当前参数节点的祖先节点,则复制该节点;如果父节点,则复制节点并结束,否则递归。

以下是转换代码:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:param name="subSelectionXPath" 
        select="document('test_input2.xml')//chapter[@name='II']"/>

    <xsl:template match="/">
        <xsl:apply-templates 
            select="$subSelectionXPath/ancestor::*[position()=last()]"/>
    </xsl:template>

    <xsl:template match="*">
        <xsl:choose>

            <xsl:when test="$subSelectionXPath/ancestor::*
                [generate-id() = generate-id(current())]">
                <xsl:copy>
                    <xsl:copy-of select="@*"/>

                    <xsl:choose>
                        <xsl:when test="generate-id(.)=
                            generate-id($subSelectionXPath/ancestor::*[1])">
                            <xsl:copy-of select="$subSelectionXPath"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:apply-templates select="*"/>
                        </xsl:otherwise>
                    </xsl:choose>

                </xsl:copy>
            </xsl:when>
            <xsl:otherwise/>
        </xsl:choose>
    </xsl:template>
  </xsl:stylesheet>

当应用于问题中提供的输入时,假设问题中的输入参数值相同,则产生:
<book>
   <section name="A">
      <chapter name="II">
         <paragraph name="1"/>
      </chapter>
   </section>
</book>

很抱歉进行了多次编辑,但格式令我很疯狂 :) - Emiliano Poggi
有趣的是,这个解决方案对我很有效。为了清晰起见,我简化了示例,但在我的实际代码中,$subSelectionXPath参数也以doc(...)开头,并且我从命令行提供它。这在Dimitre的代码中没有奏效,但在Empo的代码中却奏效了。 - Brunsaim
@Brunsaim:你只需要在我的代码中用你的定义替换$subSelectionXpath的定义 - 当本质不会改变时,我避免在我的答案中使用doc()document() - Dimitre Novatchev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接