使用 xslt 转换 xml 以对保留注释的节点进行分组

Transform xml with xslt to group nodes preserving comments

我有一个非常简单的 xml,我想用 xmlstarlet 重新排列它。

示例:

<myXml description="example 1">

    <!-- Comment XXX -->
    <randomNodeX>
        <randomSubNode1>value1</randomSubNode1>
        <randomSubNode2>value2</randomSubNode2>
    </randomNodeX>

    <!-- Comment YYY1 -->
    <!-- Comment YYY2 -->
    <randomNodeY attribute1="value3" attribute2="value4"/> 

    <!-- Comment ZZZ -->
    <randomNodeZ attribute1="value5" attribute0="value6">
        <randomSubNode3 attribute3="value7" attribute4="value8"/>
    </randomNodeZ>

    <!-- Comment for node1 first occurrence -->
    <node1 attribute1="value9" attribute5="value10" attribute6="value11"/>

    <!-- Comment for node2 first occurrence -->
    <node2 attribute1="value12" attribute7="value13" attribute8="value14">
        <subNode21 attributeX="value15"/>
        <subNode22 attributeY="value16" attributeZ="value17"/>
    </node2>

    <!-- Comment for node3 first occurrence -->
    <node3 attribute1="value18" attribute9="value19">
        <subNode31 attributeW="value20"/>
    </node3>

    <!-- Comment for node1 second occurrence -->
    <node1 attribute1="value21" attribute5="value22" attribute6="value23"/>

    <!-- Comment for node3 second occurrence -->
    <node3 attribute1="value24" attribute9="value25">
        <subNode31 attributeW="value26"/>
    </node3>

    <!-- Comment for node2 second occurrence -->
    <node2 attribute1="value27" attribute7="value28" attribute8="value29">
        <subNode21 attributeX="value30"/>
        <subNode22 attributeY="value31" attributeZ="value32"/>
    </node2>
</myXml>

我想重新排列 xml,以便所有 node1、node2 和 node3 元素与其各自的注释一起出现。此外,我想保留文档的其余部分和注释 而不必知道存在哪些标签 。我的意思是,除了 node1、node2 和 node3 之外,xml 中还可以有其他标记,我想将它们保留在文档的开头(包括注释)。

预期结果:

<myXml description="example 1">

  <!-- Comment XXX -->
  <randomNodeX>
    <randomSubNode1>value1</randomSubNode1>
    <randomSubNode2>value2</randomSubNode2>
  </randomNodeX>

  <!-- Comment YYY1 -->
  <!-- Comment YYY2 -->
  <randomNodeY attribute1="value3" attribute2="value4"/>

  <!-- Comment ZZZ -->
  <randomNodeZ attribute1="value5" attribute0="value6">
    <randomSubNode3 attribute3="value7" attribute4="value8"/>
  </randomNodeZ>

  <!-- Comment for node1 first occurrence -->
  <node1 attribute1="value9" attribute5="value10" attribute6="value11"/>

  <!-- Comment for node1 second occurrence -->
  <node1 attribute1="value21" attribute5="value22" attribute6="value23"/>

  <!-- Comment for node2 first occurrence -->
  <node2 attribute1="value12" attribute7="value13" attribute8="value14">
    <subNode21 attributeX="value15"/>
    <subNode22 attributeY="value16" attributeZ="value17"/>
  </node2>

  <!-- Comment for node2 second occurrence -->
  <node2 attribute1="value27" attribute7="value28" attribute8="value29">
    <subNode21 attributeX="value30"/>
    <subNode22 attributeY="value31" attributeZ="value32"/>
  </node2>

  <!-- Comment for node3 first occurrence -->
  <node3 attribute1="value18" attribute9="value19">
    <subNode31 attributeW="value20"/>
  </node3>

  <!-- Comment for node3 second occurrence -->
  <node3 attribute1="value24" attribute9="value25">
    <subNode31 attributeW="value26"/>
  </node3>
</myXml>

