消耗节点集并使用前兄弟轴的递归模板

时间:2014-03-21 00:26:30

标签: xml xslt recursion node-set

我有一些像这样的XML:

<TEI>
  <text>
    <div type="scene" n="1">
      <sp xml:id="sp1">
        <speaker>Julius</speaker>
        <l>Lorem ipsum dolor sit amet</l>
        <ptr cRef="..." />
        <stage>Aside</stage>
        <ptr cRef="..." />
        <l>consectetur adipisicing elit</l>
        <stage>To Antony</stage>
        <l>sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</l>
      </sp>
      <sp xml:id="sp2">
        ...

我需要将所有<stage>个元素提升到一个级别,成为<sp>的兄弟姐妹,打破<sp>以使<stage>元素保留他们与<sp>内其他元素的前后关系,例如

<TEI>
  <text>
    <div type="scene" n="1"> 
     <sp by="#Julius">
       <l>Lorem ipsum dolor sit amet</l>
       <ptr cRef="..." />
     </sp>
     <stage>Aside</stage>
     <sp by="#Julius">
       <ptr cRef="..." />
       <l>consectetur adipisicing elit</l>
     </sp>
     <stage>To Antony</stage>
     <sp by="#Julius">
       <l>sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</l>
     </sp>

我一直在使用XSLT来做这件事。它包含一个递归模板,用于使用<sp>的所有子元素,但不包括第一个<stage>子项,并在结果树中将它们作为新{<sp>的子项发出。 1}}。然后发出第一个<stage>元素。然后递归第一个<stage>元素后面的所有元素。最终,当子元素列表没有剩余<stage>时,所有剩余的元素将在新<sp>内的结果树中发出。这是代码,包括调试<xsl:message> s:

<xsl:template name="sp-with-stage">
  <!-- call with speaker -->
  <xsl:param name="speaker" />
  <!-- call with an <sp> element -->
  <xsl:param name="sp" />
  <!-- $content parameter is optional, by default it's the children of the given $sp; this is the parameter whose value is different with each recursive call -->
  <xsl:param name="content" select="$sp/*" />
  <!-- find the first <stage> element amongst the $content node set -->
  <xsl:variable name="stage" select="$content/following-sibling::stage[1]" />

  <xsl:message>ID = <xsl:value-of select="$sp/@xml:id" /></xsl:message>
  <xsl:message>speaker = "<xsl:value-of select="$speaker" />"</xsl:message>
  <xsl:message>content length = <xsl:value-of select="count($content)" /></xsl:message>
  <xsl:if test="$stage">
  <xsl:message>nodes before $stage = <xsl:value-of select="count($stage/preceding-sibling::*)" /></xsl:message>
  <xsl:message>nodes after $stage = <xsl:value-of select="count($stage/following-sibling::*)" /></xsl:message>
  </xsl:if>

  <xsl:if test="$stage">
    <sp by="#{$speaker}">
      <!-- process all the nodes in the $content node set before the current <stage> -->
      <xsl:message>Processing <xsl:value-of select="count($stage/preceding-sibling::*)" /> nodes before "<xsl:value-of select="$stage/text()" />"</xsl:message>
      <xsl:apply-templates select="$stage/preceding-sibling::*" />
    </sp>
    <xsl:apply-templates select="$stage" />
  </xsl:if>
  <xsl:choose>
    <xsl:when test="$stage/following-sibling::stage">
      <!-- if there's another <stage> element in the $content node set then call this template recursively -->
      <xsl:message>Call recursively with <xsl:value-of select="count($stage/following-sibling::*)" /> following nodes</xsl:message>
      <xsl:call-template name="sp-with-stage">
        <xsl:with-param name="speaker"><xsl:value-of select="$speaker" /></xsl:with-param>
        <xsl:with-param name="sp" select="$sp" />
        <!-- the $content node set for this call is all the nodes after the current <stage> -->
        <xsl:with-param name="content" select="$stage/following-sibling::*" />
      </xsl:call-template>
    </xsl:when>
    <xsl:when test="$stage/following-sibling::*">
      <!-- if there's no <stage> element in the $content node set, but there are still some elements, emit them in an <sp> element -->
      <sp by="#{$speaker}">
        <xsl:message>Processing <xsl:value-of select="count($stage/following-sibling::*)" /> trailing nodes</xsl:message>
        <xsl:apply-templates select="$stage/following-sibling::*" />
      </sp>
    </xsl:when>
  </xsl:choose>
</xsl:template>

然后调用此模板:

