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