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