使用 XSL 将引号转换为斜引号

Transform quotes to angle quotes using XSL

我正在寻找一种使用 XSL 将 XML 文件中的引号自动转换为斜引号的方法。

示例XML:

<root>
  <p>There "are" quotes</p>
  <p>Here "are quotes <b>too</b>"</p>
</root>

这应该转换为:

<root>
  <p>There »are« quotes</p>
  <p>Here »are quotes <b>too</b>«</p>
</root>

这在 XSL 中可行吗?另请注意,起始引语不需要与结束引语位于同一标签中。

对于连续的strings/text个节点,这个递归函数有效:

<xsl:template name="quote">
  <xsl:param name="text" select="." />
  <xsl:param name="old" select="'&quot;&quot;'" />
  <xsl:param name="new" select="'»«'" />
  <xsl:param name="state" select="0" />

  <xsl:variable name="o" select="substring($old, $state + 1, 1)" />
  <xsl:variable name="n" select="substring($new, $state + 1, 1)" />

  <xsl:choose>
    <xsl:when test="not($o and contains($text, $o))">
      <xsl:value-of select="$text" />
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="substring-before($text, $o)" />
      <xsl:value-of select="$n" /> 
      <xsl:call-template name="quote">
        <xsl:with-param name="text" select="substring-after($text, $o)" />
        <xsl:with-param name="old" select="$old" />
        <xsl:with-param name="new" select="$new" />
        <xsl:with-param name="state" select="($state + 1) mod 2" />
      </xsl:call-template>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

如果您提供 $old$new 参数,它们应该是长度为 2 的字符串,分别包含开始和结束引号的字符。

所有参数使用默认值的示例:

<xsl:template match="text()">
    <xsl:call-template name="quote" />
</xsl:template>

如果有问题的文本节点是嵌套结构的一部分 (<p>Here "are quotes <b>too</b>"</p>),事情会稍微复杂一些。

要在非连续文本节点上实现不对称引用(不同的开始和结束引号),我们需要做一些假设:

  • 我们将使用引用计数,即如果一个引用前面有偶数个引用(0、2、4 ...),我们假设它是一个开场白,否则我们假设一个收盘价。
  • 我们假设所有相关引用都发生在同级级别(没有 "incorrect nesting",例如 text "text <x>"</x>,其中结束引号位于不同的嵌套级别)。
  • 输入的引号必须正确,否则我们的计数将被取消。
  • 为了获得最大的兼容性,我将采用普通的 XSLT 1.0 处理器。

首先我们需要一个函数来计算输入文本中的字符数。我们将使用它作为我们引用计数方法的基础。这很容易;作为一个小复杂功能,我们将其设计为能够计算多个不同的字符:

<xsl:template name="count-chars">
  <xsl:param name="input" select="." />
  <xsl:param name="chars" select="$input" />

  <xsl:value-of select="
    string-length($input) - string-length(translate($input, $chars, ''))
  " />
</xsl:template>

当使用 $input = "input A input B"$chars = "AB" 调用时,它会 return 2. 在没有任何参数的情况下调用它只会 return 输入的总字符串长度(默认到当前节点)。

接下来我们需要一个能够计算一组节点中的字符的模板。这基本上是通过迭代节点的输入集并在每个节点上调用 count-chars 来工作的。同样,这是递归的,以便能够计算总计:

<xsl:template name="count-chars-mutiple">
  <xsl:param name="nodes" />
  <xsl:param name="chars" />

  <xsl:choose>
    <xsl:when test="not($chars and count($nodes))">
      <xsl:value-of select="0" />
    </xsl:when>
    <xsl:otherwise>
      <xsl:variable name="c">
        <xsl:call-template name="count-chars">
          <xsl:with-param name="input" select="$nodes[1]" />
          <xsl:with-param name="chars" select="$chars" />
        </xsl:call-template>
      </xsl:variable>
      <xsl:variable name="d">
        <xsl:call-template name="count-chars-mutiple">
          <xsl:with-param name="nodes" select="$nodes[position() &gt; 1]" />
          <xsl:with-param name="chars" select="$chars" />
        </xsl:call-template>
      </xsl:variable>
      <xsl:value-of select="$c + $d" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

