DokuWiki

It's better when it's simple

User Tools

Site Tools


tips:tableswithrowspans2

Tables with rowspans and vertical alignment

I hacked this together in a couple hours last night and it seems to be working correctly. Unfortunately, I don't have time to learn darc and how to make a patch so I'm just posting this hack here and hopefully someone else can put together a code patch for the devs. – jmucchiello [at] yahoo [dot] com

By applying the code fixes listed here, you can create tables with rowspans each with their own vertical alignment. This patch does not alter the existing table syntax. To create a rowspan, the “lost” columns must equal (without any spaces between |'s or ^'s) one of the following character sequences /\, /-\, or /_\ to indicate top, center, or bottom alignment respectively. Multi-dimensional spans work just like existing colspans: the ghost column's must be followed by the appropriate number of |'s or ^'s.

Features at a glance

  • Compatible with 99.9% of existing DokuWiki tables. (How many tables have content that looks like /\, /-\ and/or /_\?)
  • Supports bidirectional spans.
  • Supports valign=“top”, valign=“center” and valign=“bottom” in each cell.
  • Supports both header and non-header spans.

Usage

The following code should generate the HTML listed below:

^ ^col 1 ^  col 2  ^  col 3&4^^
^row a  |a1-b1  |  a2-c3  || a4 |
^row b&c  |/-\|/_\|| b4|
^/\|c1  |/_\|^ c4|
^row d  |d1  |  d2  |  d3| d4 |

This 4×4 table (plus headers) has individual cells at a4, b4, c1, c4, d1, d2, d4, and d4.
The top row and leftmost columns are header cells. c4 is also a header cell.
There are 2 2-cell blocks, the one at row2&3 is bottom aligned and the one at a1-b1 is top aligned.
There is a 6-cell block spanning a2, a3, b2, b3, c2 and c3 which is center aligned.

<html>
<table class="inline">
    <tbody><tr>
        <th valign="top"> </th>
        <th valign="top">col 1 </th>
        <th class="centeralign" valign="top">  col 2  </th>
        <th class="rightalign" colspan="2" valign="top">  col 3&amp;4</th>
    </tr><tr>
        <th valign="top">row a  </th>
        <td class="leftalign" rowspan="2" valign="center">a1-b1  </td>
        <td class="centeralign" rowspan="3" colspan="2" valign="bottom">  a2-c3  </td>
        <td valign="top"> a4 </td>
    </tr><tr>
        <th rowspan="2" valign="top">row b&amp;c  </th>
        <td valign="top"> b4</td>
    </tr><tr>
        <th valign="top">c1  </th>
        <th valign="top"> c4</th>                         <!-- Notice the caret in the source? -->
    </tr><tr>
        <th valign="top">row d  </th>
        <td class="leftalign" valign="top">d1  </td>
        <td class="centeralign" valign="top">  d2  </td>
        <td class="rightalign" valign="top">  d3</td>
        <td valign="top"> d4 </td>
    </tr>
</tbody></table>
</html>

Known Issues

  • While you can have leading spaces in ghost columns, trailing spaces are forbidden.
  • The colspan code makes sure all rows have the same number of columns but this code doesn't ensure all columns have the same number of rows.
  • The valign=“” stuff should be CSS. Someone else can make the 9 different alignment styles. This method is more backward compatible.

Patch Code

The patch involves 5 files in the inc/parser directory: parser.php, handler.php, metadata.php, renderer.php, and xhtml.php:

inc/parser/parser.php

// Add this extra line to the indicated class/method
//-------------------------------------------------------------------
class Doku_Parser_Mode_table extends Doku_Parser_Mode {
 
    function postConnect() {
        $this->Lexer->addPattern('\n\^','table');
        $this->Lexer->addPattern('\n\|','table');
        #$this->Lexer->addPattern(' {2,}','table');
        $this->Lexer->addPattern('[\t ]+','table');
// rowspan:patch++
        $this->Lexer->addPattern('/[-_]?\\\\[|^]+','table');
// rowspan:patch--
        $this->Lexer->addPattern('\^','table');
        $this->Lexer->addPattern('\|','table');
        $this->Lexer->addExitPattern('\n','table');
    }
}

inc/parser/handler.php