<xsl:template match="sp[stage]">
  <xsl:call-template name="sp-with-stage">
    <xsl:param name="speaker"><xsl:value-of select="speaker" /></xsl:param>
    <xsl:param name="sp" select="." />
  </xsl:call-template>
</xsl:template>

问题在于我使用$stage/preceding-sibling::*,我的意思是仅处理当前$content节点之前的当前$stage节点集中的节点。实际发生的是,在每次递归调用中,此$stage选择所有节点,这些节点位于当前<sp>节点之前的原始$stage/preceding-sibling::*上下文中。 。尽管递归调用每次都会获得正确的新$content节点集,并且正在从正确的$stage节点集中获取$content节点。

在上述示例XML的情况下,澄清当<stage>To Antony</stage>$stage节点且$content节点仅包含:

<l>consectetur adipisicing elit</l>
<stage>To Antony</stage>
<l>sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</l>

$stage/preceding-sibling::*表达式仍然会将所有原始<sp>的子项生成<{1}}。

我想我必须对<stage>To Antony</stage>提出一些我没有正确理解的内容。有什么建议?甚至是以完全不同的方式实现转型的任何建议?

2 个答案:

答案 0 :(得分:1)

我怀疑你做的比现在要复杂得多。看看以下样式表:

XSLT 1.0

<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:strip-space elements="*"/>

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="sp">
    <xsl:copy>
        <xsl:copy-of select="speaker"/>
        <xsl:copy-of select="l[1]"/>
    </xsl:copy>
    <xsl:apply-templates select="stage | l[position() > 1]"/>
</xsl:template>

<xsl:template match="l">
    <sp>
        <xsl:copy-of select="."/>
    </sp>
</xsl:template>

</xsl:stylesheet>

当应用于以下示例输入时:

<root>
    <sp id="sp1">
      <speaker>Julius</speaker>
      <l>Lorem ipsum dolor sit amet</l>
      <stage>Aside</stage>
      <l>consectetur adipisicing elit</l>
      <stage>To Antony</stage>
      <l>sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</l>
    </sp>
    <sp id="sp2">
      <speaker>Antony</speaker>
      <l>Nullam at dui.</l>
      <stage>Front</stage>
      <l>Nunc lobortis. </l>
    </sp>
</root>

结果是:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <sp>
      <speaker>Julius</speaker>
      <l>Lorem ipsum dolor sit amet</l>
   </sp>
   <stage>Aside</stage>
   <sp>
      <l>consectetur adipisicing elit</l>
   </sp>
   <stage>To Antony</stage>
   <sp>
      <l>sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</l>
   </sp>
   <sp>
      <speaker>Antony</speaker>
      <l>Nullam at dui.</l>
   </sp>
   <stage>Front</stage>
   <sp>
      <l>Nunc lobortis. </l>
   </sp>
</root>

答案 1 :(得分:0)

这是一个分组问题 - 您希望将每个sp内的所有元素(speakerstage除外)放在最近的stage之前(如果有的话)是一个)。 XSLT 1.0中的标准方法称为 Muenchian分组。您定义了一个密钥,给出了分组标准,然后使用generate-id技巧处理每个组中的 first 节点作为整个组的代理。

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

  <!-- group first by the parent sp and then by the nearest preceding stage.
       generate-id(emptynodeset) is the empty string by definition, so this
       is still well defined for the elements before the first stage in an sp -->
  <xsl:key name="groupKey" match="sp/*[not(self::speaker | self::stage)]" use="
     concat(generate-id(..), '|', generate-id(preceding-sibling::stage[1]))" />

  <!-- identity template - copy everything as-is unless overridden -->
  <xsl:template match="@*|node()">
    <xsl:copy><xsl:apply-templates select="@*|node()" /></xsl:copy>
  </xsl:template>

  <xsl:template match="sp">
    <!-- for each group -->
    <xsl:for-each select="*[generate-id() = generate-id(key('groupKey',
          concat(generate-id(..), '|', generate-id(preceding-sibling::stage[1]))
        )[1])]">
      <!-- the "stage" if there is one - if we are before the first stage in this
           sp then the preceding-sibling:: will select nothing -->
      <xsl:apply-templates select="preceding-sibling::stage[1]" />
      <sp by="#{../speaker}">
        <!-- the following elements up to the next stage -->
        <xsl:apply-templates select="key('groupKey',
          concat(generate-id(..), '|', generate-id(preceding-sibling::stage[1]))
        )" />
      </sp>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

这适用于您的示例输入,但如果有任何实例中您有两个连续的stage元素,则可能需要进行一些更改。