在 XSLT-2.0 中将 space 分隔的标记从一个属性移动到另一个属性的最佳方法是什么?

What's the best way to move space-delimited tokens from one attribute to another in XSLT-2.0?

我试图在 XSLT-2.0 中将 space 分隔的标记从一个属性移动到另一个属性。例如,给定

<!-- SOURCE DOCUMENT -->
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <p class="foo"/>
    <p class="foo bar baz"/>
    <p class="foo bar baz" outputclass="BAR"/>
    <p class="foo bar baz" outputclass="BAR HELLO"/>
</root>

我需要将@class="foo" 移动到@outputclass="FOO" 并将@class="bar" 移动到@outputclass ="BAR",如果源属性变为空则删除源属性,如果存在则增加目标属性(简单的标记集操作):

<!-- RESULTING DOCUMENT -->
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <p             outputclass="FOO"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR HELLO"/>
</root>

我想除了实际的令牌移动部分之外,我已经想通了所有事情。我走的每一个方向最终都变得复杂和破碎,我觉得 XSLT-2.0 肯定有一个我所缺少的简单方法。

这是我目前的情况:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mine="mine:local"
    exclude-result-prefixes="xs"
    version="2.0">

    <!-- baseline identity transform -->
    <!-- (for non-elements - attributes, whitespace PCDATA, etc.)  -->
    <xsl:template match="@*|(node() except *)">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- for element nodes, remap attributes then copy element -->
    <xsl:template match="*">
        <!-- get original attribute sequence -->
        <xsl:variable name="atts1" select="@*"/>

        <!-- use our function to remap two attribute tokens -->
        <xsl:variable name="atts2" select="mine:remap($atts1, 'class', 'foo', 'outputclass', 'FOO')"/>
        <xsl:variable name="atts3" select="mine:remap($atts2, 'class', 'bar', 'outputclass', 'BAR')"/>

        <!-- stuff updated attribute sequence into element -->
        <xsl:copy>
            <xsl:sequence select="$atts3"/>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- remap  @from_att~="$from_token"  to  @to_att~="$to_token" -->
    <xsl:function name="mine:remap">
        <xsl:param name="orig_atts"/>
        <xsl:param name="from_att"/>
        <xsl:param name="from_token"/>
        <xsl:param name="to_att"/>
        <xsl:param name="to_token"/>

        <!-- ******** TOKEN-MOVING MAGIC!?! ******** -->

        <xsl:sequence select="$orig_atts"/>
    </xsl:function>
</xsl:stylesheet>

基本上我需要弄清楚 TOKEN-MOVING MAGIC!?! 如何移动单个标记(包括删除空的“from”属性)。我已经搜索了很多,但我还没有看到这个特定的问题。

编辑:要重新映射的属性的数量和名称可以是任何内容,并且它们的值区分大小写。这是 mine:remap 函数内部的魔法,可以重新映射我正在寻找的属性序列中的单个值。

编辑:使用函数来处理属性修改的原因是我们有许多不同的标记重新映射可以应用于不同的文件,我希望让我们不熟悉 XSLT 的用户能够轻松地将重新映射调整为他们的需要。我无法弄清楚如何使用基于模板匹配的方法提供类似的概括。

谢谢!

