xslt - 根据规则集对节点进行分组

标签 xslt xpath

我有:

<node handle="full"/>
<node handle="full"/>
<node handle="left"/>
<node handle="right"/>
<node handle="top-left"/>
<node handle="top-right"/>
<node handle="bottom"/>
<node handle="full"/>
<node handle="full"/>

我需要根据以下逻辑对这些节点进行分组:

  • full 应该是独立的。
  • left 应包含最少 1 个、最多 2 个类型为 righttop-rightbottom 的额外节点-右
  • right 应包含最少 1 个、最多 2 个类型为 lefttop-leftbottom 的额外节点-左
  • top-left 应包含最少 2 个、最多 3 个类型为 bottom-righttop-right 的节点, 右下角底部
  • ...

显然,如果我从left开始并且following-siblingright,则该过程应该重置并继续下一个元素。

因此输出应如下所示:

<group>
    <node handle="full"/>
</group>
<group>
    <node handle="full"/>
</group>
<group>
    <node handle="left"/>
    <node handle="right"/>
</group>
<group>
    <node handle="top-left"/>
    <node handle="top-right"/>
    <node handle="bottom"/>
</group>
<group>
    <node handle="full"/>
</group>
<group>
    <node handle="full"/>
</group>

是否有一种有效的(对于人类和机器)方法来处理这个问题,或者应该在代码中逐案管理?


编辑1:

认为我可以像这样定义我的规则集,然后根据具体情况进行比较:

<xsl:variable name="layouts">
    <opt start="left" min="1" max="2">
        <allow pos="right"          value="2"/>
        <allow pos="top-right"      value="1"/>
        <allow pos="bottom-right"   value="1"/>
    </opt>
    <opt start="right" min="1" max="2">
        <allow pos="left"           value="2"/>
        <allow pos="top-left"       value="1"/>
        <allow pos="bottom-left"    value="1"/>
    </opt>
</xsl:variable>

我将使用最大分数,从中减去添加的每个元素的。 看起来怎么样?


编辑2:

Tim C 发布答案之前,我发现该解决方案有效。
我发现两者之间的第一个区别是我的版本限制了序列可接受的起始元素(左、左上)。我不再知道这是一件好事还是我为了避免匹配已经成为序列一部分的节点而引入的限制。
不管怎样,蒂姆的回答比我的要优雅得多。

<!-- 
    For each /item:

    - see it it's one of the starting points of a sequence:
        - Not "full"
        - Left, Top-Left

    - if full, just return the element
    - if not full and not a starting point, skip it, since it means it being added by the previous item.
    - if not full and either of the starting points, kick into a recursion loop in a separate template:

    - store the item's current "score" (2 or 1 for single-quadrant images)
    - recur through the following-siblings with a counter for position(), checking if they are in the allowed list, and decreasing the "score" counter.
    - every time a match is found:
        - recreate the "allow" list, minus the current match, and pass the updated list to the next iteration
        - decrease the counter
    - if the iteration completes, reaching zero, return the position() of the last matched item
    - if during the iteration, while the score is still >0, a match is not found, return false(). Our sequence is broken, we have a user error.

    - the calling template (the one matching *every* item) checks whether the returned result is >0 or false()
    - if >0 returns a copy of every node up the number specified by >0
    - if false() print out and error, suggesting possible sequences.
-->

<xsl:variable name="layouts">
    <start handle="left"            score="2">      <!-- The starting score which we'll subtract from on every iteration -->
        <allow handle="right"           value="2"/> <!-- the acceptable position which we'll check against on every iteration -->
        <allow handle="top-right"       value="1"/> <!-- the value for each position which we'll subtract from the <start> score -->
        <allow handle="bottom-right"    value="1"/>
    </start>
    <start handle="top-left"        score="3">
        <allow handle="right"           value="2"/>
        <allow handle="bottom-left"     value="1"/>
        <allow handle="top-right"       value="1"/>
        <allow handle="bottom-right"    value="1"/>
    </start>
    <start handle="full"            score="0"/> <!-- Position which are not acceptable as the start of a sequence are scored 0 -->
    <start handle="right"           score="0"/>
    <start handle="top-right"       score="0"/>
    <start handle="bottom-right"    score="0"/>
    <start handle="bottom-left"     score="0"/>
</xsl:variable>

