tags:

views:

243

answers:

3

I have this XML document where I want to modify using XSLT to a different format. The problem I'm currently facing is finding the absolute position of a tag relative to root and not to the parent.

For instance take the following example:

<book>
  <section>
     <chapter>
     </chapter>
  </section>
</book>
<book>
  <section>
     <chapter>
     </chapter>
  </section>
</book>    <book>
  <section>
     <chapter>
     </chapter>
  </section>
</book>    <book>
  <section>
     <chapter>
     </chapter>
  </section>
</book>

Desired output:

<book id=1>
  <section id=1>
     <chapter id=1>
     </chapter>
  </section>
</book>
<book id=2>
  <section id=2>
     <chapter id=2>
     </chapter>
  </section>
</book>    
<book id=3>
  <section id=3>
     <chapter id=3>
     </chapter>
  </section>
</book>    
<book id=4>
  <section id=4>
     <chapter id=4>
     </chapter>
  </section>
</book>

To get the id for the book tag can be easily achieved by using the position(), but once we go down to section and chapter things get trickier.

A solution for this problem would be creating a global variables that would work as counters for section and chapter, which would increment every time one of these tags are found in the document, but variables in XSLT behave like constants.

thanks in advance,

fbr

A: 

Must the IDs be integers? An easy way to generate unique IDs would be to create them be appending their parents to them:

<book id="1">
  <section id="1.1">
     <chapter id="1.1.1">
     </chapter>
  </section>
</book>

In that case you can use position() and recursion to generate the IDs easily.

Welbog
They must be integers and must follow a sequence like you would have in a sql table.
ForeignerBR
A: 

How about

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="*|@*|text()">
    <xsl:copy>
        <xsl:apply-templates select="*|@*|text()" />
    </xsl:copy>
</xsl:template>

<xsl:template match="book|section|chapter">
    <xsl:copy>
           <xsl:attribute name="ix">
                <xsl:value-of select="1 + count(preceding::*[name() = name(current())])"/>
            </xsl:attribute> 
        <xsl:apply-templates select="*|@*|text()" />
    </xsl:copy>
</xsl:template>
</xsl:stylesheet>

("ix" used instead of "id" as you really shouldn't have multiple elements with the same id in your XML)

Alohci
+1  A: 

xsl:number was built for this sort of scenario.

It makes it very easy to produce various formatted numbers and counts and is used often in XSL-FO for things such as a Table of Contents and labels for figures and tables (e.g. figure 3.a, section 1.1, etc.)

I adjusted the sample XML by adding a document element in order to make it well formed.

Using this stylesheet produces the desired output.

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

<xsl:template match="/">
            <xsl:apply-templates select="*/book" />
</xsl:template>

    <xsl:template match="*">
        <xsl:copy>
            <xsl:attribute name="id">   
                <xsl:number format="1 " level="single" count="book"/>
            </xsl:attribute>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>
Mads Hansen
Your solution is almost perfect; however the resulting document is not well-formed because it lacks a root element; you should add a root element in the template that matches the root of the source document. (It's true that the examples given by the OP are not well-formed either).
@bambax - That is why the apply-templates in the template of the root node selects `*/book`. It applies templates to all of the book children of the document element, whatever it is, and ensures that templates are not applied to the document element.
Mads Hansen