321 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			321 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace dokuwiki\Parsing\Handler;
 | |
| 
 | |
| class Table extends AbstractRewriter
 | |
| {
 | |
| 
 | |
|     protected $tableCalls = array();
 | |
|     protected $maxCols = 0;
 | |
|     protected $maxRows = 1;
 | |
|     protected $currentCols = 0;
 | |
|     protected $firstCell = false;
 | |
|     protected $lastCellType = 'tablecell';
 | |
|     protected $inTableHead = true;
 | |
|     protected $currentRow = array('tableheader' => 0, 'tablecell' => 0);
 | |
|     protected $countTableHeadRows = 0;
 | |
| 
 | |
|     /** @inheritdoc */
 | |
|     public function finalise()
 | |
|     {
 | |
|         $last_call = end($this->calls);
 | |
|         $this->writeCall(array('table_end',array(), $last_call[2]));
 | |
| 
 | |
|         $this->process();
 | |
|         $this->callWriter->finalise();
 | |
|         unset($this->callWriter);
 | |
|     }
 | |
| 
 | |
|     /** @inheritdoc */
 | |
|     public function process()
 | |
|     {
 | |
|         foreach ($this->calls as $call) {
 | |
|             switch ($call[0]) {
 | |
|                 case 'table_start':
 | |
|                     $this->tableStart($call);
 | |
|                     break;
 | |
|                 case 'table_row':
 | |
|                     $this->tableRowClose($call);
 | |
|                     $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
 | |
|                     break;
 | |
|                 case 'tableheader':
 | |
|                 case 'tablecell':
 | |
|                     $this->tableCell($call);
 | |
|                     break;
 | |
|                 case 'table_end':
 | |
|                     $this->tableRowClose($call);
 | |
|                     $this->tableEnd($call);
 | |
|                     break;
 | |
|                 default:
 | |
|                     $this->tableDefault($call);
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
|         $this->callWriter->writeCalls($this->tableCalls);
 | |
| 
 | |
|         return $this->callWriter;
 | |
|     }
 | |
| 
 | |
|     protected function tableStart($call)
 | |
|     {
 | |
|         $this->tableCalls[] = array('table_open',$call[1],$call[2]);
 | |
|         $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
 | |
|         $this->firstCell = true;
 | |
|     }
 | |
| 
 | |
|     protected function tableEnd($call)
 | |
|     {
 | |
|         $this->tableCalls[] = array('table_close',$call[1],$call[2]);
 | |
|         $this->finalizeTable();
 | |
|     }
 | |
| 
 | |
|     protected function tableRowOpen($call)
 | |
|     {
 | |
|         $this->tableCalls[] = $call;
 | |
|         $this->currentCols = 0;
 | |
|         $this->firstCell = true;
 | |
|         $this->lastCellType = 'tablecell';
 | |
|         $this->maxRows++;
 | |
|         if ($this->inTableHead) {
 | |
|             $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     protected function tableRowClose($call)
 | |
|     {
 | |
|         if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
 | |
|             $this->countTableHeadRows++;
 | |
|         }
 | |
|         // Strip off final cell opening and anything after it
 | |
|         while ($discard = array_pop($this->tableCalls)) {
 | |
|             if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
 | |
|                 break;
 | |
|             }
 | |
|             if (!empty($this->currentRow[$discard[0]])) {
 | |
|                 $this->currentRow[$discard[0]]--;
 | |
|             }
 | |
|         }
 | |
|         $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
 | |
| 
 | |
|         if ($this->currentCols > $this->maxCols) {
 | |
|             $this->maxCols = $this->currentCols;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     protected function isTableHeadRow()
 | |
|     {
 | |
|         $td = $this->currentRow['tablecell'];
 | |
|         $th = $this->currentRow['tableheader'];
 | |
| 
 | |
|         if (!$th || $td > 2) return false;
 | |
|         if (2*$td > $th) return false;
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     protected function tableCell($call)
 | |
|     {
 | |
|         if ($this->inTableHead) {
 | |
|             $this->currentRow[$call[0]]++;
 | |
|         }
 | |
|         if (!$this->firstCell) {
 | |
|             // Increase the span
 | |
|             $lastCall = end($this->tableCalls);
 | |
| 
 | |
|             // A cell call which follows an open cell means an empty cell so span
 | |
|             if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') {
 | |
|                 $this->tableCalls[] = array('colspan',array(),$call[2]);
 | |
|             }
 | |
| 
 | |
|             $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
 | |
|             $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
 | |
|             $this->lastCellType = $call[0];
 | |
|         } else {
 | |
|             $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
 | |
|             $this->lastCellType = $call[0];
 | |
|             $this->firstCell = false;
 | |
|         }
 | |
| 
 | |
|         $this->currentCols++;
 | |
|     }
 | |
| 
 | |
|     protected function tableDefault($call)
 | |
|     {
 | |
|         $this->tableCalls[] = $call;
 | |
|     }
 | |
| 
 | |
|     protected function finalizeTable()
 | |
|     {
 | |
| 
 | |
|         // Add the max cols and rows to the table opening
 | |
|         if ($this->tableCalls[0][0] == 'table_open') {
 | |
|             // Adjust to num cols not num col delimeters
 | |
|             $this->tableCalls[0][1][] = $this->maxCols - 1;
 | |
|             $this->tableCalls[0][1][] = $this->maxRows;
 | |
|             $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
 | |
|         } else {
 | |
|             trigger_error('First element in table call list is not table_open');
 | |
|         }
 | |
| 
 | |
|         $lastRow = 0;
 | |
|         $lastCell = 0;
 | |
|         $cellKey = array();
 | |
|         $toDelete = array();
 | |
| 
 | |
|         // if still in tableheader, then there can be no table header
 | |
|         // as all rows can't be within <THEAD>
 | |
|         if ($this->inTableHead) {
 | |
|             $this->inTableHead = false;
 | |
|             $this->countTableHeadRows = 0;
 | |
|         }
 | |
| 
 | |
|         // Look for the colspan elements and increment the colspan on the
 | |
|         // previous non-empty opening cell. Once done, delete all the cells
 | |
|         // that contain colspans
 | |
|         for ($key = 0; $key < count($this->tableCalls); ++$key) {
 | |
|             $call = $this->tableCalls[$key];
 | |
| 
 | |
|             switch ($call[0]) {
 | |
|                 case 'table_open':
 | |
|                     if ($this->countTableHeadRows) {
 | |
|                         array_splice($this->tableCalls, $key+1, 0, array(
 | |
|                                                           array('tablethead_open', array(), $call[2])));
 | |
|                     }
 | |
|                     break;
 | |
| 
 | |
|                 case 'tablerow_open':
 | |
|                     $lastRow++;
 | |
|                     $lastCell = 0;
 | |
|                     break;
 | |
| 
 | |
|                 case 'tablecell_open':
 | |
|                 case 'tableheader_open':
 | |
|                     $lastCell++;
 | |
|                     $cellKey[$lastRow][$lastCell] = $key;
 | |
|                     break;
 | |
| 
 | |
|                 case 'table_align':
 | |
|                     $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
 | |
|                     $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
 | |
|                     // If the cell is empty, align left
 | |
|                     if ($prev && $next) {
 | |
|                         $this->tableCalls[$key-1][1][1] = 'left';
 | |
| 
 | |
|                         // If the previous element was a cell open, align right
 | |
|                     } elseif ($prev) {
 | |
|                         $this->tableCalls[$key-1][1][1] = 'right';
 | |
| 
 | |
|                         // If the next element is the close of an element, align either center or left
 | |
|                     } elseif ($next) {
 | |
|                         if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
 | |
|                             $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
 | |
|                         } else {
 | |
|                             $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
 | |
|                         }
 | |
|                     }
 | |
| 
 | |
|                     // Now convert the whitespace back to cdata
 | |
|                     $this->tableCalls[$key][0] = 'cdata';
 | |
|                     break;
 | |
| 
 | |
|                 case 'colspan':
 | |
|                     $this->tableCalls[$key-1][1][0] = false;
 | |
| 
 | |
|                     for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
 | |
|                         if ($this->tableCalls[$i][0] == 'tablecell_open' ||
 | |
|                             $this->tableCalls[$i][0] == 'tableheader_open'
 | |
|                         ) {
 | |
|                             if (false !== $this->tableCalls[$i][1][0]) {
 | |
|                                 $this->tableCalls[$i][1][0]++;
 | |
|                                 break;
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
| 
 | |
|                     $toDelete[] = $key-1;
 | |
|                     $toDelete[] = $key;
 | |
|                     $toDelete[] = $key+1;
 | |
|                     break;
 | |
| 
 | |
|                 case 'rowspan':
 | |
|                     if ($this->tableCalls[$key-1][0] == 'cdata') {
 | |
|                         // ignore rowspan if previous call was cdata (text mixed with :::)
 | |
|                         // we don't have to check next call as that wont match regex
 | |
|                         $this->tableCalls[$key][0] = 'cdata';
 | |
|                     } else {
 | |
|                         $spanning_cell = null;
 | |
| 
 | |
|                         // can't cross thead/tbody boundary
 | |
|                         if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
 | |
|                             for ($i = $lastRow-1; $i > 0; $i--) {
 | |
|                                 if ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
 | |
|                                     $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
 | |
|                                 ) {
 | |
|                                     if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
 | |
|                                         $spanning_cell = $i;
 | |
|                                         break;
 | |
|                                     }
 | |
|                                 }
 | |
|                             }
 | |
|                         }
 | |
|                         if (is_null($spanning_cell)) {
 | |
|                             // No spanning cell found, so convert this cell to
 | |
|                             // an empty one to avoid broken tables
 | |
|                             $this->tableCalls[$key][0] = 'cdata';
 | |
|                             $this->tableCalls[$key][1][0] = '';
 | |
|                             break;
 | |
|                         }
 | |
|                         $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
 | |
| 
 | |
|                         $this->tableCalls[$key-1][1][2] = false;
 | |
| 
 | |
|                         $toDelete[] = $key-1;
 | |
|                         $toDelete[] = $key;
 | |
|                         $toDelete[] = $key+1;
 | |
|                     }
 | |
|                     break;
 | |
| 
 | |
|                 case 'tablerow_close':
 | |
|                     // Fix broken tables by adding missing cells
 | |
|                     $moreCalls = array();
 | |
|                     while (++$lastCell < $this->maxCols) {
 | |
|                         $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
 | |
|                         $moreCalls[] = array('cdata', array(''), $call[2]);
 | |
|                         $moreCalls[] = array('tablecell_close', array(), $call[2]);
 | |
|                     }
 | |
|                     $moreCallsLength = count($moreCalls);
 | |
|                     if ($moreCallsLength) {
 | |
|                         array_splice($this->tableCalls, $key, 0, $moreCalls);
 | |
|                         $key += $moreCallsLength;
 | |
|                     }
 | |
| 
 | |
|                     if ($this->countTableHeadRows == $lastRow) {
 | |
|                         array_splice($this->tableCalls, $key+1, 0, array(
 | |
|                             array('tablethead_close', array(), $call[2])));
 | |
|                     }
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // condense cdata
 | |
|         $cnt = count($this->tableCalls);
 | |
|         for ($key = 0; $key < $cnt; $key++) {
 | |
|             if ($this->tableCalls[$key][0] == 'cdata') {
 | |
|                 $ckey = $key;
 | |
|                 $key++;
 | |
|                 while ($this->tableCalls[$key][0] == 'cdata') {
 | |
|                     $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
 | |
|                     $toDelete[] = $key;
 | |
|                     $key++;
 | |
|                 }
 | |
|                 continue;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         foreach ($toDelete as $delete) {
 | |
|             unset($this->tableCalls[$delete]);
 | |
|         }
 | |
|         $this->tableCalls = array_values($this->tableCalls);
 | |
|     }
 | |
| }
 |