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