在以下示例中,我尝试将尽可能多的委托给模板:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">
    
  <xsl:param name="tokens" as="xs:string*"
    select="'foo', 'bar'"/>
    
  <xsl:param name="collation" as="xs:string">http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive</xsl:param>

  <xsl:mode on-no-match="shallow-copy"/>

  <xsl:template match="*[@class][exists($tokens[contains-token(current()/@class, ., $collation)])]">
      <xsl:copy>
          <xsl:variable name="new-att" as="attribute()">
              <xsl:attribute name="outputclass"/>
          </xsl:variable>
          <xsl:apply-templates select="@*, $new-att[not(current()/@outputclass)]">
              <xsl:with-param name="tokens-found" 
                select="$tokens[contains-token(current()/@class, ., $collation)]"/>
          </xsl:apply-templates>
          <xsl:apply-templates/>
      </xsl:copy>
  </xsl:template>
  
  <xsl:template match="@class">
      <xsl:param name="tokens-found"/>
      <xsl:variable name="remaining-tokens" select="tokenize(., ' ')[not(. = $tokens-found)]"/>
      <xsl:if test="exists($remaining-tokens)">
          <xsl:attribute name="{name()}" select="$remaining-tokens"/>
      </xsl:if>
  </xsl:template>
  
  <xsl:template match="@outputclass">
      <xsl:param name="tokens-found"/>
      <xsl:variable name="new-tokens" select="$tokens-found[not(contains-token(current(), ., $collation))]"/>
      <xsl:attribute name="{name()}" select="$new-tokens, ."/>
  </xsl:template>
  
</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bEzkTcx/1

我没有实现要移动的令牌的大写转换,我想应该很容易添加。

该代码将 XSLT 3 与 XPath 3 和函数 https://www.w3.org/TR/xpath-functions/#func-contains-token 结合使用,但它在规范中有一个定义,您可以在用户定义的 XSLT 2 函数中使用该定义。当然,不使用 xsl:mode 而是通过拼写来声明身份转换也很容易。

XSLT 3 适用于 Java 和 .NET 的 Saxon 9.8 或更高版本,适用于 C/C++ 的 Saxon-C,以及适用于 PHP 和 [=28= 的绑定] 以及现代网络浏览器中的 Saxon-JS 2 和 Node.js.

下面是我为 mine:remap() 函数得到的结果:

<!-- remap  @from_att~="$from_token"  to  @to_att~="$to_token" -->
<xsl:function name="mine:remap">
    <xsl:param name="orig_atts" as="attribute()*"/>
    <xsl:param name="from_att"/>
    <xsl:param name="from_token"/>
    <xsl:param name="to_att"/>
    <xsl:param name="to_token"/>

    <!-- get tokenized list of values of "from" attributes -->
    <xsl:variable name="from_att_values" select="tokenize($orig_atts[name() = $from_att], ' ')"/>

    <xsl:choose>
        <!-- does the "from" attribute contain our value to replace? -->
        <xsl:when test="$from_att_values = $from_token">

            <!-- if so, iterate through attributes to preserve their order -->
            <xsl:for-each select="$orig_atts">
                <xsl:choose>
                    <!-- if "from" and "to" attributes are the same, replace $from_token with $to_token in-place -->
                    <xsl:when test="(name(.) = $from_att) and ($from_att = $to_att)">
                        <xsl:attribute name="{name(.)}" select="for $t in $from_att_values
                            return ($t[$t != $from_token], $to_token[$t = $from_token])"/>                        
                    </xsl:when>
                    <!-- if "from" attribute, define with $from_token value removed -->
                    <xsl:when test="name(.) = $from_att">
                        <xsl:variable name="new_from_att_values" select="$from_att_values[not(. = $from_token)]"/>
                        <xsl:if test="count($new_from_att_values) > 0">
                            <xsl:attribute name="{$from_att}" select="$new_from_att_values"/>
                        </xsl:if>
                    </xsl:when>
                    <!-- if "to" attribute, define with $to_token value added -->
                    <xsl:when test="name(.) = $to_att">
                        <xsl:attribute name="{$to_att}" select="distinct-values((tokenize(., ' '), $to_token))"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:copy/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>
            <!-- if there was no "from" attribute to modify above, create it here -->
            <xsl:if test="not($orig_atts[name() = $to_att])">
                <xsl:attribute name="{$to_att}" select="$to_token"/>
            </xsl:if>
        </xsl:when>

        <!-- if not, return original attributes -->
        <xsl:otherwise>
            <xsl:sequence select="$orig_atts"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:function>

我遍历属性以保留它们的顺序,然后我使用 xsl:choose 来处理 from(删除标记)、to(添加标记)或其他(复制)属性。

