为下一个兄弟调用命名模板

Call named template for next sibling

我有以下 XML:

    <Text>
        <p id="258">Step.</p>
        <p id="1123">Step info.</p>
        <p id="258">Step.</p>
        <p id="1123">Step info.</p>
        <p id="258">Step.</p>
        <p id="1123">Step info:</p>
        <p id="1123">- Comment.</p>
        <p id="1123">- Comment.</p>
        <p id="1123">- Comment.</p>
    </Text>

我必须把它变成 DocBook <orderedlist>:

<orderedlist>
        <listitem>
            <para>Step.</para>
            <para>
                <emphasis>Step info.</emphasis>
            </para>
        </listitem>
        <listitem>
            <para>Step.</para>
            <para>
                <emphasis>Step info.</emphasis>
            </para>
        </listitem>
        <listitem>
            <para>Step.</para>
            <para>
                <emphasis>Step info:</emphasis>
            </para>
            <para>
                <emphasis>- Comment:</emphasis>
            </para>
            <para>
                <emphasis>- Comment:</emphasis>
            </para>
            <para>
                <emphasis>- Comment:</emphasis>
            </para>
        </listitem>
    </orderedlist>

我先把所有<p id="258">个元素变成<listitem><para>:

<xsl:template match="AIT:p[@id='258'][1]">
    <orderedlist>
        <xsl:for-each select="../AIT:p[@id='258']">
            <xsl:call-template name="stepNoLine"/>
        </xsl:for-each>
    </orderedlist>
</xsl:template>

<xsl:template name="stepNoLine">
    <listitem>
        <para>
            <xsl:apply-templates select="*|node()"/>
        </para>
    </listitem>
</xsl:template>

然后我删除所有 non-first 个元素:

<xsl:template match="AIT:p[@id='258'][position() > 1]"/>

目前一切顺利:

<orderedlist>
        <listitem>
            <para>Step.</para>
        </listitem>
        <listitem>
            <para>Step.</para>
        </listitem>
        <listitem>
            <para>Step.</para>
        </listitem>
    </orderedlist>

但现在我不知道如何处理 <p id="1123"> 个元素。两个 <p id="258"> 之间的所有 <p id="1123"> 必须是第一个 <p id="258"> 的兄弟姐妹,children 必须是 <listitem> 的兄弟姐妹。再次:

    <listitem>
        <para>Step.</para>
        <para>
            <emphasis>Step info.</emphasis>
        </para>
    </listitem>

我微不足道的尝试可耻地以耻辱的方式失败了:

<xsl:template name="stepNoLine">
    <listitem>
        <para>
            <xsl:apply-templates select="*|node()"/>
        </para>
        <xsl:if test="following-sibling::AIT:p/@id='1123'">
            <xsl:call-template name="stepInfo"/>
        </xsl:if>
    </listitem>
</xsl:template>

<xsl:template name="stepInfo">
    <para>
        <emphasis>
            <xsl:apply-templates select="*|node()"/>
        </emphasis>
    </para>
</xsl:template>

我得到类似的东西:

<orderedlist>
        <listitem>
            <para>Step.</para>
            <para>
                <emphasis>Step.</emphasis>
            </para>
        </listitem>
        <listitem>
            <para>Step.</para>
            <para>
                <emphasis>Step.</emphasis>
            </para>
        </listitem>
        <listitem>
            <para>Step.</para>
            <para>
                <emphasis>Step.</emphasis>
            </para>
        </listitem>
    </orderedlist>

换句话说,每个 <p id="258"> 元素被复制两次。我以为 <xsl:if> 使下一个兄弟节点成为当前节点,但我显然错了。

其他尝试(例如使用 xsl:for-each 而不是 xsl:if)以同样悲惨的方式失败。

有人能给我指出正确的方向吗?

您可以使用 XSLT 2.0 或 3.0 for-each-group group-starting-with:

<xsl:template match="Text">
    <orderedlist>
        <xsl:for-each-group select="*" group-starting-with="p[@id = 258]">
            <listitem>
                <xsl:apply-templates select="current-group()"/>
            </listitem>
        </xsl:for-each-group>
    </orderedlist>
</xsl:template>

<xsl:template match="Text/p[@id = 258]">
    <para>
        <xsl:apply-templates/>
    </para>
</xsl:template>

<xsl:template match="Text/*[not(self::p[@id = 258])]">
    <para>
        <emphasis>
            <xsl:apply-templates/>
        </emphasis>
    </para>
</xsl:template>

现在几乎所有此类问题的正确答案都是 "use XSLT 2.0",但是在只有 XSLT 1.0 可用的环境中,能够在 1.0 中解决问题也很有用。

考虑以下输入,与所示示例同构,但具有不同的字符数据,以便更容易看出发生了什么:

 <Text>
   <p id="258">First step.</p>
   <p id="1123">Step info for step 1.</p>
   <p id="258">Step two.</p>
   <p id="1123">Step info for second step.</p>
   <p id="258">Step three.</p>
   <p id="1123">Step info for third step:</p>
   <p id="1123">- Comment on step 3.</p>
   <p id="1123">- Comment 2 on step 3.</p>
   <p id="1123">- Comment 3 on step 3.</p>
 </Text>

通过此输入,可以更容易地看出 (a) 您的样式表草稿(假设我已根据您的描述正确重建了它)确实设法为每个步骤获得一个 listItem(尽管略有它使用的绕屋方法),而且 (b) 它没有匹配 Text 元素或任何 p 元素与 id="1123" 的模板,这意味着默认模板会触发,我们会在前 258 段的模板输出之后得到 1123 段字符数据的转储。

<orderedlist>
<listitem>
  <para>First step.</para>
  <para><emphasis>First step.</emphasis></para>
</listitem>
<listitem>
  <para>Step two.</para>
  <para><emphasis>Step two.</emphasis></para>
</listitem>
<listitem>
  <para>Step three.</para>
  <para><emphasis>Step three.</emphasis></para>
</listitem>
</orderedlist>
       Step info for step 1.

       Step info for second step.

       Step info for third step:
       - Comment on step 3.
       - Comment 2 on step 3.
       - Comment 3 on step 3.

代码中的直接问题是 stepNoLine 调用 stepinfo 时没有做任何改变上下文节点的事情,所以那里的模板应用程序处理 258 段落的子项,而不是下面的 1123段落。

您正在处理的输入在元素序列中嵌入了大量信息;您的拉式样式表忽略了该信息并试图从无到有地重新创建它。如果您让样式表以输入为导向,您的样式表将会更简单并且会做得更好,这在 XSLT 编程中通常被称为 'push' 样式。

在下面的 XSLT 样式表中,Text 的模板调用 xsl:apply-templates 仅为应该生成 listItem 元素的子元素,即 p 元素 id="258"。 1123 段未被该指令处理。

258 个段落元素的模板依次创建列表项的所有内容:首先是 258 个段落,然后是所有后续 p 个元素的序列 id="1123"。 (我们只将模板应用于第一个,但第一个负责整个序列。)

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

  <xsl:template match="Text">
    <xsl:element name="orderedList">
      <xsl:apply-templates select="p[@id='258']"/>
    </xsl:element>
  </xsl:template>

  <xsl:template match="p[@id='258']">
    <xsl:element name="listitem">
      <xsl:element name="para">
        <xsl:apply-templates/>
      </xsl:element>
      <xsl:apply-templates select="following-sibling::*[1]/self::p[@id='1123']"/>
    </xsl:element>
  </xsl:template>

  <xsl:template match="p[@id='1123']">
    <xsl:element name="para">
      <xsl:apply-templates/>
    </xsl:element>
    <xsl:apply-templates select="following-sibling::*[1]/self::p[@id='1123']"/>
  </xsl:template>

</xsl:stylesheet>

N.B。在 258 个元素的模板中,我们不会尝试将模板应用于所有具有 id="1123" 的适当同级元素。如果我们在 XPath 1.0 中有一种方便的方式说 "all the immediately following p elements with id="1123" up to but not including the first p with id="258", or the end of the parent element",并且确信我们做对了,我们可以,而且一切都会更简单。更简单的说法是“当且仅当它是 pid="1123" 时,然后让该元素的模板做同样的事情。当我们到达 pid="1123" 紧随其后的兄弟不是 pid="1123",或者没有后续兄弟,递归停止。

根据修改后的输入,这产生了输出:

<?xml version="1.0" encoding="UTF-8"?>
<orderedList>
  <listitem>
    <para>First step.</para>
    <para>Step info for step 1.</para>
  </listitem>
  <listitem>
    <para>Step two.</para>
    <para>Step info for second step.</para>
  </listitem>
  <listitem>
    <para>Step three.</para>
    <para>Step info for third step:</para>
    <para>- Comment on step 3.</para>
    <para>- Comment 2 on step 3.</para>
    <para>- Comment 3 on step 3.</para>
  </listitem>
</orderedList>

我认为这是必需的。 (如果要将 1123 个段落的内容包装在 emph 中,应该很容易看出在哪里做。)

<xsl:template match="Text">
    <xsl:element name="orderedlist">
        <xsl:apply-templates select="p[@id='258']"/>
    </xsl:element>
</xsl:template>

<xsl:template match="p">
    <xsl:choose>
        <xsl:when test="@id='258'">
            <xsl:element name="listitem">
                <xsl:element name="para"><xsl:apply-templates /></xsl:element>
                <xsl:apply-templates select="following-sibling::p[1][@id = '1123']"/>
            </xsl:element>
        </xsl:when>
        <xsl:otherwise>
            <xsl:element name="para">
                <xsl:element name="emphasis">
                    <xsl:apply-templates />
                </xsl:element>
            </xsl:element>
            <xsl:apply-templates select="following-sibling::p[1][@id = '1123']"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>