Re: [xsl] Table formatting challenge

Subject: Re: [xsl] Table formatting challenge
From: Oliver Becker <obecker@xxxxxxxxxxxxxxxxxxxxxxx>
Date: Thu, 12 Jul 2001 17:39:12 +0200 (MET DST)
> Consider the following table:
> 
> +--------+--------+--------+--------+
> |        |   B    |        |   D    |
> +   A    +--------+   C    +--------+
> |        |        |        |   E    |
> +--------+--------+--------+--------+
> 
> Represented by this markup:
> 
> <informaltable>
> <tgroup cols="4">
> <colspec colname="c4" colnum="4"/>
> <tbody>
> <row>
>   <entry morerows="1">A</entry>
>   <entry>B</entry>
>   <entry morerows="1">C</entry>
>   <entry>D</entry>
> </row>
> <row>
>   <entry namest="c4">E</entry>
> </row>
> </tbody>
> </tgroup>
> </informaltable>
> 
> The correct HTML for this table is:
> 
> <table border="1">
> <tr>
>   <td rowspan="2">A</td>
>   <td>B</td>
>   <td rowspan="2">C</td>
>   <td>D</td>
> </tr>
> <tr>
>   <td class="auto-generated"></td>
>   <td>E</td>
> </tr>
> </table>
> 
> Can anyone see a practical way to achieve this result without
> resorting to extension functions? The tricky bit is not simply
> noticing that the introduction of namest may require some
> auto-generated cells to be inserted, but calculating correctly, in the
> presence of overhang (from an arbitrarily large number of preceeding
> rows), how many cells to insert.
> 
> I'm hoping I've overlooked something clever and elegant.

Evaluate it yourself ;-)
Tested with Saxon and Xalan, doesn't need extensions.
I hope my comments are reasonable useful.

Cheers,
Oliver



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

<xsl:output method="html" />


<xsl:template match="informaltable">
   <table border="1">
      <xsl:apply-templates />
   </table>
</xsl:template>


<xsl:template match="row">
   <tr>
      <xsl:call-template name="process-row">
         <xsl:with-param name="col" select="1" />
      </xsl:call-template>
   </tr>
</xsl:template>


<!-- 
 | key for the colspec element;
 | not optimal, because colnames don't have to be globally unique, I assume
 +-->
<xsl:key name="colspec" match="colspec" use="@colname" />


<!--
 | Process each column of a row 
 +-->
<xsl:template name="process-row">
   <xsl:param name="col" />
   <!-- is the current column number <= total number of cols? -->
   <xsl:if test="$col &lt;= ../../@cols">
      <!-- compute the overhang for this column -->
      <xsl:variable name="overhang">
         <xsl:call-template name="compute-overhang">
            <xsl:with-param name="col" select="$col" />
            <xsl:with-param name="rows" select="preceding-sibling::row" />
         </xsl:call-template>
      </xsl:variable>
      <!-- only if $overhang is 0 we have to create a <td> -->
      <xsl:if test="$overhang = 0">
         <!-- find out, if there's an explicit entry in the input -->
         <!-- perhaps one with a namest attribute? -->
         <xsl:variable name="namest" 
              select="entry[key('colspec',@namest)/@colnum = $col]" />
         <xsl:choose>
            <xsl:when test="$namest">
               <!-- you may optimize this if you like and discard
                    rowspan="1" attributes ;-) -->
               <td rowspan="{sum($namest/@morerows)+1}">
                  <xsl:value-of select="$namest" />
               </td>
            </xsl:when>
            <xsl:otherwise>
               <!-- look for an entry at the requested position -->
               <xsl:variable name="entry-id">
                  <xsl:call-template name="find-entry">
                     <xsl:with-param name="col" select="$col" />
                     <xsl:with-param name="entries" select="entry" />
                  </xsl:call-template>
               </xsl:variable>
               <xsl:variable name="entry"
                             select="entry[generate-id()=$entry-id]" />
               <xsl:choose>
                  <xsl:when test="$entry">
                     <td rowspan="{sum($entry/@morerows)+1}">
                        <xsl:value-of select="$entry" />
                     </td>
                  </xsl:when>
                  <xsl:otherwise>
                     <td class="auto-generated" />
                  </xsl:otherwise>
               </xsl:choose>
            </xsl:otherwise>
         </xsl:choose>
      </xsl:if>
      <!-- next column -->
      <xsl:call-template name="process-row">
         <xsl:with-param name="col" select="$col + 1" />
      </xsl:call-template>
   </xsl:if>
</xsl:template>


<!-- 
 | Compute the overhang for a given column 
 +-->
