^ ^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 4x4 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.
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
====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 .= '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 .= ' 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.