1*be906b56SAndreas Gohr<?php 2*be906b56SAndreas Gohr 3*be906b56SAndreas Gohrnamespace dokuwiki\Parsing\Handler; 4*be906b56SAndreas Gohr 5*be906b56SAndreas Gohrclass Table implements ReWriterInterface 6*be906b56SAndreas Gohr{ 7*be906b56SAndreas Gohr 8*be906b56SAndreas Gohr /** @var CallWriterInterface original CallWriter */ 9*be906b56SAndreas Gohr protected $callWriter; 10*be906b56SAndreas Gohr 11*be906b56SAndreas Gohr protected $calls = array(); 12*be906b56SAndreas Gohr protected $tableCalls = array(); 13*be906b56SAndreas Gohr protected $maxCols = 0; 14*be906b56SAndreas Gohr protected $maxRows = 1; 15*be906b56SAndreas Gohr protected $currentCols = 0; 16*be906b56SAndreas Gohr protected $firstCell = false; 17*be906b56SAndreas Gohr protected $lastCellType = 'tablecell'; 18*be906b56SAndreas Gohr protected $inTableHead = true; 19*be906b56SAndreas Gohr protected $currentRow = array('tableheader' => 0, 'tablecell' => 0); 20*be906b56SAndreas Gohr protected $countTableHeadRows = 0; 21*be906b56SAndreas Gohr 22*be906b56SAndreas Gohr /** @inheritdoc */ 23*be906b56SAndreas Gohr public function __construct(CallWriterInterface $CallWriter) 24*be906b56SAndreas Gohr { 25*be906b56SAndreas Gohr $this->callWriter = $CallWriter; 26*be906b56SAndreas Gohr } 27*be906b56SAndreas Gohr 28*be906b56SAndreas Gohr /** @inheritdoc */ 29*be906b56SAndreas Gohr public function writeCall($call) 30*be906b56SAndreas Gohr { 31*be906b56SAndreas Gohr $this->calls[] = $call; 32*be906b56SAndreas Gohr } 33*be906b56SAndreas Gohr 34*be906b56SAndreas Gohr /** 35*be906b56SAndreas Gohr * @inheritdoc 36*be906b56SAndreas Gohr * Probably not needed but just in case... 37*be906b56SAndreas Gohr */ 38*be906b56SAndreas Gohr public function writeCalls($calls) 39*be906b56SAndreas Gohr { 40*be906b56SAndreas Gohr $this->calls = array_merge($this->calls, $calls); 41*be906b56SAndreas Gohr } 42*be906b56SAndreas Gohr 43*be906b56SAndreas Gohr /** @inheritdoc */ 44*be906b56SAndreas Gohr public function finalise() 45*be906b56SAndreas Gohr { 46*be906b56SAndreas Gohr $last_call = end($this->calls); 47*be906b56SAndreas Gohr $this->writeCall(array('table_end',array(), $last_call[2])); 48*be906b56SAndreas Gohr 49*be906b56SAndreas Gohr $this->process(); 50*be906b56SAndreas Gohr $this->callWriter->finalise(); 51*be906b56SAndreas Gohr unset($this->callWriter); 52*be906b56SAndreas Gohr } 53*be906b56SAndreas Gohr 54*be906b56SAndreas Gohr /** @inheritdoc */ 55*be906b56SAndreas Gohr public function process() 56*be906b56SAndreas Gohr { 57*be906b56SAndreas Gohr foreach ($this->calls as $call) { 58*be906b56SAndreas Gohr switch ($call[0]) { 59*be906b56SAndreas Gohr case 'table_start': 60*be906b56SAndreas Gohr $this->tableStart($call); 61*be906b56SAndreas Gohr break; 62*be906b56SAndreas Gohr case 'table_row': 63*be906b56SAndreas Gohr $this->tableRowClose($call); 64*be906b56SAndreas Gohr $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); 65*be906b56SAndreas Gohr break; 66*be906b56SAndreas Gohr case 'tableheader': 67*be906b56SAndreas Gohr case 'tablecell': 68*be906b56SAndreas Gohr $this->tableCell($call); 69*be906b56SAndreas Gohr break; 70*be906b56SAndreas Gohr case 'table_end': 71*be906b56SAndreas Gohr $this->tableRowClose($call); 72*be906b56SAndreas Gohr $this->tableEnd($call); 73*be906b56SAndreas Gohr break; 74*be906b56SAndreas Gohr default: 75*be906b56SAndreas Gohr $this->tableDefault($call); 76*be906b56SAndreas Gohr break; 77*be906b56SAndreas Gohr } 78*be906b56SAndreas Gohr } 79*be906b56SAndreas Gohr $this->callWriter->writeCalls($this->tableCalls); 80*be906b56SAndreas Gohr 81*be906b56SAndreas Gohr return $this->callWriter; 82*be906b56SAndreas Gohr } 83*be906b56SAndreas Gohr 84*be906b56SAndreas Gohr protected function tableStart($call) 85*be906b56SAndreas Gohr { 86*be906b56SAndreas Gohr $this->tableCalls[] = array('table_open',$call[1],$call[2]); 87*be906b56SAndreas Gohr $this->tableCalls[] = array('tablerow_open',array(),$call[2]); 88*be906b56SAndreas Gohr $this->firstCell = true; 89*be906b56SAndreas Gohr } 90*be906b56SAndreas Gohr 91*be906b56SAndreas Gohr protected function tableEnd($call) 92*be906b56SAndreas Gohr { 93*be906b56SAndreas Gohr $this->tableCalls[] = array('table_close',$call[1],$call[2]); 94*be906b56SAndreas Gohr $this->finalizeTable(); 95*be906b56SAndreas Gohr } 96*be906b56SAndreas Gohr 97*be906b56SAndreas Gohr protected function tableRowOpen($call) 98*be906b56SAndreas Gohr { 99*be906b56SAndreas Gohr $this->tableCalls[] = $call; 100*be906b56SAndreas Gohr $this->currentCols = 0; 101*be906b56SAndreas Gohr $this->firstCell = true; 102*be906b56SAndreas Gohr $this->lastCellType = 'tablecell'; 103*be906b56SAndreas Gohr $this->maxRows++; 104*be906b56SAndreas Gohr if ($this->inTableHead) { 105*be906b56SAndreas Gohr $this->currentRow = array('tablecell' => 0, 'tableheader' => 0); 106*be906b56SAndreas Gohr } 107*be906b56SAndreas Gohr } 108*be906b56SAndreas Gohr 109*be906b56SAndreas Gohr protected function tableRowClose($call) 110*be906b56SAndreas Gohr { 111*be906b56SAndreas Gohr if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) { 112*be906b56SAndreas Gohr $this->countTableHeadRows++; 113*be906b56SAndreas Gohr } 114*be906b56SAndreas Gohr // Strip off final cell opening and anything after it 115*be906b56SAndreas Gohr while ($discard = array_pop($this->tableCalls)) { 116*be906b56SAndreas Gohr if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { 117*be906b56SAndreas Gohr break; 118*be906b56SAndreas Gohr } 119*be906b56SAndreas Gohr if (!empty($this->currentRow[$discard[0]])) { 120*be906b56SAndreas Gohr $this->currentRow[$discard[0]]--; 121*be906b56SAndreas Gohr } 122*be906b56SAndreas Gohr } 123*be906b56SAndreas Gohr $this->tableCalls[] = array('tablerow_close', array(), $call[2]); 124*be906b56SAndreas Gohr 125*be906b56SAndreas Gohr if ($this->currentCols > $this->maxCols) { 126*be906b56SAndreas Gohr $this->maxCols = $this->currentCols; 127*be906b56SAndreas Gohr } 128*be906b56SAndreas Gohr } 129*be906b56SAndreas Gohr 130*be906b56SAndreas Gohr protected function isTableHeadRow() 131*be906b56SAndreas Gohr { 132*be906b56SAndreas Gohr $td = $this->currentRow['tablecell']; 133*be906b56SAndreas Gohr $th = $this->currentRow['tableheader']; 134*be906b56SAndreas Gohr 135*be906b56SAndreas Gohr if (!$th || $td > 2) return false; 136*be906b56SAndreas Gohr if (2*$td > $th) return false; 137*be906b56SAndreas Gohr 138*be906b56SAndreas Gohr return true; 139*be906b56SAndreas Gohr } 140*be906b56SAndreas Gohr 141*be906b56SAndreas Gohr protected function tableCell($call) 142*be906b56SAndreas Gohr { 143*be906b56SAndreas Gohr if ($this->inTableHead) { 144*be906b56SAndreas Gohr $this->currentRow[$call[0]]++; 145*be906b56SAndreas Gohr } 146*be906b56SAndreas Gohr if (!$this->firstCell) { 147*be906b56SAndreas Gohr // Increase the span 148*be906b56SAndreas Gohr $lastCall = end($this->tableCalls); 149*be906b56SAndreas Gohr 150*be906b56SAndreas Gohr // A cell call which follows an open cell means an empty cell so span 151*be906b56SAndreas Gohr if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') { 152*be906b56SAndreas Gohr $this->tableCalls[] = array('colspan',array(),$call[2]); 153*be906b56SAndreas Gohr } 154*be906b56SAndreas Gohr 155*be906b56SAndreas Gohr $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); 156*be906b56SAndreas Gohr $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]); 157*be906b56SAndreas Gohr $this->lastCellType = $call[0]; 158*be906b56SAndreas Gohr } else { 159*be906b56SAndreas Gohr $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]); 160*be906b56SAndreas Gohr $this->lastCellType = $call[0]; 161*be906b56SAndreas Gohr $this->firstCell = false; 162*be906b56SAndreas Gohr } 163*be906b56SAndreas Gohr 164*be906b56SAndreas Gohr $this->currentCols++; 165*be906b56SAndreas Gohr } 166*be906b56SAndreas Gohr 167*be906b56SAndreas Gohr protected function tableDefault($call) 168*be906b56SAndreas Gohr { 169*be906b56SAndreas Gohr $this->tableCalls[] = $call; 170*be906b56SAndreas Gohr } 171*be906b56SAndreas Gohr 172*be906b56SAndreas Gohr protected function finalizeTable() 173*be906b56SAndreas Gohr { 174*be906b56SAndreas Gohr 175*be906b56SAndreas Gohr // Add the max cols and rows to the table opening 176*be906b56SAndreas Gohr if ($this->tableCalls[0][0] == 'table_open') { 177*be906b56SAndreas Gohr // Adjust to num cols not num col delimeters 178*be906b56SAndreas Gohr $this->tableCalls[0][1][] = $this->maxCols - 1; 179*be906b56SAndreas Gohr $this->tableCalls[0][1][] = $this->maxRows; 180*be906b56SAndreas Gohr $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]); 181*be906b56SAndreas Gohr } else { 182*be906b56SAndreas Gohr trigger_error('First element in table call list is not table_open'); 183*be906b56SAndreas Gohr } 184*be906b56SAndreas Gohr 185*be906b56SAndreas Gohr $lastRow = 0; 186*be906b56SAndreas Gohr $lastCell = 0; 187*be906b56SAndreas Gohr $cellKey = array(); 188*be906b56SAndreas Gohr $toDelete = array(); 189*be906b56SAndreas Gohr 190*be906b56SAndreas Gohr // if still in tableheader, then there can be no table header 191*be906b56SAndreas Gohr // as all rows can't be within <THEAD> 192*be906b56SAndreas Gohr if ($this->inTableHead) { 193*be906b56SAndreas Gohr $this->inTableHead = false; 194*be906b56SAndreas Gohr $this->countTableHeadRows = 0; 195*be906b56SAndreas Gohr } 196*be906b56SAndreas Gohr 197*be906b56SAndreas Gohr // Look for the colspan elements and increment the colspan on the 198*be906b56SAndreas Gohr // previous non-empty opening cell. Once done, delete all the cells 199*be906b56SAndreas Gohr // that contain colspans 200*be906b56SAndreas Gohr for ($key = 0; $key < count($this->tableCalls); ++$key) { 201*be906b56SAndreas Gohr $call = $this->tableCalls[$key]; 202*be906b56SAndreas Gohr 203*be906b56SAndreas Gohr switch ($call[0]) { 204*be906b56SAndreas Gohr case 'table_open': 205*be906b56SAndreas Gohr if ($this->countTableHeadRows) { 206*be906b56SAndreas Gohr array_splice($this->tableCalls, $key+1, 0, array( 207*be906b56SAndreas Gohr array('tablethead_open', array(), $call[2]))); 208*be906b56SAndreas Gohr } 209*be906b56SAndreas Gohr break; 210*be906b56SAndreas Gohr 211*be906b56SAndreas Gohr case 'tablerow_open': 212*be906b56SAndreas Gohr $lastRow++; 213*be906b56SAndreas Gohr $lastCell = 0; 214*be906b56SAndreas Gohr break; 215*be906b56SAndreas Gohr 216*be906b56SAndreas Gohr case 'tablecell_open': 217*be906b56SAndreas Gohr case 'tableheader_open': 218*be906b56SAndreas Gohr $lastCell++; 219*be906b56SAndreas Gohr $cellKey[$lastRow][$lastCell] = $key; 220*be906b56SAndreas Gohr break; 221*be906b56SAndreas Gohr 222*be906b56SAndreas Gohr case 'table_align': 223*be906b56SAndreas Gohr $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open')); 224*be906b56SAndreas Gohr $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close')); 225*be906b56SAndreas Gohr // If the cell is empty, align left 226*be906b56SAndreas Gohr if ($prev && $next) { 227*be906b56SAndreas Gohr $this->tableCalls[$key-1][1][1] = 'left'; 228*be906b56SAndreas Gohr 229*be906b56SAndreas Gohr // If the previous element was a cell open, align right 230*be906b56SAndreas Gohr } elseif ($prev) { 231*be906b56SAndreas Gohr $this->tableCalls[$key-1][1][1] = 'right'; 232*be906b56SAndreas Gohr 233*be906b56SAndreas Gohr // If the next element is the close of an element, align either center or left 234*be906b56SAndreas Gohr } elseif ($next) { 235*be906b56SAndreas Gohr if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') { 236*be906b56SAndreas Gohr $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center'; 237*be906b56SAndreas Gohr } else { 238*be906b56SAndreas Gohr $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left'; 239*be906b56SAndreas Gohr } 240*be906b56SAndreas Gohr } 241*be906b56SAndreas Gohr 242*be906b56SAndreas Gohr // Now convert the whitespace back to cdata 243*be906b56SAndreas Gohr $this->tableCalls[$key][0] = 'cdata'; 244*be906b56SAndreas Gohr break; 245*be906b56SAndreas Gohr 246*be906b56SAndreas Gohr case 'colspan': 247*be906b56SAndreas Gohr $this->tableCalls[$key-1][1][0] = false; 248*be906b56SAndreas Gohr 249*be906b56SAndreas Gohr for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) { 250*be906b56SAndreas Gohr if ($this->tableCalls[$i][0] == 'tablecell_open' || 251*be906b56SAndreas Gohr $this->tableCalls[$i][0] == 'tableheader_open' 252*be906b56SAndreas Gohr ) { 253*be906b56SAndreas Gohr if (false !== $this->tableCalls[$i][1][0]) { 254*be906b56SAndreas Gohr $this->tableCalls[$i][1][0]++; 255*be906b56SAndreas Gohr break; 256*be906b56SAndreas Gohr } 257*be906b56SAndreas Gohr } 258*be906b56SAndreas Gohr } 259*be906b56SAndreas Gohr 260*be906b56SAndreas Gohr $toDelete[] = $key-1; 261*be906b56SAndreas Gohr $toDelete[] = $key; 262*be906b56SAndreas Gohr $toDelete[] = $key+1; 263*be906b56SAndreas Gohr break; 264*be906b56SAndreas Gohr 265*be906b56SAndreas Gohr case 'rowspan': 266*be906b56SAndreas Gohr if ($this->tableCalls[$key-1][0] == 'cdata') { 267*be906b56SAndreas Gohr // ignore rowspan if previous call was cdata (text mixed with :::) 268*be906b56SAndreas Gohr // we don't have to check next call as that wont match regex 269*be906b56SAndreas Gohr $this->tableCalls[$key][0] = 'cdata'; 270*be906b56SAndreas Gohr } else { 271*be906b56SAndreas Gohr $spanning_cell = null; 272*be906b56SAndreas Gohr 273*be906b56SAndreas Gohr // can't cross thead/tbody boundary 274*be906b56SAndreas Gohr if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) { 275*be906b56SAndreas Gohr for ($i = $lastRow-1; $i > 0; $i--) { 276*be906b56SAndreas Gohr if ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' || 277*be906b56SAndreas Gohr $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open' 278*be906b56SAndreas Gohr ) { 279*be906b56SAndreas Gohr if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) { 280*be906b56SAndreas Gohr $spanning_cell = $i; 281*be906b56SAndreas Gohr break; 282*be906b56SAndreas Gohr } 283*be906b56SAndreas Gohr } 284*be906b56SAndreas Gohr } 285*be906b56SAndreas Gohr } 286*be906b56SAndreas Gohr if (is_null($spanning_cell)) { 287*be906b56SAndreas Gohr // No spanning cell found, so convert this cell to 288*be906b56SAndreas Gohr // an empty one to avoid broken tables 289*be906b56SAndreas Gohr $this->tableCalls[$key][0] = 'cdata'; 290*be906b56SAndreas Gohr $this->tableCalls[$key][1][0] = ''; 291*be906b56SAndreas Gohr continue; 292*be906b56SAndreas Gohr } 293*be906b56SAndreas Gohr $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++; 294*be906b56SAndreas Gohr 295*be906b56SAndreas Gohr $this->tableCalls[$key-1][1][2] = false; 296*be906b56SAndreas Gohr 297*be906b56SAndreas Gohr $toDelete[] = $key-1; 298*be906b56SAndreas Gohr $toDelete[] = $key; 299*be906b56SAndreas Gohr $toDelete[] = $key+1; 300*be906b56SAndreas Gohr } 301*be906b56SAndreas Gohr break; 302*be906b56SAndreas Gohr 303*be906b56SAndreas Gohr case 'tablerow_close': 304*be906b56SAndreas Gohr // Fix broken tables by adding missing cells 305*be906b56SAndreas Gohr $moreCalls = array(); 306*be906b56SAndreas Gohr while (++$lastCell < $this->maxCols) { 307*be906b56SAndreas Gohr $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]); 308*be906b56SAndreas Gohr $moreCalls[] = array('cdata', array(''), $call[2]); 309*be906b56SAndreas Gohr $moreCalls[] = array('tablecell_close', array(), $call[2]); 310*be906b56SAndreas Gohr } 311*be906b56SAndreas Gohr $moreCallsLength = count($moreCalls); 312*be906b56SAndreas Gohr if ($moreCallsLength) { 313*be906b56SAndreas Gohr array_splice($this->tableCalls, $key, 0, $moreCalls); 314*be906b56SAndreas Gohr $key += $moreCallsLength; 315*be906b56SAndreas Gohr } 316*be906b56SAndreas Gohr 317*be906b56SAndreas Gohr if ($this->countTableHeadRows == $lastRow) { 318*be906b56SAndreas Gohr array_splice($this->tableCalls, $key+1, 0, array( 319*be906b56SAndreas Gohr array('tablethead_close', array(), $call[2]))); 320*be906b56SAndreas Gohr } 321*be906b56SAndreas Gohr break; 322*be906b56SAndreas Gohr } 323*be906b56SAndreas Gohr } 324*be906b56SAndreas Gohr 325*be906b56SAndreas Gohr // condense cdata 326*be906b56SAndreas Gohr $cnt = count($this->tableCalls); 327*be906b56SAndreas Gohr for ($key = 0; $key < $cnt; $key++) { 328*be906b56SAndreas Gohr if ($this->tableCalls[$key][0] == 'cdata') { 329*be906b56SAndreas Gohr $ckey = $key; 330*be906b56SAndreas Gohr $key++; 331*be906b56SAndreas Gohr while ($this->tableCalls[$key][0] == 'cdata') { 332*be906b56SAndreas Gohr $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0]; 333*be906b56SAndreas Gohr $toDelete[] = $key; 334*be906b56SAndreas Gohr $key++; 335*be906b56SAndreas Gohr } 336*be906b56SAndreas Gohr continue; 337*be906b56SAndreas Gohr } 338*be906b56SAndreas Gohr } 339*be906b56SAndreas Gohr 340*be906b56SAndreas Gohr foreach ($toDelete as $delete) { 341*be906b56SAndreas Gohr unset($this->tableCalls[$delete]); 342*be906b56SAndreas Gohr } 343*be906b56SAndreas Gohr $this->tableCalls = array_values($this->tableCalls); 344*be906b56SAndreas Gohr } 345*be906b56SAndreas Gohr} 346