[xsl] setting up tables differently based on existance of xml tags. (static variables, recursion, templates, and intrigue)

Subject: [xsl] setting up tables differently based on existance of xml tags. (static variables, recursion, templates, and intrigue)
From: robert frapples <oyenstikker@xxxxxxxxx>
Date: Wed, 1 Dec 2004 09:09:09 -0500
This was on [www-xsl-fo@xxxxxx], somebody suggested I put it on this
list. Enjoy.  (xsl:fo is used, but not an issue to the problem.)

Here's a (hopefully) interesting problem: (I've been editing this
email all day. Every time I can't figure out how to do something, I
come up with an alternate plan, assisted by a few people in #xml on
irc.freenode.net. (Thank you Bebabo).  I figured at this point I'd
share my experiences. If you can think of better, faster, cleaner ways
to do this, I'd love to hear from you.)

source xml:
<document>
   <field>
       <title>a</title>
   </field>
   <field>
       <title>b</title>
       <value/>
   <field>
       <title>c</title>
       <value/>
   </field>
   <field>
       <title>d</title>
   </field>
   <field>
       <title>e</title>
       <value/>
   </field>
</document>

What I want to happen is basically, for-each select="field", if
<field> has a <value/> and the next <field> also has a value, have 4
cells in a row: field[n]/title, text , field[n+1]/title, 'text'.  If
<field> does not have a <value/> or if the next <field> does not have
a <value/>, then have 2 cells: field[n]/title, and 'other text' with
number-columns-spanned=3.

My first thought was this:
/-----------------------------------------------------------------------------------------\
. . .
<xsl:variable name="col" select="''"/>
<xsl:table-body>
<xsl:for-each select="field">
<xsl:choose>
   <xsl:when test="value and not($col)">
       <fo:table-row>
           <fo:table-cell><xsl:value-of select="title"/></fo:table-cell>
           <fo:table-cell>text</fo:table-cell>
           <xsl:variable name="col" select="'true'"/>
   </xsl:when>
   <xsl:when test="value and $col">
           <fo:table-cell><xsl:value-of select="title"/></fo:table-cell>
           <fo:table-cell>text</fo:table-cell>
        </fo:table-row>
        <xsl:variable name="col" select="''"/>
   </xsl:when>
   <xsl:when test="not(value) and not($col)">
        <fo:table-row>
            <fo:table-cell><xsl:value-of select="title"/></fo:table-cell>
            <fo:table-cell>other text</fo:table-cell>
            <fo:table-cell number-columns-spanned="2"/>
        </fo:table-row>
   </xsl:when>
   <xsl:when test="not(value) and $col">
            <fo:table-cell number-columns-spanned="2"/>
         </fo:table-row>
         <fo:table-row>
            <fo:table-cell><xsl:value-of select="title"/></fo:table-cell>
            <fo:table-cell>other text</fo:table-cell>
            <fo:table-cell number-columns-spanned="2"/>
         </fo:table-row>
          <xsl:variable name="col" select="''"/>
   </xsl:when>
</xsl:choose>
</xsl:for-each>
</fo:table-body>
\---------------------------------------------------------------------------------------/

This obviously will not work, because XSLT does not allow variables to
be reassigned.  After reading extensively from O'Reilly's XSLT book
and the w3c specs for XSLT and XPath, I decided I could achive my goal
with recursive template matches.  Here is what I arrived at:

/----------------------------------------------------------------------------------------\
<xsl:stylesheet . . .>
   <xsl:template match="document">
. . .
        <fo:table-body>
            <xsl:apply-templates select="field[1]">
        </fo:table-body>
. . .
   </xsl:template> <!-- end template match="document" -->
   <xsl:template match="field">
       <xsl:choose>
           <!-- When current <field> has <value/> and next <field>
has <value/> -->
           <xsl:when test="value and following-sibling::field[1]/value">
               <fo:table-row>
                   <fo:table-cell>
                       <!-- current title -->
                       <fo:block><xsl:value-of select="title"/></fo:block>
                   </fo:table-cell>
                   <fo:table-cell>
                       <fo:block>text</fo:block>
                   </fo:table-cell>
                   <fo:table-cell>
                       <!-- next title -->
                       <fo:block><xsl:value-of
select="following-sibling::field[1]/title"/></fo:block>
                   </fo:table-cell>
                   <fo:table-cell>
                       <fo:block>text</fo:block>
                   </fo:table-cell>
               </fo:table-row>
               <!-- use this template for the next element (after the
one previously referred
                     to as 'next'. omitting the final '[1]' will
cause this template to be processed
                     remaining <field>, which results in very long
files with lots of stuff i don't
                     want. -->
               <xsl:apply-templates
select="following-sibling::field[position() &gt; 1][1]"/>
           </xsl:when>
           <!-- if not. . . -->
           <xsl:otherwise>
               <fo:table-row>
                   <fo:table-cell>
                       <fo:block><xsl:value-of select="title"/></fo:block>
                   </fo:table-cell>
                   <fo:table-cell number-columns-spanned="3">
                       <fo:block>other text</fo:block>
                   </fo:table-cell>
               </fo:table-row>
               <!-- use this template for the next element. again,
don't omit '[1]'. -->
               <xsl:apply-templates select="following-sibling::field[1]"/>
           </xsl:otherwise>
       </xsl:choose>
   </xsl:template>
\------------------------------------------------------------------------------------/

This works great.  Based on my understanding of the XSLT parsers, this
is pretty memory intensive, as applying "following-sibling::field[1]"
actually passes the remaining <field> nodes in <document> every time,
leaving a lot of stuff in the stack. Is that correct?

At any rate, any ideas on a better way to do this?  (Other than a SAX
parsing of the XML with a few variables and leaving an extra node in
select <field>s.)

Current Thread