这很简单。

当用 $nodes = ["input A input B", "input A input C"]$chars = "AB" 调用时,它将 return 3.

设置支持函数后,我们现在可以从 post 的开头修改函数,以便能够从引用计数中获取其上下文。

为此,我们将计算所有前同级文本节点中的引号,并根据该计数加上手头文本节点中的引号数来决定使用哪个引号。

例如:

<p>Here "<i>are</i> quotes <b>too</b>", and "here"</p>
   -----^          --------          ~~~~~~~~~~~~~
        1                            2      3    4

当我们在文本最后一个文本节点 (~) 时,带下划线的文本节点被考虑在内,其中第一个包含一个引号 (1),因此我们知道引号 (2) 是结束引号。 (3) 和 (4) 就像在我的原始函数中一样处理(即通过递归):

  <xsl:template name="quote">
    <xsl:param name="text" select="." />
    <xsl:param name="old" select="'&quot;&quot;'" />
    <xsl:param name="new" select="'»«'" />
    <xsl:param name="context">
      <xsl:call-template name="count-chars-mutiple">
        <xsl:with-param name="nodes" select="preceding-sibling::text()" />
        <xsl:with-param name="chars" select="$old" />
      </xsl:call-template>
    </xsl:param>

    <xsl:variable name="state" select="($context mod 2) + 1" />
    <xsl:variable name="o" select="substring($old, $state, 1)" />
    <xsl:variable name="n" select="substring($new, $state, 1)" />

    <xsl:choose>
      <xsl:when test="not($o and contains($text, $o))">
        <xsl:value-of select="$text" />
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="substring-before($text, $o)" />
        <xsl:value-of select="$n" />
        <xsl:call-template name="quote">
          <xsl:with-param name="text" select="substring-after($text, $o)" />
          <xsl:with-param name="old" select="$old" />
          <xsl:with-param name="new" select="$new" />
          <xsl:with-param name="context" select="$context + 1" />
        </xsl:call-template>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

$state 最终为 1 或 2,因此我们可以从 $new 参数中选择开盘价或收盘价。 $context 默认为相应的前面的引用计数,并在下一个递归步骤中简单地增加。

我知道这不是很漂亮,但是当放在一起时它会将您的输入转换为:

<root>
  <p>There »are« quotes</p>
  <p>Here »are quotes <b>too</b>«</p>
</root>

<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="xml" indent="yes" />

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

  <xsl:template match="text()">
      <xsl:call-template name="quote" />
  </xsl:template>

  <xsl:template name="quote">
    <xsl:param name="text" select="." />
    <xsl:param name="old" select="'&quot;&quot;'" />
    <xsl:param name="new" select="'»«'" />
    <xsl:param name="context">
      <xsl:call-template name="count-chars-mutiple">
        <xsl:with-param name="nodes" select="preceding-sibling::text()" />
        <xsl:with-param name="chars" select="$old" />
      </xsl:call-template>
    </xsl:param>

    <xsl:variable name="state" select="($context mod 2) + 1" />
    <xsl:variable name="o" select="substring($old, $state, 1)" />
    <xsl:variable name="n" select="substring($new, $state, 1)" />
    
    <xsl:choose>
      <xsl:when test="not($o and contains($text, $o))">
        <xsl:value-of select="$text" />
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="substring-before($text, $o)" />
        <xsl:value-of select="$n" />
        <xsl:call-template name="quote">
          <xsl:with-param name="text" select="substring-after($text, $o)" />
          <xsl:with-param name="old" select="$old" />
          <xsl:with-param name="new" select="$new" />
          <xsl:with-param name="context" select="$context + 1" />
        </xsl:call-template>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template name="count-chars">
    <xsl:param name="input" select="." />
    <xsl:param name="chars" select="$input" />
  
    <xsl:value-of select="
      string-length($input) - string-length(translate($input, $chars, ''))
    " />
  </xsl:template>

  <xsl:template name="count-chars-mutiple">
    <xsl:param name="nodes" />
    <xsl:param name="chars" />

    <xsl:choose>
      <xsl:when test="not($chars and count($nodes))">
        <xsl:value-of select="0" />
      </xsl:when>
      <xsl:otherwise>
        <xsl:variable name="c">
          <xsl:call-template name="count-chars">
            <xsl:with-param name="input" select="$nodes[1]" />
            <xsl:with-param name="chars" select="$chars" />
          </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="d">
          <xsl:call-template name="count-chars-mutiple">
            <xsl:with-param name="nodes" select="$nodes[position() &gt; 1]" />
            <xsl:with-param name="chars" select="$chars" />
          </xsl:call-template>
        </xsl:variable>
        <xsl:value-of select="$c + $d" />
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  
</xsl:transform>