现在我已经使用这个样式表完成了它:

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

    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()[not(self::node1|self::node2|self::node3|self::comment())]"/>
            <xsl:apply-templates select="node1"/>
            <xsl:apply-templates select="node2"/>
            <xsl:apply-templates select="node3"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="randomNodeX|randomNodeY|randomNodeZ|node1|node2|node3">
        <xsl:apply-templates select="preceding-sibling::comment()[generate-id(following-sibling::*[1])=generate-id(current())]"/>
        <xsl:copy-of select="."/>
    </xsl:template>

</xsl:stylesheet>

问题是我必须指定 xml.

中存在的所有随机标签(randomNodeX、randomNodeY、...)

有没有办法在不知道节点 1、节点 2 和节点 3 之外的标签的情况下做到这一点???

它是一种在 XPath 2 中可用作 except 运算符的 except 操作,在 XSLT 1 的 XPath 1 中可能表示为

  <xsl:template match="/*">
    <xsl:copy>
      <xsl:variable name="nodes" select="node1 | node2 | node3"/>
      <xsl:variable name="trailers" select="$nodes | $nodes/preceding-sibling::comment()[1]"/>
      <xsl:apply-templates select="node()[count(. | $trailers) > count($trailers)]"/>
      <xsl:apply-templates select="$trailers"/>
    </xsl:copy>
  </xsl:template>

这假设您的所有 node1node2node3 元素都恰好有一个前置同级注释节点。

不过,我不太确定您为什么不直接使用 match="/*/*" 而不是 match="randomNodeX|randomNodeY|randomNodeZ|node1|node2|node3"

XSLT 2.0+ 解决方案:

我将从一个分组操作开始,该操作将元素及其“关联”注释分组,以便

  <!-- Comment ZZZ -->
  <randomNodeZ attribute1="value5" attribute0="value6">
    <randomSubNode3 attribute3="value7" attribute4="value8"/>
  </randomNodeZ>

变成

  <group>
    <!-- Comment ZZZ -->
    <randomNodeZ attribute1="value5" attribute0="value6">
      <randomSubNode3 attribute3="value7" attribute4="value8"/>
    </randomNodeZ>
  </group>

然后在第 2 阶段,根据包含的元素名称对组进行分组(同时删除 group 包装器)。

在您的示例中,每个元素前面都有一个或多个“关联”注释,但我们可以相信情况总是如此吗?为了更加容忍输入变化,我们可以说一个组从任何评论或没有紧跟评论的元素开始。如果我们假设 whitespace 已使用 xsl:strip-space 剥离,我们可以使用

进行第一个分组
<xsl:for-each-group select="child::node()"
 group-starting-with="(comment()|*)[not(preceding-sibling::*[1][self::comment()]">
  <group><xsl:copy-of select="current-group()"/></group>
</xsl:for-each-group>

第二个是

   <xsl:for-each-group select="group" group-by="name(*[1])">
     <xsl:copy-of select="current-group()/child::node()"/>
   </xsl:for-each-group>

但您可能想重新注入一些白色space。

我会这样做:

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:key name="my-comments" match="comment()" use="generate-id(following-sibling::*[1])" />

<xsl:template match="/myXml">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates select="*[not(self::node1 or self::node2 or self::node3)]"/>
        <xsl:apply-templates select="node1"/>        
        <xsl:apply-templates select="node2"/>        
        <xsl:apply-templates select="node3"/>        
    </xsl:copy>
</xsl:template>

<xsl:template match="*">
    <xsl:copy-of select="key('my-comments', generate-id())"/>
    <xsl:copy-of select="."/>
</xsl:template>

</xsl:stylesheet>

XSLT 2.0 中,这可以简化为:

<xsl:stylesheet version="2.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:key name="my-comments" match="comment()" use="generate-id(following-sibling::*[1])" />

<xsl:template match="/myXml">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:variable name="my-nodes" select="node1, node2, node3" />
        <xsl:apply-templates select="* except $my-nodes, $my-nodes"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="*">
    <xsl:copy-of select="key('my-comments', generate-id())"/>
    <xsl:copy-of select="."/>
</xsl:template>

</xsl:stylesheet>