views:

233

answers:

3

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</something>  
</P>  

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

Any ideas??

A: 

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"&gt;
  <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>
Rubens Farias
Thanx for the quick reply!Unfortunately this wil only work if i know what's in the XML-file.But, this is only a snippet. My problem is that i have to group the bullettext elements by finding them and put list tags around them no matter where they are. I have no idea if this can be done...
AZtec
so try to ask another question including more details
Rubens Farias
Essentially i want to find "any" group of "bullettext" elements in an XML file and put them (as a group) between "LIST" tags (and if possible rename them to "ITEM") (or to refrase, i want to create unordered lists). The script has to go through a file containing hundreds of lines of XML.
AZtec
+1  A: 

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"&gt;

    <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.

igor
Nice!There's only one problem left. The "other"-tag in your example should be outside the LIST-tags after transformation. Any idea if this can be done?
AZtec
Hmm, yeah I will edit in the change, but are you ever going to have multiple sets of <Bullettext>s in a node separated by other stuff? If so, that seems to be a more complicated adjacent-siblings problem that the above alone won't work for.
igor
(RE:myself) Disregard my question, now editing in a stylesheet that works for multiple groups per node as well.
igor
This is an amazing forum...Never before have i had help this good and this fast.I'm off from work now and am looking forward to your solution.IF this can be done you've deffinitely saved my day!
AZtec
+1 for the clear example of template recursion. I think my solution is more XSLT-like and dislike the more procedural approach here. But that doesn't mean mine's better, it just means I've come to identify with my torturers. Note also that in your main template, you can reduce nesting by making the `if` test the first `when` case in your `choose` block; just have it do nothing.
Robert Rossney
I have to thank you and Robert for the amzing help. Both solutions work like a charm. I would love to give a positive vote for your efforts but apparantly i first have to have 15 points or something to do this. Since i just stumled upon this site i'm not yet registered and don't have the points. I'll try to register this week-end and will give both of you a positive score as soon as i can. I've learned a lot from your code and will most certainly be back with more questions.
AZtec
+2  A: 

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.

Robert Rossney