旁注:<xsl:param> 允许引用先前在同一函数中声明的参数值,以便参数可以动态计算自己的默认值。

我会尝试做一些像文本处理器那样的事情来设置排版正确的引号:如果后面有一个字符,我们就有了开头引号。如果一个字符在前面,我们有一个结束字符。

在下面的 XSLT 中,我展示了一个解决方案,我只是在引号前后寻找 white-space。它不是适用于所有情况的解决方案(想想标点符号或类似的东西)并且它不匹配人们能想到的所有情况,但它可能对您的用例有所帮助 - 至少您的示例打印得很好:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="2.0">

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

  <xsl:template match="text()">
    <xsl:variable name="this" select="."/>

    <!-- the easy ones: -->
    <xsl:variable name="this" select="replace($this,'&quot;([^\s])','»')"/>
    <xsl:variable name="this" select="replace($this,'([^\s])&quot;','«')"/>

    <!-- now, try handling &quot; at the beginning/end of text() -->
    <xsl:variable name="this">  
        <xsl:choose>
          <xsl:when test="matches($this,'^&quot;') and matches(preceding-sibling::*[1]/text(),'[^\s]$')">
            <xsl:value-of select="replace($this,'^&quot;','«')"/>
          </xsl:when>
          <xsl:when test="matches($this,'&quot;$') and matches(following-sibling::*[1]/text(),'^[^\s]')">
            <xsl:value-of select="replace($this,'^&quot;','»')"/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="$this"/>
          </xsl:otherwise>
        </xsl:choose>
    </xsl:variable>


    <xsl:value-of select="$this"/>
  </xsl:template>

</xsl:stylesheet>

解决方案 1:

这是一个转换,它比 Tomalak 的(非常好的)解决方案少了 20% 的模板(4 对 5),代码少了 19%(68(或Sol.2 中的 63)对比 81 行)。没有使用 <xsl:choose><xsl:when><xsl:otherwise>translate()。任何模板的最大参数数为 3,而在解决方案 2 中,最大参数数为 2(对 4)。简单来说,我相信这2个转换更简单:

<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:variable name="vQ">"</xsl:variable>
 <xsl:variable name="vShevrons" select="'»«'"/>

 <xsl:variable name="vReplacedText">
   <xsl:call-template name="replQuotes"/>
 </xsl:variable>

 <xsl:variable name="vNodeOffsets">
   <xsl:call-template name="getTextOffsets"/>
   <xsl:text>|</xsl:text>
 </xsl:variable>

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

  <xsl:template name="replQuotes">
    <xsl:param name="pText" select="."/>
    <xsl:param name="pOddness" select="0"/>

    <xsl:value-of select='substring-before(concat($pText, $vQ), $vQ)'/>

    <xsl:variable name="vRemaining" select="substring-after($pText, $vQ)"/>

    <xsl:if test="contains($pText, $vQ)">
      <xsl:variable name="vShevInd" select="$pOddness + 1"/>
      <xsl:value-of select="substring($vShevrons, $vShevInd, 1)"/>

      <xsl:call-template name="replQuotes">
         <xsl:with-param name="pText" select="$vRemaining"/>
         <xsl:with-param name="pOddness" select="$vShevInd mod 2"/> 
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="text()[true()]">
    <xsl:variable name="vInd" select="count(preceding::text()) +1"/>
    <xsl:variable name="vStartMarker" select="concat('|N', $vInd, '|')"/>
    <xsl:variable name="vOffset" select="substring-before(substring-after($vNodeOffsets, $vStartMarker), '|')"/>

    <xsl:value-of select="substring($vReplacedText, $vOffset, string-length())"/>
  </xsl:template>

  <xsl:template name="getTextOffsets">
    <xsl:param name="pNodes" select="//text()"/>
    <xsl:param name="pNodeInd" select="1"/>
    <xsl:param name="pAccumLength" select="0"/>

    <xsl:if test="$pNodes">
        <xsl:variable name="vNodeLength" select="string-length($pNodes[1])"/>
        <xsl:value-of select="concat('|N', $pNodeInd, '|')"/>
      <xsl:variable name="vNewAccum" select="$pAccumLength+$vNodeLength"/>
      <xsl:value-of select="$pAccumLength+1"/>

        <xsl:call-template name="getTextOffsets">
          <xsl:with-param name="pNodes" select="$pNodes[position() > 1]"/>
        <xsl:with-param name="pNodeInd" select="$pNodeInd+1"/>
        <xsl:with-param name="pAccumLength" select="$vNewAccum"/>
        </xsl:call-template>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

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