// In this file, we must modify the Doku_Handler and Doku_Handler_Table classes.
// These listings are 
class Doku_Handler {
    function table($match, $state, $pos) {
        switch ( $state ) {
            case DOKU_LEXER_MATCHED:
                if ( $match == ' ' ){
                    $this->_addCall('cdata', array($match), $pos);
                } else if ( preg_match('/\t+/',$match) ) {
                    $this->_addCall('table_align', array($match), $pos);
                } else if ( preg_match('/ {2,}/',$match) ) {
                    $this->_addCall('table_align', array($match), $pos);
                } else if ( $match == "\n|" ) {
                    $this->_addCall('table_row', array(), $pos);
                    $this->_addCall('tablecell', array(), $pos);
                } else if ( $match == "\n^" ) {
                    $this->_addCall('table_row', array(), $pos);
                    $this->_addCall('tableheader', array(), $pos);
                } else if ( $match == '|' ) {
                    $this->_addCall('tablecell', array(), $pos);
                } else if ( $match == '^' ) {
                    $this->_addCall('tableheader', array(), $pos);
// rowspan:patch++
                } else if ( substr($match,0,2) == '/\\') {
                    $this->_addCall(($match[2] == '^') ? 'tableheader' : 'tablecell', array('top',strlen($match)-2), $pos);
                } else if ( substr($match,0,3) == '/-\\') {
                    $this->_addCall(($match[2] == '^') ? 'tableheader' : 'tablecell', array('center',strlen($match)-3), $pos);
                } else if ( substr($match,0,3) == '/_\\') {
                    $this->_addCall(($match[2] == '^') ? 'tableheader' : 'tablecell', array('bottom',strlen($match)-3), $pos);
// rowspan:patch--
                }
            break;
 
 
 
//------------------------------------------------------------------------
class Doku_Handler_Table {
 
    //------------------------------------------------------------------------
    function process() {
        foreach ( $this->calls as $call ) {
            switch ( $call[0] ) {
                case 'tableheader':
                case 'tablecell':
// rowspan:patch++
                case 'tablevspan':
// rowspan:patch--
                    $this->tableCell($call);
                break;
 
 
 
    function tableCell($call) {
        if ( !$this->firstCell ) {
            $lastCall = end($this->tableCalls);
 
// rowspan:patch++
            if (count($call[1]) > 0) {
                 $this->tableCalls[] = array('rowspan',$call[1],$call[2]);
            }
            // A cell call which follows an open cell means an empty cell so span
            else if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) {
// rowspan:patch--
                 $this->tableCalls[] = array('colspan',array(),$call[2]);
            }
 
            $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
            $this->tableCalls[] = array($call[0].'_open',array(1,NULL,1,'top'),$call[2]);
            $this->lastCellType = $call[0];
 
        } else {
 
            $this->tableCalls[] = array($call[0].'_open',array(1,NULL,1,'top'),$call[2]);
            $this->lastCellType = $call[0];
            $this->firstCell = false;
 
        }
 
        $this->currentCols++;
    }
 
 
 
// rowspan:patch++
    // new function: finds a call in the tableCalls array
    function _findCall($needle, $key, $dir = -1) {
        while (1) {
            if ($key < 0 || $key >= count($this->tableCalls)) {
                return false;
            }
 
            if ($this->tableCalls[$key][0] == $needle) {
                return $key;
            }
 
            $key += $dir;
        }
        return false;
    }
// rowspan:patch--
 
    function finalizeTable() {
 
        // Add the max cols and rows to the table opening
        if ( $this->tableCalls[0][0] == 'table_open' ) {
            // Adjust to num cols not num col delimeters
            $this->tableCalls[0][1][] = $this->maxCols - 1;
            $this->tableCalls[0][1][] = $this->maxRows;
        } else {
            trigger_error('First element in table call list is not table_open');
        }
 
        $lastRow = 0;
        $lastCell = 0;
        $toDelete = array();
 
        // Look for the colspan elements and increment the colspan on the
        // previous non-empty opening cell. Once done, delete all the cells
        // that contain colspans
        foreach ( $this->tableCalls as $key => $call ) {
 
            if ( $call[0] == 'tablerow_open' ) {
 
                $lastRow = $key;
 
            } else if ( $call[0] == 'tablecell_open' || $call[0] == 'tableheader_open' ) {
 
                $lastCell = $key;
 
            } else if ( $call[0] == 'table_align' ) {
 
                // If the previous element was a cell open, align right
                if ( $this->tableCalls[$key-1][0] == 'tablecell_open' || $this->tableCalls[$key-1][0] == 'tableheader_open' ) {
                    $this->tableCalls[$key-1][1][1] = 'right';
 
                // If the next element if the close of an element, align either center or left
                } else if ( $this->tableCalls[$key+1][0] == 'tablecell_close' || $this->tableCalls[$key+1][0] == 'tableheader_close' ) {
                    if ( $this->tableCalls[$lastCell][1][1] == 'right' ) {
                        $this->tableCalls[$lastCell][1][1] = 'center';
                    } else {
                        $this->tableCalls[$lastCell][1][1] = 'left';
                    }
 
                }
 
                // Now convert the whitespace back to cdata
                $this->tableCalls[$key][0] = 'cdata';
 
            } else if ( $call[0] == 'colspan' ) {
 
                $this->tableCalls[$key-1][1][0] = false;
 
                for($i = $key-2; $i > $lastRow; $i--) {
 
                    if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) {
 
                        if ( false !== $this->tableCalls[$i][1][0] ) {
                            $this->tableCalls[$i][1][0]++;
                            break;
                        }
                    }
                }
 
                $toDelete[] = $key-1;
                $toDelete[] = $key;
                $toDelete[] = $key+1;
 
// rowspan:patch++
            } else if ( $call[0] == 'rowspan' ) {
 
                $this->tableCalls[$key-1][1][2] = 0;
 
                $colofs = $call[1][1];
                $rowstart = $this->_findCall('tablerow_open', $key-2);
                for($i = $key-2; $i > $rowstart; $i--) {
                    if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) {
                        $colofs += $this->tableCalls[$i][1][0];
                    }
                }
 
                $rowend = $rowstart-1;
                while (($prevrow = $this->_findCall('tablerow_open', $rowend)) !== false) {
                    $colsleft = $colofs;
                    for ($i = $prevrow; $i < $rowend; ++$i) {
                        if ($this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open') {
                            if ($this->tableCalls[$i][1][2] == 0) {
                                $colsleft = -1;
                            } else {
                                $colsleft -= $this->tableCalls[$i][1][0];
                            }
                        }
                        if ($colsleft == 0) {
                            $this->tableCalls[$i][1][2]++;
                            $this->tableCalls[$i][1][3] = $call[1][0];
                            $this->tableCalls[$key-1][1][0] = $this->tableCalls[$i][1][0]; // set the colspan to the parent colspan
                            break 2;
                        } else if ($colsleft < 0) {
                            break;
                        }                        
                    }   
                    $rowend = $prevrow - 1;                
                }
 
                $toDelete[] = $key-1;
                $toDelete[] = $key;
                $toDelete[] = $key+1;
            }
// rowspan:patch--
        }

inc/parser/renderer.php and inc/parser/metadata.php

class Doku_Renderer extends DokuWiki_Plugin {
    // we just need to add some parameters to these two functions
    function tableheader_open($colspan = 1, $align = NULL, $rowspan = 1, $valign = 'bottom'){}
 
    function tablecell_open($colspan = 1, $align = NULL, $rowspan = 1, $valign = 'top'){}

inc/parser/xhtml.php

    function tableheader_open($colspan = 1, $align = NULL, $rowspan = 1, $valign = 'bottom'){
        $this->doc .= '<th';
        if ( !is_null($align) ) {
            $this->doc .= ' class="'.$align.'align"';
        }
        if ( !is_null($valign) ) {
            $this->doc .= ' valign="'.$valign.'"';
        }
        if ( $rowspan > 1 ) {
            $this->doc .= ' rowspan="'.$rowspan.'"';
        }
        if ( $colspan > 1 ) {
            $this->doc .= ' colspan="'.$colspan.'"';
        }
        $this->doc .= '>';
    }
 
    function tablecell_open($colspan = 1, $align = NULL, $rowspan = 1, $valign = 'top'){
        $this->doc .= '<td';
        if ( !is_null($align) ) {
            $this->doc .= ' class="'.$align.'align"';
        }
        if ( !is_null($valign) ) {
            $this->doc .= ' valign="'.$valign.'"';
        }
        if ( $rowspan > 1 ) {
            $this->doc .= ' rowspan="'.$rowspan.'"';
        }
        if ( $colspan > 1 ) {
            $this->doc .= ' colspan="'.$colspan.'"';
        }
        $this->doc .= '>';
    }

This version has some errors. Like lower table :

^ col1  ^ col2  ^ col3  ^
|  2,1  | 2,2  | 2,3  |
|/\| 3,2  | 3,3  |
|/\|/-\| 4,3  |
|/\|/-\| 5,3  |
|/-\| 6,2  | 6,3  |
| 7,1  | 7,2  | 7,3  |

So I changed(inc/parser/handler.php):

                while (($prevrow = $this->_findCall('tablerow_open', $rowend)) !== false) {
                    $colsleft = $colofs;
                    for ($i = $prevrow; $i < $rowend; ++$i) {
                        // patch++
                        if ($this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open') {
                            $colsleft -= $this->tableCalls[$i][1][0];
                        }
                        if ($colsleft == 0 && $this->tableCalls[$i][1][2] != 0) {
                        // patch--
                            $this->tableCalls[$i][1][2]++;
                            $this->tableCalls[$i][1][3] = $call[1][0];
                            $this->tableCalls[$key-1][1][0] = $this->tableCalls[$i][1][0]; // set the colspan to the parent colspan
                            break 2;
                        } else if ($colsleft < 0) {
                            break;
                        }                        
                    }
 
                    $rowend = $prevrow - 1;                
                }

Discussion

It's a bit complex to modify the code and is error prone, I wrote an article in http://forum.dokuwiki.org/thread/3662 which is easy to understand.

tips/tableswithrowspans2.txt · Last modified: 2015-08-29 21:38 by Aleksandr

Except where otherwise noted, content on this wiki is licensed under the following license: CC Attribution-Share Alike 4.0 International
CC Attribution-Share Alike 4.0 International Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki