[ 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  
   8      protected $tableCalls = array();
   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 = array('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(array('table_end',array(), $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(array('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[] = array('table_open',$call[1],$call[2]);
  62          $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
  63          $this->firstCell = true;
  64      }
  65  
  66      protected function tableEnd($call)
  67      {
  68          $this->tableCalls[] = array('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 = array('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[] = array('tablerow_close', array(), $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[] = array('colspan',array(),$call[2]);
 128              }
 129  
 130              $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
 131              $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
 132              $this->lastCellType = $call[0];
 133          } else {
 134              $this->tableCalls[] = array($call[0].'_open',array(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 = array();
 163          $toDelete = array();
 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  
 172          // Look for the colspan elements and increment the colspan on the
 173          // previous non-empty opening cell. Once done, delete all the cells
 174          // that contain colspans
 175          for ($key = 0; $key < count($this->tableCalls); ++$key) {
 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, array(
 182                                                            array('tablethead_open', array(), $call[2])));
 183                      }
 184                      break;
 185  
 186                  case 'tablerow_open':
 187                      $lastRow++;
 188                      $lastCell = 0;
 189                      break;
 190  
 191                  case 'tablecell_open':
 192                  case 'tableheader_open':
 193                      $lastCell++;
 194                      $cellKey[$lastRow][$lastCell] = $key;
 195                      break;
 196  
 197                  case 'table_align':
 198                      $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
 199                      $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
 200                      // If the cell is empty, align left
 201                      if ($prev && $next) {
 202                          $this->tableCalls[$key-1][1][1] = 'left';
 203  
 204                          // If the previous element was a cell open, align right
 205                      } elseif ($prev) {
 206                          $this->tableCalls[$key-1][1][1] = 'right';
 207  
 208                          // If the next element is the close of an element, align either center or left
 209                      } elseif ($next) {
 210                          if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
 211                              $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
 212                          } else {
 213                              $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
 214                          }
 215                      }
 216  
 217                      // Now convert the whitespace back to cdata
 218                      $this->tableCalls[$key][0] = 'cdata';
 219                      break;
 220  
 221                  case 'colspan':
 222                      $this->tableCalls[$key-1][1][0] = false;
 223  
 224                      for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
 225                          if ($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 ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
 252                                      $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
 253                                  ) {
 254                                      if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
 255                                          $spanning_cell = $i;
 256                                          break;
 257                                      }
 258                                  }
 259                              }
 260                          }
 261                          if (is_null($spanning_cell)) {
 262                              // No spanning cell found, so convert this cell to
 263                              // an empty one to avoid broken tables
 264                              $this->tableCalls[$key][0] = 'cdata';
 265                              $this->tableCalls[$key][1][0] = '';
 266                              break;
 267                          }
 268                          $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
 269  
 270                          $this->tableCalls[$key-1][1][2] = false;
 271  
 272                          $toDelete[] = $key-1;
 273                          $toDelete[] = $key;
 274                          $toDelete[] = $key+1;
 275                      }
 276                      break;
 277  
 278                  case 'tablerow_close':
 279                      // Fix broken tables by adding missing cells
 280                      $moreCalls = array();
 281                      while (++$lastCell < $this->maxCols) {
 282                          $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
 283                          $moreCalls[] = array('cdata', array(''), $call[2]);
 284                          $moreCalls[] = array('tablecell_close', array(), $call[2]);
 285                      }
 286                      $moreCallsLength = count($moreCalls);
 287                      if ($moreCallsLength) {
 288                          array_splice($this->tableCalls, $key, 0, $moreCalls);
 289                          $key += $moreCallsLength;
 290                      }
 291  
 292                      if ($this->countTableHeadRows == $lastRow) {
 293                          array_splice($this->tableCalls, $key+1, 0, array(
 294                              array('tablethead_close', array(), $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  }