<root>
  <p>There "are" quotes</p>
  <p>Here "are quotes <b>too</b>"</p>
</root>

产生想要的正确结果:

<root>
   <p>There »are« quotes</p>
   <p>Here »are quotes <b>too</b>«</p>
</root>

解决方案 2: 如果使用的 XSLT 1.0 处理器实现了 xxx:node-set() 扩展函数,则存在更简单和更短的解决方案:

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

 <xsl:variable name="vQ">"</xsl:variable>
 <xsl:variable name="vShevrons" select="'»«'"/>

 <xsl:variable name="vReplacedText">
   <xsl:call-template name="replQuotes"/>
 </xsl:variable>

 <xsl:variable name="vrtfChunks">
   <xsl:call-template name="getTextChunks"/>
 </xsl:variable>

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

  <xsl:template name="replQuotes">
    <xsl:param name="pText" select="."/>
    <xsl:param name="pOddness" select="0"/>

    <xsl:value-of select='substring-before(concat($pText, $vQ), $vQ)'/>

    <xsl:variable name="vRemaining" select="substring-after($pText, $vQ)"/>

    <xsl:if test="contains($pText, $vQ)">
      <xsl:variable name="vShevInd" select="$pOddness + 1"/>
      <xsl:value-of select="substring($vShevrons, $vShevInd, 1)"/>

      <xsl:call-template name="replQuotes">
         <xsl:with-param name="pText" select="$vRemaining"/>
         <xsl:with-param name="pOddness" select="$vShevInd mod 2"/> 
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="text()[true()]">
    <xsl:variable name="vInd" select="count(preceding::text()) +1"/>
    <xsl:value-of select="ext:node-set($vrtfChunks)/*[position()=$vInd]"/>
  </xsl:template>

  <xsl:template name="getTextChunks">
    <xsl:param name="pNodes" select="//text()"/>
    <xsl:param name="pTextOffset" select="1"/>

    <xsl:if test="$pNodes">
        <xsl:variable name="vNodeLength" select="string-length($pNodes[1])"/>
      <chunk>
        <xsl:value-of select="substring($vReplacedText, $pTextOffset, $vNodeLength)"/>
      </chunk>

      <xsl:call-template name="getTextChunks">
        <xsl:with-param name="pNodes" select="$pNodes[position() > 1]"/>
        <xsl:with-param name="pTextOffset" select="$pTextOffset + $vNodeLength"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

重要说明:如果文档的文本节点并非都是兄弟节点,则这两种解决方案都会产生正确的结果。请注意,在这种情况下,Tomalak 的转换不会产生正确的结果。

让我们获取此来源 XML 文档:

<root>
  <p>There "are" quotes</p>
  <p>Here "are quotes <b>too " "yes?</b>"</p>
</root>

上面的解决方案 1 和解决方案 2 都产生了正确的结果:

<root>
   <p>There »are« quotes</p>
   <p>Here »are quotes <b>too « »yes?</b>«</p>
</root>

Tomalak 答案的转换产生了这个不正确的结果:

<?xml version="1.0" encoding="utf-8"?>
<root>

   <p>There »are« quotes</p>

   <p>Here »are quotes <b>too » «yes?</b>«</p>

</root>