0, 'tablecell' => 0]; protected $countTableHeadRows = 0; /** @inheritdoc */ public function finalise() { $last_call = end($this->calls); $this->writeCall(['table_end', [], $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(['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[] = ['table_open', $call[1], $call[2]]; $this->tableCalls[] = ['tablerow_open', [], $call[2]]; $this->firstCell = true; } protected function tableEnd($call) { $this->tableCalls[] = ['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 = ['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[] = ['tablerow_close', [], $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[] = ['colspan', [], $call[2]]; } $this->tableCalls[] = [$this->lastCellType . '_close', [], $call[2]]; $this->tableCalls[] = [$call[0] . '_open', [1, null, 1], $call[2]]; $this->lastCellType = $call[0]; } else { $this->tableCalls[] = [$call[0] . '_open', [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 = []; $toDelete = []; // if still in tableheader, then there can be no table header // as all rows can't be within 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 $key = -1; while (++$key < count($this->tableCalls)) { $call = $this->tableCalls[$key]; switch ($call[0]) { case 'table_open': if ($this->countTableHeadRows) { array_splice($this->tableCalls, $key + 1, 0, [['tablethead_open', [], $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], ['tablecell_open', 'tableheader_open']); $next = in_array($this->tableCalls[$key + 1][0], ['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 = []; while (++$lastCell < $this->maxCols) { $moreCalls[] = ['tablecell_open', [1, null, 1], $call[2]]; $moreCalls[] = ['cdata', [''], $call[2]]; $moreCalls[] = ['tablecell_close', [], $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, [['tablethead_close', [], $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); } }