<!-- Applied to every /item -->
<xsl:template mode="imagewraps" match="item">
    <xsl:param name="i" select="position()"/>
    <xsl:variable name="nodeName"   select="name(.)"/>
    <xsl:variable name="layout"     select="exsl:node-set($layouts)"/>
    <xsl:variable name="position"   select="position/item/@handle"/>
    <xsl:variable name="score"      select="$layout/start[@handle = $position]/@score"/>
    <xsl:variable name="allowList"  select="$layout/start[@handle = $position]"/>

    <!-- This variable will store the final result of the recursion lanunched from within.
         The returned value will be a number, indication the position of the last node that is part of the sequence -->
    <xsl:variable name="sequenceFound">
        <xsl:if test="$score > 0">
            <xsl:apply-templates mode="test" select="parent::node()/*[name() = $nodeName][$i +1]">
                <xsl:with-param name="i" select="$i +1"/>
                <xsl:with-param name="score"        select="$score"/>
                <xsl:with-param name="allowList"    select="$allowList"/>
            </xsl:apply-templates>
        </xsl:if>
    </xsl:variable>


    <div style="border: 1px solid red">
    <xsl:choose>
        <!-- If the $score is 0 and the position is 'full' just return a copy if the current node -->
        <xsl:when test="$score = 0 and $position = 'full'">
            <xsl:copy-of select="."/>
        </xsl:when>
        <!-- if the $score is greater than 0, return a copy of the current node
             and the siblings the follow, up to the value stored in $sequenceFound -->
        <xsl:when test="$score > 0">
            <xsl:choose>
                <!-- Actually do the above only if $sequenceFound didn't end up being 0
                     (it currently never does, but good to have as an option to handle errors in here) -->
                <xsl:when test="$sequenceFound != 0">
                    <xsl:copy-of select="."/>
                    <xsl:copy-of select="following-sibling::*[$sequenceFound - $i >= position()]"/>
                </xsl:when>
            </xsl:choose>
        </xsl:when>
        <!-- If the first item is wrong, let jsut say it -->
        <xsl:when test="$score = 0 and position() > 1">
            <xsl:message>The first item should either be "full", "left", "top-left".</xsl:message>
        </xsl:when>
    </xsl:choose>
    </div>
</xsl:template>

<xsl:template mode="test" match="*">
    <xsl:param name="i"/>
    <xsl:param name="score"/>
    <xsl:param name="allowList"/>
    <xsl:variable name="this" select="."/>
    <xsl:variable name="nodeName"       select="name()"/>
    <xsl:variable name="position"       select="position/item/@handle"/>
    <xsl:variable name="isInAllowList"  select="count($allowList/allow[@handle = $position]) > 0"/>
    <xsl:variable name="value">
        <xsl:if test="$isInAllowList">
            <xsl:value-of select="$allowList/allow[@handle = $position]/@value"/>
        </xsl:if>
    </xsl:variable>
    <xsl:variable name="allowListMinusMatched">
        <xsl:if test="$isInAllowList">
            <xsl:copy-of select="$allowList/allow[@handle != $position]"/>
        </xsl:if>
    </xsl:variable>

    <xsl:choose>
        <xsl:when test="$isInAllowList">
            <xsl:choose>
                <!-- if we've not ran out of loops, continue -->
                <xsl:when test="($score - $value) > 0">
                    <xsl:apply-templates mode="test" select="parent::node()/*[name() = $nodeName][$i +1]">
                        <xsl:with-param name="i" select="$i +1"/>
                        <xsl:with-param name="allowList" select="$allowListMinusMatched"/>
                        <xsl:with-param name="score" select="$score - $value"/>
                    </xsl:apply-templates>
                </xsl:when>
                <xsl:when test="($score - $value) = 0">
                    <xsl:value-of select="$i"/>
                </xsl:when>
            </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
            <xsl:variable name="layout"     select="exsl:node-set($layouts)"/>
            <xsl:variable name="allowed"  select="$layout/start[@handle = $position]"/>
            <xsl:message>Bombing out. Wrong Sequence.</xsl:message>
            <xsl:message>
                Items allowed after "<xsl:value-of select="$allowed/@handle"/>" are:
                <xsl:for-each select="$allowed/allow">
                    <xsl:value-of select="@handle"/>
                    <xsl:if test="count($allowed/allow) > position()">, </xsl:if>
                    <xsl:if test="count($allowed/allow) = position()">.</xsl:if>
                </xsl:for-each>
            </xsl:message>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

最佳答案

我已经为此找到了解决方案,但我并不完全确定“最低”要求。不过,我会发布到目前为止我所做的事情,如果有帮助的话......

首先,我为每个规则定义了一个xsl:key。例如,对于 left 规则:

