[ 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 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                              break;
 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  }