这是一个简短的 XSLT 2.0 解决方案(仅 26 行):

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

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="p/@class[tokenize(., ' ') = ('foo', 'bar')]">
    <xsl:if test="tokenize(., ' ')[not(. = ('foo', 'bar'))]">
        <xsl:attribute name="class" 
             select="string-join(tokenize(., ' ')[not(. = ('foo', 'bar'))], ' ')"/>
    </xsl:if>
    <xsl:attribute name="outputclass" select=
      "upper-case(string-join(
                   (
                    tokenize(., ' ')[. = ('foo', 'bar')],
                    tokenize(../@outputclass, ' ')
                                 [not(lower-case(.) = tokenize(current(), ' '))]
                    ),
                    ' '
                              )
                  )"/>
  </xsl:template>
  
  <xsl:template match="p/@outputclass[../@class[tokenize(., ' ') = ('foo', 'bar')]]"/>
</xsl:stylesheet>

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

<root>
    <p class="foo"/>
    <p class="foo bar baz"/>
    <p class="foo bar baz" outputclass="BAR"/>
    <p class="foo bar baz" outputclass="BAR HELLO"/>
</root>

产生了想要的、正确的结果:

<root>
    <p outputclass="FOO"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR HELLO"/>
</root>

更新:

这是几乎所有参数化的相同转换,正如 OP 在评论中所要求的,只有 32 行:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:param name="pfromName" select="'class'"/>
 <xsl:param name="ptoName" select="'outputclass'"/>
 <xsl:param name="pTokens" select="'foo', 'bar'"/>
 <xsl:param name="pnewNames" select="'FOO', 'BAR'"/>

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

  <xsl:template match="p/@*[name() = $pfromName][tokenize(., ' ') = $pTokens]">
    <xsl:if test="tokenize(., ' ')[not(. = $pTokens)]">
        <xsl:attribute name="{$pfromName}"
             select="string-join(tokenize(., ' ')[not(. = $pTokens)], ' ')"/>
    </xsl:if>
    <xsl:attribute name="{$ptoName}" select=
      "upper-case(string-join(
                   (
                    tokenize(., ' ')[. = $pTokens],
                    tokenize(../@*[name()=$ptoName], ' ')
                                 [not(lower-case(.) = tokenize(current(), ' '))]
                    ),
                    ' '
                              )
                  )"/>
  </xsl:template>

  <xsl:template 
    match="p/@*[name()=$ptoName][../@*[name()=$pfromName][tokenize(., ' ') = $pTokens]]"/>
</xsl:stylesheet>

更新2:

这是一个完全参数化的 XSLT 2.0 转换(不使用 upper-case()lower-case() 函数),只有 37 行:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:param name="pfromName" select="'class'"/>
 <xsl:param name="ptoName" select="'outputclass'"/>
 <xsl:param name="pTokens" select="'foo', 'bar'"/>
 <xsl:param name="pnewNames" select="'FOO', 'BAR'"/>

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

  <xsl:template match="p/@*[name() = $pfromName][tokenize(., ' ') = $pTokens]">
    <xsl:if test="tokenize(., ' ')[not(. = $pTokens)]">
        <xsl:attribute name="{$pfromName}"
             select="string-join(tokenize(., ' ')[not(. = $pTokens)], ' ')"/>
    </xsl:if>
    <xsl:attribute name="{$ptoName}" select=
      "string-join(
                   distinct-values(
                            (for $token in tokenize(., ' ')[. = $pTokens],
                                    $n in 1 to count($pTokens),
                                    $ind in $n[$token eq $pTokens[$n]]
                                  return $pnewNames[$ind]
                             ,
                              tokenize(../@*[name()=$ptoName], ' ')
                              )
                                    ),
                    ' '
                    )
                  "/>
  </xsl:template>

  <xsl:template
  match="p/@*[name()=$ptoName][../@*[name()=$pfromName][tokenize(., ' ') = $pTokens]]"/>
</xsl:stylesheet>