[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/Parsing/Handler/ -> Table.php (source)

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