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