【问题标题】:Transform quotes to angle quotes using XSL使用 XSL 将引号转换为角度引号
【发布时间】:2015-04-07 13:52:24
【问题描述】:

我正在寻找一种使用 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 中可行吗?也请注意,起始引号不需要与结束引号在同一个标​​签中。

【问题讨论】:

  • 您想查看多远是否有限制,或者您是否只想用» 替换每个奇数引用整个文档 和每个偶数引用与«?请注意,您选择以任何方式查看此内容,您都会被 "Yes, we have 5" nails", he said. 绊倒
  • 理论上没有限制。在真实文档中,开始和结束引用可能会在某个标签内(不是在作为该标签的直接子节点的文本节点中,而是在标签内的某处)。第三个引用会导致错误,但这对数据生成来说是个问题。
  • 请告诉我们您使用的是什么 XSLT 处理器。
  • 目前我使用的是 Xalan 2.7.1。但如果需要,我可以更改。

标签: xml xslt quotes


【解决方案1】:

对于连续的字符串/文本节点,这个递归函数有效:

<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>

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

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

  • 我们将使用引号计数,即如果一个引号前面有偶数个引号(0、2、4 ...),我们假设它是一个开始引号,否则我们假设一个结束引号.
  • 我们将假设所有相关引用都发生在同级级别(没有“不正确的嵌套”,例如 text "text &lt;x&gt;"&lt;/x&gt;,其中结束引号位于不同的嵌套级别)。
  • 输入中的引号必须正确,否则我们的计数将被取消。
  • 为了获得最大的兼容性,我将假设使用 vanilla 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" 调用时,它将返回 2。不带任何参数调用时,它只会返回输入的总字符串长度(默认为当前节点)。

接下来,我们需要一个能够跨一组节点计算字符数的模板。这基本上是通过迭代输入节点集并在每个节点上调用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" 调用时,它将返回 3。

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

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

例如:

<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>

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

【讨论】:

  • 感谢您的回复,但遗憾的是我需要“更复杂”的案例。所以不使用 XSL 而是使用某种编程语言这样做可能有意义吗?
  • 我正在处理“更复杂”的案例。给我一分钟。
  • 太棒了。请考虑我在最初问题中的评论。开始和结束引用将在某种标记内(不是直接子级,而是在某个标记内)。也许这会有所帮助:)
  • @Werzi2001 好了,vanilla XSLT 1.0 解决方案,与您使用的任何东西都兼容。由于您的 XSLT 处理器支持the EXSLT extensions,我敢打赌,您可以找到使这更高效、更简洁的方法。但就目前而言,它有效。
  • 非常感谢您的解决方案。为我工作。
【解决方案2】:

我会尝试像 Text-Processors 那样设置印刷正确的引号:如果后面有一个字符,我们有开头的引号。如果一个字符在前面,我们有一个结束。

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

<?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])','»$1')"/>
    <xsl:variable name="this" select="replace($this,'([^\s])&quot;','$1«')"/>

    <!-- 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>

【讨论】:

  • “如果后面有一个字符,我们有开引号。如果一个字符在前面,我们有一个结束引号。” - 除了这种情况:@ 987654322@ 或其他一千种配置。
  • 是的,当然!我试图展示我的想法的原理:您可以轻松地在正则表达式中添加括号、标点符号等。这个问题并不容易,我认为你只能接近完美的解决方案,你永远也达不到。
  • 是的。我目前正在研究的解决方案自己做出假设,也不是很漂亮。 :|
【解决方案3】:

解决方案 1

这是一个转换,与 Tomalak 的(非常好的)解决方案相比,模板减少了 20%(4 对 5),代码减少了 19%(68(或 Sol.2 中的 63) ) 与 81 行)。不使用 &lt;xsl:choose&gt;&lt;xsl:when&gt;&lt;xsl:otherwise&gt;translate()。任何模板的最大参数数量为 3,在解决方案 2 中,此最大参数数量为 2(vs.4)。简单来说,我相信这两种转换更简单

<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>

【讨论】:

  • 我将花一些时间来分析这个。请注意,我的解决方案中的“兄弟级别”限制是经过深思熟虑的。我想要一些方法来限制引用计数的范围,兄弟级别是最简单的,其他范围也是可能的。另外我不确定为什么避免&lt;xsl:choose&gt;, &lt;xsl:when&gt;, &lt;xsl:otherwise&gt; 是一个优势。通过这种方式可以节省几行代码,但 XSLT 无论如何都不适合代码高尔夫,所以行计数纯粹是学术性的。
  • @Tomalak,DRY 原则提倡尽可能避免条件指令。它们增加了代码的复杂性,使其更难以理解、阅读和维护。您可以在我关于 XSLT 2.0 和 1.0 的 Pluralsight 课程中阅读有关避免/消除 XSLT 条件指令的更多信息
猜你喜欢
  • 2011-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-20
  • 2016-09-23
  • 2018-01-07
相关资源
最近更新 更多