tags:

views:

215

answers:

4

I have cheated every time I've needed to do a line count in XSLT by using JScript, but in this case I can't do that. I simply want to write out a line counter throughout an output file. This basic example has a simple solution:

<xsl:for-each select="Records/Record">
   <xsl:value-of select="position()"/>
</xsl:for-each>

Output would be:

1

2

3

4

etc...

But what if the structure is more complex with nested foreach's :

<xsl:for-each select="Records/Record">
   <xsl:value-of select="position()"/>
   <xsl:for-each select="Records/Record">
       <xsl:value-of select="position()"/>
   </xsl:for-each>
</xsl:for-each>

Here, the inner foreach would just reset the counter (so you get 1, 1, 2, 3, 2, 1, 2, 3, 1, 2 etc). Does anyone know how I can output the position in the file (ie. a line count)?

+3  A: 

A line in an XML file is not really the same as an element. In your first example you don't really count the lines - but the number of elements.

An XML file could look like this:

<cheeseCollection>
<cheese country="Cyprus">Gbejna</cheese><cheese>Liptauer</cheese><cheese>Anari</cheese>
</cheeseCollection>

Or the exact same XML file can look like this:

<cheeseCollection>
    <cheese
       country="Cyprus">Gbejna</cheese>
    <cheese>Liptauer</cheese>
    <cheese>Anari</cheese>
</cheeseCollection>

which the XSLT will interpet exactly the same - it will not really bother with the line breaks.

Therefore it's hard to show line numbers in the way you want using XSLT - it's not really meant for for that kind of parsing.

Someone correct me if I'm wrong, but I'd say you would need Javascript or some other scripting language to do what you want.

becquerel
You're exactly right - "position" (i.e. line number) is completely meaningless in XML since whitespace, including carriage returns, is not considered to be part of the document tree.
GalacticCowboy
@GalacticCowboy: Whitespaces *are* considered to be part of document tree--they are included in text nodes.
liori
+2  A: 

While it is quite impossible to mark the line numbers for the serialization of an XML document (because this serialization per se is ambiguous), it is perfectly possible, and easy, to number the lines of regular text.

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>

 <xsl:template match="/">
   <xsl:call-template name="numberLines"/>
 </xsl:template>

 <xsl:template name="numberLines">
  <xsl:param name="pLastLineNum" select="0"/>
  <xsl:param name="pText" select="."/>

  <xsl:if test="string-length($pText)">
   <xsl:value-of select="concat($pLastLineNum+1, ' ')"/>

   <xsl:value-of select="substring-before($pText, '&#xA;')"/>
   <xsl:text>&#xA;</xsl:text>

   <xsl:call-template name="numberLines">
    <xsl:with-param name="pLastLineNum"
      select="$pLastLineNum+1"/>
    <xsl:with-param name="pText"
      select="substring-after($pText, '&#xA;')"/>
   </xsl:call-template>
  </xsl:if>
 </xsl:template>
</xsl:stylesheet>

when applied on this XML document:

<t>The biggest airlines are imposing "peak travel surcharges"
this summer. In other words, they're going to raise fees
without admitting they're raising fees: Hey, it's not a $30
 price hike. It's a surcharge! This comes on the heels of
 checked-baggage fees, blanket fees, extra fees for window
 and aisle seats, and "snack packs" priced at exorbitant
 markups. Hotels in Las Vegas and elsewhere, meanwhile, are
 imposing "resort fees" for the use of facilities (in other
 words, raising room rates without admitting they're
 raising room rates). The chiseling dishonesty of these
 tactics rankles, and every one feels like another nail in
 the coffin of travel as something liberating and
 pleasurable.
</t>

produces the desired line-numbering:

1 The biggest airlines are imposing "peak travel surcharges"
2 this summer. In other words, they're going to raise fees
3 without admitting they're raising fees: Hey, it's not a $30
4  price hike. It's a surcharge! This comes on the heels of
5  checked-baggage fees, blanket fees, extra fees for window
6  and aisle seats, and "snack packs" priced at exorbitant
7  markups. Hotels in Las Vegas and elsewhere, meanwhile, are
8  imposing "resort fees" for the use of facilities (in other
9  words, raising room rates without admitting they're
10  raising room rates). The chiseling dishonesty of these
11  tactics rankles, and every one feels like another nail in
12  the coffin of travel as something liberating and
13  pleasurable.
Dimitre Novatchev
A: 

Thanks for the responses guys - yup you're totally correct, some external function is the only way to get this behaviour in XSLT. For those searching, this is how I did this when using a compiled transform in .Net 3.5:

Create a helper class for your function(s)

/// <summary>
/// Provides functional support to XSLT
/// </summary>
public class XslHelper
{
    /// <summary>
    /// Initialise the line counter value to 1
    /// </summary>
    Int32 counter = 1;

    /// <summary>
    /// Increment and return the line count
    /// </summary>
    /// <returns></returns>
    public Int32 IncrementCount()
    {
        return counter++;
    }
}

Add an instance to an args list for XSLT

XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load(XmlReader.Create(s));
XsltArgumentList xslArg = new XsltArgumentList();
XslHelper helper = new XslHelper();
xslArg.AddExtensionObject("urn:helper", helper);
xslt.Transform(xd.CreateReader(), xslArg, writer);

Use it in you XSLT

Put this in the stylesheet declaration element:

 xmlns:helper="urn:helper"   

Then use like so:

<xsl:value-of select="helper:IncrementCount()" />
Mr AH
A: 

Generally, position() is referring to the number of the current node relative to the entire batch of nodes that is being processed currently.

With your "nested for-each" example, consecutive numbering can easily be achieved when you stop nesting for-each constructs and just select all desired elements at once.

With this XML:

<a><b><c/><c/></b><b><c/></b></a>

a loop construct like this

<xsl:for-each "a/b">
  <xsl:value-of select="position()" />
  <xsl:for-each "c">
    <xsl:value-of select="position()" />
  </xsl:for-each>
</xsl:for-each>

will result in

11221
bccbc  // referred-to nodes

but you could simply do this instead:

<xsl:for-each "a/b/c">
  <xsl:value-of select="position()" />
</xsl:for-each>

and you would get

123
ccc    // referred-to nodes
Tomalak