[ 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  
 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          $key = -1;
 175          while (++$key < count($this->tableCalls)) {
 176              $call = $this->tableCalls[$key];
 177  
 178              switch ($call[0]) {
 179                  case 'table_open':
 180                      if ($this->countTableHeadRows) {
 181                          array_splice($this->tableCalls, $key + 1, 0, [['tablethead_open', [], $call[2]]]);
 182                      }
 183                      break;
 184  
 185                  case 'tablerow_open':
 186                      $lastRow++;
 187                      $lastCell = 0;
 188                      break;
 189  
 190                  case 'tablecell_open':
 191                  case 'tableheader_open':
 192                      $lastCell++;
 193                      $cellKey[$lastRow][$lastCell] = $key;
 194                      break;
 195  
 196                  case 'table_align':
 197                      $prev = in_array($this->tableCalls[$key - 1][0], ['tablecell_open', 'tableheader_open']);
 198                      $next = in_array($this->tableCalls[$key + 1][0], ['tablecell_close', 'tableheader_close']);
 199                      // If the cell is empty, align left
 200                      if ($prev && $next) {
 201                          $this->tableCalls[$key - 1][1][1] = 'left';
 202  
 203                          // If the previous element was a cell open, align right
 204                      } elseif ($prev) {
 205                          $this->tableCalls[$key - 1][1][1] = 'right';
 206  
 207                          // If the next element is the close of an element, align either center or left
 208                      } elseif ($next) {
 209                          if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
 210                              $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
 211                          } else {
 212                              $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
 213                          }
 214                      }
 215  
 216                      // Now convert the whitespace back to cdata
 217                      $this->tableCalls[$key][0] = 'cdata';
 218                      break;
 219  
 220                  case 'colspan':
 221                      $this->tableCalls[$key - 1][1][0] = false;
 222  
 223                      for ($i = $key - 2; $i >= $cellKey[$lastRow][1]; $i--) {
 224                          if (
 225                              $this->tableCalls[$i][0] == 'tablecell_open' ||
 226                              $this->tableCalls[$i][0] == 'tableheader_open'
 227                          ) {
 228                              if (false !== $this->tableCalls[$i][1][0]) {
 229                                  $this->tableCalls[$i][1][0]++;
 230                                  break;
 231                              }
 232                          }
 233                      }
 234  
 235                      $toDelete[] = $key - 1;
 236                      $toDelete[] = $key;
 237                      $toDelete[] = $key + 1;
 238                      break;
 239  
 240                  case 'rowspan':
 241                      if ($this->tableCalls[$key - 1][0] == 'cdata') {
 242                          // ignore rowspan if previous call was cdata (text mixed with :::)
 243                          // we don't have to check next call as that wont match regex
 244                          $this->tableCalls[$key][0] = 'cdata';
 245                      } else {
 246                          $spanning_cell = null;
 247  
 248                          // can't cross thead/tbody boundary
 249                          if (!$this->countTableHeadRows || ($lastRow - 1 != $this->countTableHeadRows)) {
 250                              for ($i = $lastRow - 1; $i > 0; $i--) {
 251                                  if (
 252                                      $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
 253                                      $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
 254                                  ) {
 255                                      if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
 256                                          $spanning_cell = $i;
 257                                          break;
 258                                      }
 259                                  }
 260                              }
 261                          }
 262                          if (is_null($spanning_cell)) {
 263                              // No spanning cell found, so convert this cell to
 264                              // an empty one to avoid broken tables
 265                              $this->tableCalls[$key][0] = 'cdata';
 266                              $this->tableCalls[$key][1][0] = '';
 267                              break;
 268                          }
 269                          $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
 270  
 271                          $this->tableCalls[$key - 1][1][2] = false;
 272  
 273                          $toDelete[] = $key - 1;
 274                          $toDelete[] = $key;
 275                          $toDelete[] = $key + 1;
 276                      }
 277                      break;
 278  
 279                  case 'tablerow_close':
 280                      // Fix broken tables by adding missing cells
 281                      $moreCalls = [];
 282                      while (++$lastCell < $this->maxCols) {
 283                          $moreCalls[] = ['tablecell_open', [1, null, 1], $call[2]];
 284                          $moreCalls[] = ['cdata', [''], $call[2]];
 285                          $moreCalls[] = ['tablecell_close', [], $call[2]];
 286                      }
 287                      $moreCallsLength = count($moreCalls);
 288                      if ($moreCallsLength) {
 289                          array_splice($this->tableCalls, $key, 0, $moreCalls);
 290                          $key += $moreCallsLength;
 291                      }
 292  
 293                      if ($this->countTableHeadRows == $lastRow) {
 294                          array_splice($this->tableCalls, $key + 1, 0, [['tablethead_close', [], $call[2]]]);
 295                      }
 296                      break;
 297              }
 298          }
 299  
 300          // condense cdata
 301          $cnt = count($this->tableCalls);
 302          for ($key = 0; $key < $cnt; $key++) {
 303              if ($this->tableCalls[$key][0] == 'cdata') {
 304                  $ckey = $key;
 305                  $key++;
 306                  while ($this->tableCalls[$key][0] == 'cdata') {
 307                      $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
 308                      $toDelete[] = $key;
 309                      $key++;
 310                  }
 311                  continue;
 312              }
 313          }
 314  
 315          foreach ($toDelete as $delete) {
 316              unset($this->tableCalls[$delete]);
 317          }
 318          $this->tableCalls = array_values($this->tableCalls);
 319      }
 320  }