<xsl:key name="left" 
 match="node[@handle='right' or @handle='top-right' or @handle='bottom-right']" 
 use="generate-id(preceding-sibling::node[@handle='left'][1])"/>

因此,在本例中,它匹配 righttop-rightbottom-right 元素,并将它们链接到最前面的 <强>左元素。请注意,如果 leftright 元素之间有 bottom 元素,这将选取您需要的更多元素。这将稍后处理...

然后,我创建了一个模板来匹配组中的第一个节点。最初,我使用相关的 xsl:key 定义了一个变量来包含以下节点(其中 $maximum 是包含最大节点数的变量)

<xsl:variable name="followingNodes" 
 select="key(@handle, generate-id())[position() &lt;= $maximum]" />

但是,这将拾取之间可能存在无效元素的元素。为了阻止这个,我首先计算了当前节点的位置......

<xsl:variable name="position" select="count(preceding-sibling::node)"/>

然后我可以检查键中节点的位置是否等于相对于当前节点的位置,在这种情况下,它们之前没有无效元素。

<xsl:variable 
 name="followingNodes" 
 select="key(@handle, generate-id())[position() &lt;= $maximum][count(preceding-sibling::node) - $position &lt;= position()]"/>

这是完整的 XSLT...

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

   <xsl:key name="left" 
      match="node[@handle='right' or @handle='top-right' or @handle='bottom-right']" 
      use="generate-id(preceding-sibling::node[@handle='left'][1])"/>

   <xsl:key name="right" 
      match="node[@handle='left' or @handle='top-left' or @handle='bottom-left']" 
      use="generate-id(preceding-sibling::node[@handle='right'][1])"/>

   <xsl:key name="top-left" 
      match="node[@handle='bottom-right' or @handle='top-right' or @handle='bottom-right' or @handle='bottom']" 
      use="generate-id(preceding-sibling::node[@handle='top-left'][1])"/>

   <xsl:template match="nodes">
      <xsl:apply-templates select="node[1]" mode="first"/>
   </xsl:template>

   <xsl:template match="node[@handle='full']" mode="first">
      <group>
         <xsl:copy-of select="."/>
      </group>
      <xsl:apply-templates select="following-sibling::node[1]" mode="first"/>
   </xsl:template>

   <xsl:template match="node" mode="first">
      <xsl:variable name="maximum">
         <xsl:choose>
            <xsl:when test="@handle='top-left'">3</xsl:when>
            <xsl:otherwise>2</xsl:otherwise>
         </xsl:choose>
      </xsl:variable>
      <xsl:variable name="position" select="count(preceding-sibling::node)"/>
      <xsl:variable name="followingNodes" select="key(@handle, generate-id())[position() &lt;= $maximum][count(preceding-sibling::node) - $position &lt;= position()]"/>
      <group>
         <xsl:copy-of select="."/>
         <xsl:apply-templates select="$followingNodes"/>
      </group>
      <xsl:apply-templates select="following-sibling::node[count($followingNodes) + 1]" mode="first"/>
   </xsl:template>

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

当这应用于以下输入 XML 时...

<nodes>
    <node handle="full"/>
    <node handle="full"/>
    <node handle="left"/>
    <node handle="right"/>
    <node handle="top-left"/>
    <node handle="top-right"/>
    <node handle="bottom"/>
    <node handle="full"/>
    <node handle="full"/>
</nodes>

输出如下:

<group>
   <node handle="full"/>
</group>
<group>
   <node handle="full"/>
</group>
<group>
   <node handle="left"/>
   <node handle="right"/>
</group>
<group>
   <node handle="top-left"/>
   <node handle="top-right"/>
   <node handle="bottom"/>
</group>
<group>
   <node handle="full"/>
</group>
<group>
   <node handle="full"/>
</group>

关于xslt - 根据规则集对节点进行分组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7652982/

相关文章:

xslt - 使用 JDK 内部 Xalan 和 SecurityManager 从 XSL 样式表内部加载外部 Java 方法

javascript - 对 <xsl :template> 中的 <div> 的引用

xml - 如何使用 xslt 更新 xml 节点值?

python - 如何使用 lxml 选择和更新混合内容中的文本节点?

html - 简单的 Xpath 难题

xpath - Scrapy + Splash:在内部html内抓取元素

xslt - <key> 元素中的 key() 函数

xml - 动态元素名称

xml - 如何在遍历节点集时引用源 XML?

c# - Selenium WebDriver : how to count number of DIV elements in a list