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 ( 229 $this->tableCalls[$i][0] == 'tablecell_open' || 230 $this->tableCalls[$i][0] == 'tableheader_open' 231 ) { 232 if (false !== $this->tableCalls[$i][1][0]) { 233 $this->tableCalls[$i][1][0]++; 234 break; 235 } 236 } 237 } 238 239 $toDelete[] = $key - 1; 240 $toDelete[] = $key; 241 $toDelete[] = $key + 1; 242 break; 243 244 case 'rowspan': 245 if ($this->tableCalls[$key - 1][0] == 'cdata') { 246 // ignore rowspan if previous call was cdata (text mixed with :::) 247 // we don't have to check next call as that wont match regex 248 $this->tableCalls[$key][0] = 'cdata'; 249 } else { 250 $spanning_cell = null; 251 252 // can't cross thead/tbody boundary 253 if (!$this->countTableHeadRows || ($lastRow - 1 != $this->countTableHeadRows)) { 254 for ($i = $lastRow - 1; $i > 0; $i--) { 255 if ( 256 $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' || 257 $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open' 258 ) { 259 if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) { 260 $spanning_cell = $i; 261 break; 262 } 263 } 264 } 265 } 266 if (is_null($spanning_cell)) { 267 // No spanning cell found, so convert this cell to 268 // an empty one to avoid broken tables 269 $this->tableCalls[$key][0] = 'cdata'; 270 $this->tableCalls[$key][1][0] = ''; 271 break; 272 } 273 $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++; 274 275 $this->tableCalls[$key - 1][1][2] = false; 276 277 $toDelete[] = $key - 1; 278 $toDelete[] = $key; 279 $toDelete[] = $key + 1; 280 } 281 break; 282 283 case 'tablerow_close': 284 // Fix broken tables by adding missing cells 285 $moreCalls = []; 286 while (++$lastCell < $this->maxCols) { 287 $moreCalls[] = ['tablecell_open', [1, null, 1], $call[2]]; 288 $moreCalls[] = ['cdata', [''], $call[2]]; 289 $moreCalls[] = ['tablecell_close', [], $call[2]]; 290 } 291 $moreCallsLength = count($moreCalls); 292 if ($moreCallsLength) { 293 array_splice($this->tableCalls, $key, 0, $moreCalls); 294 $key += $moreCallsLength; 295 } 296 297 if ($this->countTableHeadRows == $lastRow) { 298 array_splice($this->tableCalls, $key + 1, 0, [['tablethead_close', [], $call[2]]]); 299 } 300 break; 301 } 302 } 303 304 // condense cdata 305 $cnt = count($this->tableCalls); 306 for ($key = 0; $key < $cnt; $key++) { 307 if ($this->tableCalls[$key][0] == 'cdata') { 308 $ckey = $key; 309 $key++; 310 while ($this->tableCalls[$key][0] == 'cdata') { 311 $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0]; 312 $toDelete[] = $key; 313 $key++; 314 } 315 continue; 316 } 317 } 318 319 foreach ($toDelete as $delete) { 320 unset($this->tableCalls[$delete]); 321 } 322 $this->tableCalls = array_values($this->tableCalls); 323 } 324} 325