Solved

XSL: Selecting a specific nodeset in a flat structure using XPath

Posted on 2004-10-14
5
495 Views
Last Modified: 2006-11-17
I have the following XML structure:

<root>
  <a>MainText</a>
  <b>SubText</b>
  <c>SubText</c>
  <b>SubText</b>
  <a>MainText</a>
  <b>SubText</b>
  <b>SubText</b>
  <d>SubText</d>
  <a>MainText</a>
  <b>SubText</b>
  <c>SubText</c>
  <b>SubText</b>
</root>

The <a> nodes contain the main content, and all of the subsequent nodes until the next <a> node can be considered to "belong" to the previous <a> node. (I realise that this is not the way to structure an XML document;
"belonging" nodes should be set as children, however, this is what I have to work with.)

I want to form a nodeset of the <a> node and all of the following nodes, up to but not including the next <a> node. In this example I would have 3 separate nodesets.

Nodeset 1:  
  <a>MainText</a>
  <b>SubText</b>
  <c>SubText</c>
  <b>SubText</b>

Nodeset 2:
  <a>MainText</a>
  <b>SubText</b>
  <b>SubText</b>
  <d>SubText</d>

Nodeset 3:
  <a>MainText</a>
  <b>SubText</b>
  <c>SubText</c>
  <b>SubText</b>

I understand that "following-sibling::*" from the first <a> node returns all of the siblings.
Also "following-sibling::a[1]" returns the next <a> node (single node in a nodeset).
What I want is all of the "siblings" in between these two nodes and in this example, I would carry out the process 3 times.

I originally tried using the following:

...
<xsl:template match="a">
     <xsl:for-each select="following-sibling::*">      
            <xsl:if test="not(a)">
                  <xsl:choose>
                        <xsl:when test=" *** name of element ****">
                              <xsl:call-template name="**** do something ****"></xsl:call-template>
                        </xsl:when>            
                  </xsl:choose>
            </xsl:if>
      </xsl:for-each>
</xsl:template>
...


This outputted all of the following siblings(first time through 9 nodes, second time 6 nodes etc.) except the <a>'s. Not what was desired.  I had originally wanted to break out of the "for-each" loop when I encountered an <a> element but realised that this is not possible in XSL. Just to complicate matters a little more, there are variable numbers of <b>, <c> and <d> elements between each <a> element, and the last group is not terminated by an <a> element.

What I think I need is something like:
         <xsl:for-each select="following-sibling::*[***all siblings [up to next 'a' or no more siblings]*** ]">      

However the XPath escapes me!

Any suggestions for the XPath statement or any alternative approaches would be greatly appreciated.

The background to this problem is that each <a> based group will be output to a separate document using <redirect> and Xalan.

Thanks

Mark

0
Comment
Question by:dev-ngps7
5 Comments
 
LVL 15

Accepted Solution

by:
dualsoul earned 200 total points
Comment Utility
hm...i'm not sure about XPath expression to do that, but your task can be handled fairly nice, using recursive template, see:

<?xml version="1.0" encoding="UTF-8"?>
<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:template match="/">
        <xsl:call-template name="processA">
            <xsl:with-param name="node" select="/root/a[1]"/>
        </xsl:call-template>
    </xsl:template>
   
    <xsl:template name="processA">
        <xsl:param name="node"/>
        <xsl:if test="name($node)='a'">
                    Group:<br/>
        </xsl:if>
        <xsl:value-of select="name($node)"/>
        <br/>
        <xsl:if test="$node/following-sibling::*">
            <xsl:call-template name="processA">
                <xsl:with-param name="node" select="$node/following-sibling::*[1]"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>
0
 

Assisted Solution

by:z38601
z38601 earned 150 total points
Comment Utility
This should work for you also:

<xsl:for-each select="a">
      <!-- DO SOMETHING, IF NEEDED, WITH THE 'A' NODE -->
      <xsl:for-each select="following-sibling::* and count(preceding-sibling::a[1] | current()) = 1]">
            <!-- DO SOMETHING WITH EACH CHILD NODE -->
      </xsl:for-each>
</xsl:for-each>
0
 
LVL 26

Assisted Solution

by:rdcpro
rdcpro earned 150 total points
Comment Utility
Close...actually you probably meant:

<?xml version="1.0" encoding="UTF-8"?>
<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:template match="root">
            <xsl:apply-templates select="a"/>
      </xsl:template>
      <xsl:template match="a">
            <b><xsl:value-of select="local-name()"/>: <xsl:value-of select="."/> </b><br/>
                  <xsl:for-each select="following-sibling::*[count(preceding-sibling::a[1] | current()) = 1][not(local-name() = 'a')]">
                        <em><xsl:value-of select="."/></em><br/>
                  </xsl:for-each>
      </xsl:template>
</xsl:stylesheet>


Good one, z38601!  I was pulling my hair out trying to figure an XPath for this.

Regards,
Mike Sharp
0
 

Author Comment

by:dev-ngps7
Comment Utility
Thanks for the solutions. I have tested them with my example file and all are working exactly as I hoped.

I am going to split the points 3 ways. (Hope that's ok for you all).

The recursive method is the solution that I implemented (because it came in first and I had already made the changes to my stylesheet before seeing the XPath solution. My real application does not consist of <a>'s and <b>'s, and is significantly messier/complicated so adapting it for the recursive method took me a while yesterday.)

In terms of performance, I'm assuming the XPath method would be more efficient, but is this true?

I'm also glad it wasn't just me that was pulling  his hair out over the XPath statement!
Any good tips for advanced XPath tutorials?

Mark
0
 
LVL 15

Expert Comment

by:dualsoul
Comment Utility
>In terms of performance, I'm assuming the XPath method would be more efficient, but is this true

hm...there are count() function is called inside XPath...it's hard to say will it be faster then recursino or not, profiling can say
0

Featured Post

How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

Join & Write a Comment

The Problem How to write an Xquery that works like a SQL outer join, providing placeholders for absent data on the outer side?  I give a bit more background at the end. The situation expressed as relational data Let’s work through this.  I’ve …
I was working on a PowerPoint add-in the other day and a client asked me "can you implement a feature which processes a chart when it's pasted into a slide from another deck?". It got me wondering how to hook into built-in ribbon events in Office.
In this seventh video of the Xpdf series, we discuss and demonstrate the PDFfonts utility, which lists all the fonts used in a PDF file. It does this via a command line interface, making it suitable for use in programs, scripts, batch files — any pl…
Polish reports in Access so they look terrific. Take yourself to another level. Equations, Back Color, Alternate Back Color. Write easy VBA Code. Tighten space to use less pages. Launch report from a menu, considering criteria only when it is filled…

771 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

10 Experts available now in Live!

Get 1:1 Help Now