开发者

How to reformat XML with group-adjacent (XSLT)

I'm new in this XSLT-thing and I can't figure out how to this:

This is a snippet from the xml i start with:

<Article>    
<Bullettext>10,00 </Bullettext>  
<Bullettext>8,00 </Bullettext>    
</Article>  
<Article>  
<something>some text</something>  
</Article>  
<Article>  
<Corpsdetexte>Bulgaria</Corpsdetexte>  
<Bullettext>15,0 </Bullettext>  
<Bullettext>10,0 </Bullettext>  
</Article> ` 

This is what i want the output to be:

<LIST>  
<ITEM>12,00 </ITEM>  
<ITEM>10,00 </ITEM>  
<ITEM>8,00 </ITEM>  
</LIST>  
<P>  
<something>some text</som开发者_如何学Cething>  
</P>  

<P>  
<Corpsdetexte>Bulgaria</Corpsdetexte>  
</P>  
<LIST>  
<ITEM>15,0 </ITEM>  
<ITEM>10,0 </ITEM>  
</LIST>  

Any ideas??


From your comment in response to Rubens Farias's answer (and really, that's something you should edit your question to include), it seems that you want a generic way to transform any group of adjacent BulletText elements into a list. That gets us to two questions: how do we find such groups, and having found them, how do we transform them into a list?

To find a group, we need to look for all BulletText elements whose immediately preceding sibling isn't a BulletText element. Each one of those starts a group, and those are the elements that we're going to transform into lists. So the first thing we want to do is create an XPath expression that will find them:

BulletText[not(preceding-sibling::*[1][name()='BulletText'])]

If you look at the predicates in that XPath expression, it's doing just what I said we need to do: it matches a BulletText element if it's not the case that its first preceding sibling (preceding-sibling::*[1]) has a name of BulletText. Note that if the element has no preceding sibling, this expression will still match it.

So now we can create a template that matches these start-of-group elements. What do we put inside this template? We're going to transform these elements into LIST elements, so the template's going to start out looking like:

<LIST>
   ...
</LIST>

Easy enough. But how do we find the elements that are going to populate that list? There are two cases we have to deal with.

The first is simple: if all of the following siblings are BulletText elements, we want to populate the list with this element and all of its following siblings.

The second is harder. If there's a following sibling that's not a BulletText element, we want our list to be all of the children of the current element's parent, starting at the current element and ending before the stop element. Here is a case where we need to use the count() function to calculate the starting and ending indexes, and the position() function to find the position of each element.

The completed template looks like this:

<xsl:template match="BulletText[not(preceding-sibling::*[1][name()='BulletText'])]">
  <!-- find the element that we want to stop at -->
  <xsl:variable name="stop" select="./following-sibling::*[name() != 'BulletText'][1]"/>
  <LIST>
    <xsl:choose>
      <!-- first, the simple case:  there's no element we have to stop at -->
      <xsl:when test="not($stop)">
        <xsl:apply-templates select="." mode="item"/>
        <xsl:apply-templates select="./following-sibling::BulletText" mode="item"/>
      </xsl:when>
      <!-- transform all elements between the start and stop index into items -->
      <xsl:otherwise>
        <xsl:variable name="start_index" select="count(preceding-sibling::*) + 1"/>
        <xsl:variable name="stop_index" select="count($stop/preceding-sibling::*)"/>
        <xsl:apply-templates select="../*[position() &gt;= $start_index 
                                      and position() &lt;= $stop_index]"
                             mode="item"/>
      </xsl:otherwise>
    </xsl:choose>
  </LIST>
</xsl:template>

You need two other templates. One converts BulletText elements into items - we use mode here so that we can apply it to BulletText elements without invoking the template we're currently using:

<xsl:template match="BulletText" mode="item">
   <ITEM>
      <xsl:value-of select="."/>
   </ITEM>
</xsl:template>

Then you also need a template that keeps BulletText elements that our first template doesn't match from generating any output (because if we're using the identity transform, they'll just get copied if we don't):

<xsl:template match='BulletText'/>

Because of the magic of XSLT's template precedence rules, any BulletText element that both templates match will be transformed by the first one, and this one will catch the rest.

Just add those three templates to the identity transform and you're good to go.


I think you're looking for a conditional deep-copy.

Here's the code in the link above rewritten for your situation:

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

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

    <!-- nodes with Bullettext children -->
    <xsl:template match="*[Bullettext]">
        <!-- for every child -->
        <xsl:copy>
            <xsl:for-each select="*">
                <!-- if child is a Bullettext and it has a Bullettext before it, don't copy it (it has already been copied) -->
                <xsl:if test="not(local-name(.) = 'Bullettext' and local-name(./preceding-sibling::*[1]) = 'Bullettext')">
                    <xsl:choose>
                        <xsl:when test="local-name(.) = 'Bullettext'">
                            <!-- copy all Bullettext children adjacent to this one and each other -->
                            <LIST>
                                <xsl:call-template name="get-all-adjacent-siblings">
                                    <xsl:with-param name="sibling-before" select="." />
                                </xsl:call-template>
                            </LIST>
                        </xsl:when>
                        <xsl:otherwise>
                            <!-- copy non-Bullettext child -->
                            <xsl:apply-templates select="." />
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:if>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="get-all-adjacent-siblings">
        <xsl:param name="sibling-before" />
        <!-- return me -->
        <xsl:copy>
            <xsl:value-of select="$sibling-before" />
        </xsl:copy>
        <!-- return my adjacent Bullettext siblings below me -->
        <xsl:if test="local-name($sibling-before/following-sibling::*[1]) = 'Bullettext'">
            <xsl:call-template name="get-all-adjacent-siblings">
                <xsl:with-param name="sibling-before" select="$sibling-before/following-sibling::*[1]" />
            </xsl:call-template>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

The input I used was:

<?xml version="1.0" encoding="utf-8"?>
<Articles>
    <Article>
        <Bullettext>10,00 </Bullettext>
        <Bullettext>8,00 </Bullettext>
    </Article>
    <Article>
        <something>some text</something>
    </Article>
    <Article>
        <Corpsdetexte>Bulgaria</Corpsdetexte>
        <deeper>
            <before>dogs</before>
            <Bullettext>15,0 </Bullettext>
            <Bullettext>10,0 </Bullettext>
            <middle>cats</middle>
            <Bullettext>25,0 </Bullettext>
            <Bullettext>20,0 </Bullettext>
            <after>cows</after>
        </deeper>
    </Article>
</Articles>

And it gave me:

<?xml version="1.0" encoding="UTF-8"?>
<Articles>
    <Article>
        <LIST>
            <Bullettext>10,00 </Bullettext>
            <Bullettext>8,00 </Bullettext>
        </LIST>
    </Article>
    <Article>
        <something>some text</something>
    </Article>
    <Article>
        <Corpsdetexte>Bulgaria</Corpsdetexte>
        <deeper>
            <before>dogs</before>
            <LIST>
                <Bullettext>15,0 </Bullettext>
                <Bullettext>10,0 </Bullettext>
            </LIST>
            <middle>cats</middle>
            <LIST>
                <Bullettext>25,0 </Bullettext>
                <Bullettext>20,0 </Bullettext>
            </LIST>
            <after>cows</after>
        </deeper>
    </Article>
</Articles>

It's a bit messy if you want to do the other transformations like adding <p></p>s in the same stylesheet but if you do a two step transformation with two stylesheets, the first doing the conditional deep copy above and then the second doing your main transformation using the result of the first, you should be good to go.


Grouping Siblings with Three Templates

Although there are currently several working answers to this question, you can actually group siblings very easily with three templates and the identity.

First, you need a template that will just remove all of the nodes and set its priority, so that we can override it with another template.

<xsl:template match="Bullettext" priority="1"/>

Then, define a template that matches any node that is not preceded by itself, assigning it a higher priority. This template will inject the group and then start copying nodes in a different mode.

<xsl:template match="Bullettext[not(preceding-sibling::*[1][self::Bullettext])]" priority="2">
  <LIST>
    <xsl:apply-templates select="." mode="bullet-list"/>
  </LIST>
</xsl:template>

Finally, define a tail recursive template to deal with the items being grouped.

<xsl:template match="Bullettext" mode="bullet-list">
  <ITEM>
    <xsl:apply-templates select="@*|node()"/>
  </ITEM>
  <xsl:apply-templates select="following-sibling::*[1][self::Bullettext]" mode="bullet-list"/>
</xsl:template>

Here is a complete stylesheet that will group the Bullettext elements in the example:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes" />

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

  <!-- Removes the Bullettext elements in the default mode. -->
  <xsl:template match="Bullettext" priority="1" />

  <!-- Creates the LIST elements around the removed Bullettext elements. -->
  <xsl:template match="Bullettext[not(preceding-sibling::*[1][self::Bullettext])]" priority="2">
    <LIST>
      <xsl:apply-templates select="." mode="bullet-list" />
    </LIST>
  </xsl:template>

  <!-- Converts sequential Bullettext elements into ITEM elements. -->
  <xsl:template match="Bullettext" mode="bullet-list">
    <ITEM>
      <xsl:apply-templates select="@*|node()" />
    </ITEM>
    <xsl:apply-templates select="following-sibling::*[1][self::Bullettext]" mode="bullet-list" />
  </xsl:template>

</xsl:stylesheet>


Try something like this:

<?xml version="1.0" encoding="iso-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/Articles">
    <LIST>
      <xsl:for-each select="Article[1]/Bullettext">
        <ITEM>
          <xsl:value-of select="." />
        </ITEM>
      </xsl:for-each>
    </LIST>

    <p>
      <something>
        <xsl:value-of select="Article[2]/something" />
      </something>
    </p>

    <p>
      <Corpsdetexte>
        <xsl:value-of select="Article[3]/Corpsdetexte" />
      </Corpsdetexte>
    </p>

    <LIST>
      <xsl:for-each select="Article[4]/Bullettext">
        <ITEM>
          <xsl:value-of select="." />
        </ITEM>
      </xsl:for-each>
    </LIST>
  </xsl:template>
</xsl:stylesheet>
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