How to create sections from a linear structure with title nodes?

E

Erhard Schwenk

Hi there,

Maybe this is a faq, if so, point me the URL please.

I have to do some xml to xml transformations in xslt, where I have -
simplified - the following:

input:
<doc>
<a/><a/><b/><a><a><a><b/><a><b/><a><a>
</doc>

output:
<doc>
<section><x/><x/></section>
<section><y/><x/><x/><x/></section>
<section><y/><x/></section>
<section><y/><x/><x/>
</doc>

e.g. I want to transform <a/> to <x/> and <b/> to <y/>, but start a
new section at each <b/>

For Example, you could think of a Text with headlines and paragraphs
and I want to start a new Page at each Headline.

Now there is the question, how to do this in xslt. Some Idea was the
following:

<section>
<xsl:apply-templates select="All a-nodes before the first b-node"/>
</section>
<xsl:for-each select="b">
<section>
<xsl:apply-templates select="b"/>
<xsl:apply-templates select="all following a-nodes up to the next
b-node"/>
</section>
</xsl:for-each>

My Problem are now the two xpath-Expressions "All a-nodes before the
first b-node" and "all following a-nodes up to the next b". Googling
around I did not find any useful tips on this, so perhaps someone here
has an Idea?

MfG,
 
A

Andy Fish

Erhard

you're going about the problem the wrong way. your XML is essentially
document-oriented rather than strongly structured so you should process it
by matching templates. something like:

<xsl:template match="a">
<x><xsl:apply-templates/></x>
</xsl:template>

<xsl:template match="b">
<section><y><xsl:apply-templates/></y></section>
</xsl:template>

don't think of XSL as a programming language - it's a declarative way of
specifying how you want the XML transformed. I suggest you read a good book
like Jeni Tennison's 'beginning xslt' which will explain it all in a
sensible order and show you the various techniques.

Andy
 
E

Erhard Schwenk

Andy Fish wrote:

The problem is that 'context' that you need to use to generate the sections
depends on nodes that have already been processed, not on ancestor nodes of
your current node. If you were to write this program in a 'proper'
programming language you would basically have a state machine with a
variable saying whether you are already in a section or not, and this is
exactly what XSL (being a functional language) is bad at.

I think you might be able to do something with following-sibling and
preceding-sibling but I've racked my brains and I can't figure out how you'd
apply it


Hmm meanwhile I got some solution like this:

<xsl:template match="/">
<section>
<xsl:apply-templates
select="*[(count(preceding-sibling::a)) = 0 and name != 'a']"/>
</section>

<xsl:for-each select="a">
<xsl:variable name="number" select="1+count(preceding-sibling::a"/>

<section>
<x/>
<xsl:apply-templates
select="../*[count(preceding-sibling::a) = $number
and name != 'a'"
/>
</section>
</xsl:for-each>
</xsl:template>

<xsl:template match="b">
<y/>
</xsl:template>


Seems to work, maybe there is something better. Thanks for your hints
anyway.

BTW: if there is someone who maintains a faq with XSL Snippets, I think
this one would be a great addition. Feel free to copy.
 
R

Richard Tobin

Erhard Schwenk said:
Maybe this is a faq, if so, point me the URL please.

It's certainly very similar to the question posted here by Marc
Baumgartner ([email protected]) in June.
input:
<doc>
<a/><a/><b/><a><a><a><b/><a><b/><a><a>
</doc>

output:
<doc>
<section><x/><x/></section>
<section><y/><x/><x/><x/></section>
<section><y/><x/></section>
<section><y/><x/><x/>
</doc>

e.g. I want to transform <a/> to <x/> and <b/> to <y/>, but start a
new section at each <b/>

Here's a general approach to this kind of problem.

Construct an XPath that will identify one member of each group. In
this case, the first <a> is the obvious thing to choose (if there was
always a <b> in the group, the <b> would be a better choice). We can
identify the first <a> in each group as being an <a> whose immediate
preceding-sibling is not an <a>:

a[local-name(preceding-sibling::*[1]) != 'a']

(If there is an <a> at the start without a preceding b, this will
still work because local-name retrns an exmpty string for an empty
node-set.)

Call apply-templates on that path. Use a mode, for reasons that will
be apparent later:

<xsl:template match="doc">
<doc>
<xsl:apply-templates mode="group"
select="a[local-name(preceding-sibling::*[1]) != 'a']"/>
</doc>
</xsl:template>

In the template for the selected representative, output the group
wrapper.

<xsl:template match="a" mode="group">
<section>
...
</section>
</xsl:template>

Now construct an XPath that will select all the elements in the same
group as the representative.

The XPath to select the group members will use something they have in
common with the representative element. Thinking up a test for this
is often the tricky bit. In this case, we want the <b> before the
representative <a> (if there is one) and the <a>s that are following
siblings of the representative <a> without any intervening <b>s. We
can do it by selecting the siblings that have the same number of
following-sibling <b>s as the representative <a>:

../*[count(following-sibling::b) = count(current()/following-sibling::b)]

Note the use of current() to get hold of the representative <a> inside
the predicate. We could have assigned it to a variable instead, but
this is simpler.

Sometimes the obvious test involves comparing two nodes for identity.
For that you can use the trick generate-id(node1) = generate-id(node2).

Inside the wrapper, call apply-templates on the members of the group.
This is why we used a mode for the group template; we are going to
call apply-templates on the representative again in its role as a
member of the group.

<xsl:template match="a" mode="group">
<section>
<xsl:apply-templates select="../*[count(following-sibling::b) =
count(current()/following-sibling::b)]"/>
</section>
</xsl:template>

Now write templates for the group members, which is trivial in this case.

<xsl:template match="a">
<a/>
</xsl:template>

<xsl:template match="b">
<y/>
</xsl:template>

-- Richard
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top