XSLT将兄弟姐妹转变为儿童

时间:2017-01-02 10:40:32

标签: xml xslt xpath

我正在尝试在xslt中做一些棘手的事情 我有一个扁平的xml,其中包含大量的兄弟姐妹,并根据名称我想将它们转换为孩子 我转型的一般规则是:

  1. 如果标签/名称为“BLOCK”,则打开标签/值为属性的“BLOCK”元素
  2. 如果标签/名称为“BLOCK_END”,请关闭“BLOCK”元素(
  3. 在所有其他情况下,创建元素标记/名称,放置标记/值并立即关闭它
  4. 对于以下xml:

    <message>
        <tag>
            <name>BLOCK</name>
            <value>first</value>
        </tag>
        <tag>
            <name>FOO</name>
            <value>BAR</value>
        </tag>
        <tag>
            <name>BLOCK</name>
            <value>second</value>
        </tag>
        <tag>
            <name>FOO2</name>
            <value>BAR2</value>
        </tag>
        <tag>
            <name>BLOCK_END</name>
        </tag>
        <tag>
            <name>BLOCK_END</name>
        </tag>
        <tag>
            <name>BLOCK</name>
            <value>third</value>
        </tag>
        <tag>
            <name>FOO3</name>
            <value>BAR3</value>
        </tag>
        <tag>
            <name>BLOCK_END</name>
        </tag>
    </message>
    

    这是我希望的结果:

    <message>
        <BLOCK id="first">
            <FOO>BAR</FOO>
            <BLOCK id="second">
                <FOO2>BAR2</FOO2>
            </BLOCK>
        </BLOCK>
        <BLOCK id="third">
            <FOO3>BAR3</FOO3>
        </BLOCK">    
    </message>
    

    我使用了以下xslt。这工作正常但遗憾的是它在第一个BLOCK_END标记

    之后完成了执行

    <xsl:template match="/">
        <message>
            <xsl:apply-templates select="message/tag[1]" />
        </message>
    </xsl:template>
    
    <xsl:template match="tag">
        <xsl:variable name="tagName" select="name"/>
        <xsl:variable name="tagValue" select="value"/>
        <xsl:choose>
            <xsl:when test="$tagName = 'BLOCK'">
                <xsl:element name="{$tagName}">
                    <xsl:attribute name="id">
                        <xsl:value-of select="$tagValue"/>
                    </xsl:attribute>
                    <xsl:apply-templates select="./following-sibling::*[1]" />
                </xsl:element>
            </xsl:when>
            <xsl:when test="$tagName = 'BLOCK_END'">
                <!-- DO NOTHING-->
            </xsl:when>
            <xsl:otherwise>
                <xsl:element name="{$tagName}">
                    <xsl:value-of select="$tagValue"/>
                </xsl:element>
                <xsl:apply-templates select="./following-sibling::*[1]" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    

    更新:感谢 BitTickler 我越来越近了,但仍然不在那里。

3 个答案:

答案 0 :(得分:2)

可能有一个较短的方式,但在我看来这仍然是最直接的方法:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:key name="child-item" match="tag[not(name='BLOCK')]" use="generate-id(preceding-sibling::tag[name='BLOCK'][@level=current()/@level][1])" />

<xsl:key name="child-block" match="tag[name='BLOCK']" use="generate-id(preceding-sibling::tag[name='BLOCK'][@level=current()/@level - 1][1])" />

<xsl:template match="/message">
    <xsl:variable name="first-pass-rtf">
        <xsl:apply-templates select="tag[1]" mode="first-pass" />
    </xsl:variable>
    <xsl:variable name="first-pass" select="exsl:node-set($first-pass-rtf)/tag" />
    <!-- output -->
    <message>
        <xsl:apply-templates select="$first-pass[name='BLOCK'][@level=1]"/>
    </message>
</xsl:template>

<!-- first-pass templates -->
<xsl:template match="tag[name='BLOCK']" mode="first-pass">
    <xsl:param name="level" select="0"/>
    <tag level="{$level + 1}">
        <xsl:copy-of select="*"/>
    </tag>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level + 1"/>
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="tag" mode="first-pass">
    <xsl:param name="level"/>
    <tag level="{$level}">
        <xsl:copy-of select="*"/>
    </tag>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level"/>
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="tag[name='BLOCK_END']" mode="first-pass">
    <xsl:param name="level"/>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level - 1"/>
    </xsl:apply-templates>
</xsl:template>

<!-- output templates -->
<xsl:template match="tag[name='BLOCK']">
    <BLOCK id="{value}">
        <xsl:apply-templates select="key('child-item', generate-id()) | key('child-block', generate-id())" />
    </BLOCK>
</xsl:template>

<xsl:template match="tag">
    <xsl:element name="{name}">
        <xsl:value-of select="value"/>
    </xsl:element>
</xsl:template>

</xsl:stylesheet>

首先将输入重写为:

<xsl:variable name="first-pass-rtf">
    <tag level="1">
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag level="1">
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag level="2">
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag level="2">
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag level="1">
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag level="1">
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
</xsl:variable>

然后将每个tag与其父块相关联变得简单(r)。

答案 1 :(得分:1)

问题来自于递归模板调用有两个目的(1个太多):

  1. 将光标一直延伸到输入标记元素中。
  2. 处理输出的嵌套级别。
  3. 要使其工作,必须从递归函数(模板)“返回”当前输出状态和“迭代”状态。

    在功能语言中,可以证明这一点,例如:使用以下短代码,模拟情况。

    type Node = 
        | Simple of string * string
        | Nested of string * string * Node list
    
    let input =
        [ 
            Simple ("BLOCK","first")
            Simple ("FOO","BAR")
            Simple ("BLOCK","second")
            Simple ("FOO2","BAR2")
            Simple ("BLOCK_END","")
            Simple ("FOO3","BAR3")
            Simple ("BLOCK_END","")
        ]
    
    let rec transform (result,remaining) =
        match remaining with
        | [] -> result,remaining
        | x::xs -> 
            match x with
            | Simple (n,v) when n = "BLOCK" ->
                let below,remaining' = transform ([],xs)
                transform (result @ [Nested(n,v,below)],remaining')
            | Simple (n,v) when n = "BLOCK_END" ->
                result,xs
            | Simple (n,v) ->
                transform (result @[x],xs)
    
    transform ([],input)
    

    现在有1个解决方案策略可行,唯一剩下的问题是,如何将此策略应用于xslt转换。

    为了启动整个事情,可能应该转换第一个<tag>元素。在其转换中,递归发生。

    BLOCK_END应该以某种方式从递归中返回,这样当前位置是已知的,因此BLOCK部分可以在稍后的那一点恢复。

    到目前为止,我最好的猜测是这样的:

           

    <xsl:template match="/">
      <xsl:element name="message">
        <xsl:apply-templates select="/message/tag[1]" />
      </xsl:element>
    </xsl:template>
    
    <xsl:template name="nest" match="tag">
      <xsl:variable name="tagName" select="name"/>
      <xsl:variable name="tagValue" select="value"/>
    
      <xsl:choose>
        <xsl:when test="./name='BLOCK'">
          <xsl:element name="{$tagName}">
            <xsl:attribute name="id">
              <xsl:value-of select="$tagValue"/>
            </xsl:attribute>
            <xsl:apply-templates select="./following-sibling::tag[1]" />
          </xsl:element>
          <!--TODO: We must continue here with the remaining nodes. But we do not know  how many 
          Nodes the block contained... Our cursor (.) is unaffected by previous recursion. -->
          <!--<xsl:apply-templates select="./following-sibling::tag[1]" />-->
        </xsl:when>
        <xsl:when test="./name='BLOCK_END'">
          <!--No nothing-->
        </xsl:when>
        <xsl:otherwise>
          <xsl:element name="{$tagName}">
            <xsl:value-of select="$tagValue"/>
          </xsl:element>
          <xsl:apply-templates select="./following-sibling::tag[1]" />
        </xsl:otherwise>
      </xsl:choose>
    
    </xsl:template>
    

    制作输出:

    <message>
       <BLOCK id="first">
          <FOO>BAR</FOO>
          <BLOCK id="second">
             <FOO2>BAR2</FOO2>
          </BLOCK>
       </BLOCK>
    </message>
    

答案 2 :(得分:0)

最终解决方案是对BitTickler的想法的延伸:

我不得不对原始xml进行twick,以便结束块标记还包含一个标识符(我们用它来搜索BLOCK标记的相应END_BLOCK标记)

<message>
    <tag>
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag>
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag>
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
        <value>second</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
        <value>first</value>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag>
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
        <value>third</value>
    </tag>
</message>

然后诀窍是对END_BLOCK + 1标签的模板进行一次循环调用(参见&#34; magic&#34;评论)。做&#34;呼叫模板&#34;没有任何好处。 (只是我身边的一些遗留的试错)我将把它还原为&#34; apply-templates&#34;

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

    <xsl:template match="/">
        <message>
            <xsl:call-template name="transformTag">
                <xsl:with-param name="tag" select="message/tag[1]"/>
            </xsl:call-template>
        </message>
    </xsl:template>

    <xsl:template name="transformTag">
        <xsl:param name="tag"/>
        <xsl:variable name="tagName" select="$tag/name"/>
        <xsl:variable name="tagValue" select="$tag/value"/>
        <xsl:choose>
            <xsl:when test="$tagName = 'BLOCK'">
                <!-- OPEN A BLOCK, RECURENTLY CALL FOR THE NEXT ELEMENT AND THEN MAKE A RECURENT CALL ONCE AGAIN FOR THE NEXT BLOCK-->
                <xsl:element name="{$tagName}">
                    <xsl:attribute name="id">
                        <xsl:value-of select="$tagValue"/>
                    </xsl:attribute>
                    <xsl:call-template name="transformTag">
                        <xsl:with-param name="tag" select="$tag/following-sibling::*[1]"/>
                    </xsl:call-template>
                </xsl:element>
                <!-- THIS IS WHERE THE MAGIC HAPPENS-->
                <xsl:variable  name="closingTag" select="$tag/following-sibling::*[name='END_BLOCK' and substring-before(value,'@@')=$tagValue][1]"/>

                <xsl:if test="$closingTag/name='END_BLOCK'">
                    <xsl:variable name="nextTag" select="$closingTag/following-sibling::*[1]"/>
                    <xsl:if test="$nextTag[name() = 'tag']">
                        <xsl:call-template name="transformTag">
                            <xsl:with-param name="tag" select="$nextTag"/>
                        </xsl:call-template>
                    </xsl:if>
                </xsl:if>
            </xsl:when>
            <xsl:when test="$tagName = 'END_BLOCK'">
                <!-- DO NOTHING AND EXIT THE RECURENT CALL (THIS CLOSES THE TAG)-->
            </xsl:when>
            <xsl:otherwise>
                <!-- PRINT THE REGULAR TAG AND RECURENTLY CALL FOR THE NEXT TAG-->
                <xsl:element name="_{$tagName}">
                    <xsl:value-of select="$tagValue"/>
                </xsl:element>
                <xsl:call-template name="transformTag">
                    <xsl:with-param name="tag" select="$tag/following-sibling::*[1]"/>
                </xsl:call-template>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>