<xsl:template name="compute-overhang">
   <xsl:param name="col" />                 <!-- column number -->
   <xsl:param name="rows" />                <!-- preceding rows -->
   <xsl:param name="overhang" select="0" /> <!-- current overhang -->
   <xsl:choose>
      <xsl:when test="not($rows)">
         <!-- no rows left -->
         <xsl:value-of select="$overhang" />
      </xsl:when>
      <xsl:when test="$overhang > 0">
         <!-- we have already an overhang, go to the next row -->
         <xsl:call-template name="compute-overhang">
            <xsl:with-param name="col" select="$col" />
            <xsl:with-param name="rows" select="$rows[position() != 1]" />
            <xsl:with-param name="overhang" select="$overhang - 1" />
         </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
         <!-- ok, current overhang is 0, we have still rows;
              let's look for an entry in our column -->
         <xsl:variable name="namest" select=
            "$rows[1]/entry[key('colspec', @namest)[@colnum = $col]]" />
         <xsl:choose>
            <xsl:when test="$namest">
               <!-- there is an <entry namest=...> element -->
               <xsl:call-template name="compute-overhang">
                  <xsl:with-param name="col" select="$col" />
                  <xsl:with-param name="rows" select="$rows[position() != 1]" />
                  <xsl:with-param name="overhang" 
                                  select="sum($namest/@morerows)" />
               </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
               <xsl:variable name="entry-id">
                  <xsl:call-template name="find-entry">
                     <xsl:with-param name="col" select="$col" />
                     <xsl:with-param name="entries" select="$rows[1]/entry" />
                  </xsl:call-template>
               </xsl:variable>
               <xsl:variable name="entry"
                    select="$rows[1]/entry[generate-id()=$entry-id]" />
               <xsl:choose>
                  <xsl:when test="$entry">
                     <xsl:call-template name="compute-overhang">
                        <xsl:with-param name="col" select="$col" />
                        <xsl:with-param name="rows" 
                                        select="$rows[position() != 1]" />
                        <xsl:with-param name="overhang" 
                                        select="sum($entry/@morerows)" />
                     </xsl:call-template>
                  </xsl:when>
                  <xsl:otherwise>
                     <!-- an auto-generated cell must be created at the end;
                          it has no overhang -->
                     <xsl:call-template name="compute-overhang">
                        <xsl:with-param name="col" select="$col" />
                        <xsl:with-param name="rows" 
                                        select="$rows[position() != 1]" />
                     </xsl:call-template>
                  </xsl:otherwise>
               </xsl:choose>   
            </xsl:otherwise>
         </xsl:choose>   
      </xsl:otherwise>
   </xsl:choose>
</xsl:template>


<!-- 
 | Finds an entry for a requested column;
 | returns an id (generate-id) of this entry, since we cannot return a node
 +-->
<xsl:template name="find-entry">
   <xsl:param name="col" />     <!-- requested col, relative to $entries -->
   <xsl:param name="entries" /> <!-- left entry elements -->
   <xsl:choose>
      <!-- the first entry doesn't have a namest attribute, touche! -->
      <xsl:when test="$col=1 and $entries[1][not(@namest)]">
         <xsl:value-of select="generate-id($entries[1])" />
      </xsl:when>
      <!-- dito, except that we don't look for the first entry,
           process the rest recursively -->
      <xsl:when test="$col>1 and $entries[1][not(@namest)]">
         <xsl:call-template name="find-entry">
            <xsl:with-param name="col" select="$col - 1" />
            <xsl:with-param name="entries" select="$entries[position() != 1]" />
         </xsl:call-template>
      </xsl:when>
      <!-- the first entry has a namest attribute, `move' $col the
           appropriate number of steps and process the rest recursively -->
      <xsl:when test="$entries[1][@namest]">
         <xsl:call-template name="find-entry">
            <xsl:with-param name="col" 
                 select="$col - key('colspec', $entries[1]/@namest)/@colnum" />
            <xsl:with-param name="entries" select="$entries[position() != 1]" />
         </xsl:call-template>
      </xsl:when>
      <!-- otherwise, i.e. $col < 1 or $entries is empty, 
           return nothing -->
   </xsl:choose>
</xsl:template>


<xsl:template match="text()" />


</xsl:stylesheet>


/-------------------------------------------------------------------\
|  ob|do        Dipl.Inf. Oliver Becker                             |
|  --+--        E-Mail: obecker@xxxxxxxxxxxxxxxxxxxxxxx             |
|  op|qo        WWW:    http://www.informatik.hu-berlin.de/~obecker |
\-------------------------------------------------------------------/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread