xref: /dokuwiki/inc/Parsing/Handler/Table.php (revision 277113f107ff36d064d0875e210e27a19f542f71)
1be906b56SAndreas Gohr<?php
2be906b56SAndreas Gohr
3be906b56SAndreas Gohrnamespace dokuwiki\Parsing\Handler;
4be906b56SAndreas Gohr
5be906b56SAndreas Gohrclass Table implements ReWriterInterface
6be906b56SAndreas Gohr{
7be906b56SAndreas Gohr
8be906b56SAndreas Gohr    /** @var CallWriterInterface original CallWriter */
9be906b56SAndreas Gohr    protected $callWriter;
10be906b56SAndreas Gohr
11be906b56SAndreas Gohr    protected $calls = array();
12be906b56SAndreas Gohr    protected $tableCalls = array();
13be906b56SAndreas Gohr    protected $maxCols = 0;
14be906b56SAndreas Gohr    protected $maxRows = 1;
15be906b56SAndreas Gohr    protected $currentCols = 0;
16be906b56SAndreas Gohr    protected $firstCell = false;
17be906b56SAndreas Gohr    protected $lastCellType = 'tablecell';
18be906b56SAndreas Gohr    protected $inTableHead = true;
19be906b56SAndreas Gohr    protected $currentRow = array('tableheader' => 0, 'tablecell' => 0);
20be906b56SAndreas Gohr    protected $countTableHeadRows = 0;
21be906b56SAndreas Gohr
22be906b56SAndreas Gohr    /** @inheritdoc */
23be906b56SAndreas Gohr    public function __construct(CallWriterInterface $CallWriter)
24be906b56SAndreas Gohr    {
25be906b56SAndreas Gohr        $this->callWriter = $CallWriter;
26be906b56SAndreas Gohr    }
27be906b56SAndreas Gohr
28be906b56SAndreas Gohr    /** @inheritdoc */
29be906b56SAndreas Gohr    public function writeCall($call)
30be906b56SAndreas Gohr    {
31be906b56SAndreas Gohr        $this->calls[] = $call;
32be906b56SAndreas Gohr    }
33be906b56SAndreas Gohr
34be906b56SAndreas Gohr    /**
35be906b56SAndreas Gohr     * @inheritdoc
36be906b56SAndreas Gohr     * Probably not needed but just in case...
37be906b56SAndreas Gohr     */
38be906b56SAndreas Gohr    public function writeCalls($calls)
39be906b56SAndreas Gohr    {
40be906b56SAndreas Gohr        $this->calls = array_merge($this->calls, $calls);
41be906b56SAndreas Gohr    }
42be906b56SAndreas Gohr
43be906b56SAndreas Gohr    /** @inheritdoc */
44be906b56SAndreas Gohr    public function finalise()
45be906b56SAndreas Gohr    {
46be906b56SAndreas Gohr        $last_call = end($this->calls);
47be906b56SAndreas Gohr        $this->writeCall(array('table_end',array(), $last_call[2]));
48be906b56SAndreas Gohr
49be906b56SAndreas Gohr        $this->process();
50be906b56SAndreas Gohr        $this->callWriter->finalise();
51be906b56SAndreas Gohr        unset($this->callWriter);
52be906b56SAndreas Gohr    }
53be906b56SAndreas Gohr
54be906b56SAndreas Gohr    /** @inheritdoc */
55be906b56SAndreas Gohr    public function process()
56be906b56SAndreas Gohr    {
57be906b56SAndreas Gohr        foreach ($this->calls as $call) {
58be906b56SAndreas Gohr            switch ($call[0]) {
59be906b56SAndreas Gohr                case 'table_start':
60be906b56SAndreas Gohr                    $this->tableStart($call);
61be906b56SAndreas Gohr                    break;
62be906b56SAndreas Gohr                case 'table_row':
63be906b56SAndreas Gohr                    $this->tableRowClose($call);
64be906b56SAndreas Gohr                    $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
65be906b56SAndreas Gohr                    break;
66be906b56SAndreas Gohr                case 'tableheader':
67be906b56SAndreas Gohr                case 'tablecell':
68be906b56SAndreas Gohr                    $this->tableCell($call);
69be906b56SAndreas Gohr                    break;
70be906b56SAndreas Gohr                case 'table_end':
71be906b56SAndreas Gohr                    $this->tableRowClose($call);
72be906b56SAndreas Gohr                    $this->tableEnd($call);
73be906b56SAndreas Gohr                    break;
74be906b56SAndreas Gohr                default:
75be906b56SAndreas Gohr                    $this->tableDefault($call);
76be906b56SAndreas Gohr                    break;
77be906b56SAndreas Gohr            }
78be906b56SAndreas Gohr        }
79be906b56SAndreas Gohr        $this->callWriter->writeCalls($this->tableCalls);
80be906b56SAndreas Gohr
81be906b56SAndreas Gohr        return $this->callWriter;
82be906b56SAndreas Gohr    }
83be906b56SAndreas Gohr
84be906b56SAndreas Gohr    protected function tableStart($call)
85be906b56SAndreas Gohr    {
86be906b56SAndreas Gohr        $this->tableCalls[] = array('table_open',$call[1],$call[2]);
87be906b56SAndreas Gohr        $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
88be906b56SAndreas Gohr        $this->firstCell = true;
89be906b56SAndreas Gohr    }
90be906b56SAndreas Gohr
91be906b56SAndreas Gohr    protected function tableEnd($call)
92be906b56SAndreas Gohr    {
93be906b56SAndreas Gohr        $this->tableCalls[] = array('table_close',$call[1],$call[2]);
94be906b56SAndreas Gohr        $this->finalizeTable();
95be906b56SAndreas Gohr    }
96be906b56SAndreas Gohr
97be906b56SAndreas Gohr    protected function tableRowOpen($call)
98be906b56SAndreas Gohr    {
99be906b56SAndreas Gohr        $this->tableCalls[] = $call;
100be906b56SAndreas Gohr        $this->currentCols = 0;
101be906b56SAndreas Gohr        $this->firstCell = true;
102be906b56SAndreas Gohr        $this->lastCellType = 'tablecell';
103be906b56SAndreas Gohr        $this->maxRows++;
104be906b56SAndreas Gohr        if ($this->inTableHead) {
105be906b56SAndreas Gohr            $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
106be906b56SAndreas Gohr        }
107be906b56SAndreas Gohr    }
108be906b56SAndreas Gohr
109be906b56SAndreas Gohr    protected function tableRowClose($call)
110be906b56SAndreas Gohr    {
111be906b56SAndreas Gohr        if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
112be906b56SAndreas Gohr            $this->countTableHeadRows++;
113be906b56SAndreas Gohr        }
114be906b56SAndreas Gohr        // Strip off final cell opening and anything after it
115be906b56SAndreas Gohr        while ($discard = array_pop($this->tableCalls)) {
116be906b56SAndreas Gohr            if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
117be906b56SAndreas Gohr                break;
118be906b56SAndreas Gohr            }
119be906b56SAndreas Gohr            if (!empty($this->currentRow[$discard[0]])) {
120be906b56SAndreas Gohr                $this->currentRow[$discard[0]]--;
121be906b56SAndreas Gohr            }
122be906b56SAndreas Gohr        }
123be906b56SAndreas Gohr        $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
124be906b56SAndreas Gohr
125be906b56SAndreas Gohr        if ($this->currentCols > $this->maxCols) {
126be906b56SAndreas Gohr            $this->maxCols = $this->currentCols;
127be906b56SAndreas Gohr        }
128be906b56SAndreas Gohr    }
129be906b56SAndreas Gohr
130be906b56SAndreas Gohr    protected function isTableHeadRow()
131be906b56SAndreas Gohr    {
132be906b56SAndreas Gohr        $td = $this->currentRow['tablecell'];
133be906b56SAndreas Gohr        $th = $this->currentRow['tableheader'];
134be906b56SAndreas Gohr
135be906b56SAndreas Gohr        if (!$th || $td > 2) return false;
136be906b56SAndreas Gohr        if (2*$td > $th) return false;
137be906b56SAndreas Gohr
138be906b56SAndreas Gohr        return true;
139be906b56SAndreas Gohr    }
140be906b56SAndreas Gohr
141be906b56SAndreas Gohr    protected function tableCell($call)
142be906b56SAndreas Gohr    {
143be906b56SAndreas Gohr        if ($this->inTableHead) {
144be906b56SAndreas Gohr            $this->currentRow[$call[0]]++;
145be906b56SAndreas Gohr        }
146be906b56SAndreas Gohr        if (!$this->firstCell) {
147be906b56SAndreas Gohr            // Increase the span
148be906b56SAndreas Gohr            $lastCall = end($this->tableCalls);
149be906b56SAndreas Gohr
150be906b56SAndreas Gohr            // A cell call which follows an open cell means an empty cell so span
151be906b56SAndreas Gohr            if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') {
152be906b56SAndreas Gohr                $this->tableCalls[] = array('colspan',array(),$call[2]);
153be906b56SAndreas Gohr            }
154be906b56SAndreas Gohr
155be906b56SAndreas Gohr            $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
156be906b56SAndreas Gohr            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
157be906b56SAndreas Gohr            $this->lastCellType = $call[0];
158be906b56SAndreas Gohr        } else {
159be906b56SAndreas Gohr            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
160be906b56SAndreas Gohr            $this->lastCellType = $call[0];
161be906b56SAndreas Gohr            $this->firstCell = false;
162be906b56SAndreas Gohr        }
163be906b56SAndreas Gohr
164be906b56SAndreas Gohr        $this->currentCols++;
165be906b56SAndreas Gohr    }
166be906b56SAndreas Gohr
167be906b56SAndreas Gohr    protected function tableDefault($call)
168be906b56SAndreas Gohr    {
169be906b56SAndreas Gohr        $this->tableCalls[] = $call;
170be906b56SAndreas Gohr    }
171be906b56SAndreas Gohr
172be906b56SAndreas Gohr    protected function finalizeTable()
173be906b56SAndreas Gohr    {
174be906b56SAndreas Gohr
175be906b56SAndreas Gohr        // Add the max cols and rows to the table opening
176be906b56SAndreas Gohr        if ($this->tableCalls[0][0] == 'table_open') {
177be906b56SAndreas Gohr            // Adjust to num cols not num col delimeters
178be906b56SAndreas Gohr            $this->tableCalls[0][1][] = $this->maxCols - 1;
179be906b56SAndreas Gohr            $this->tableCalls[0][1][] = $this->maxRows;
180be906b56SAndreas Gohr            $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
181be906b56SAndreas Gohr        } else {
182be906b56SAndreas Gohr            trigger_error('First element in table call list is not table_open');
183be906b56SAndreas Gohr        }
184be906b56SAndreas Gohr
185be906b56SAndreas Gohr        $lastRow = 0;
186be906b56SAndreas Gohr        $lastCell = 0;
187be906b56SAndreas Gohr        $cellKey = array();
188be906b56SAndreas Gohr        $toDelete = array();
189be906b56SAndreas Gohr
190be906b56SAndreas Gohr        // if still in tableheader, then there can be no table header
191be906b56SAndreas Gohr        // as all rows can't be within <THEAD>
192be906b56SAndreas Gohr        if ($this->inTableHead) {
193be906b56SAndreas Gohr            $this->inTableHead = false;
194be906b56SAndreas Gohr            $this->countTableHeadRows = 0;
195be906b56SAndreas Gohr        }
196be906b56SAndreas Gohr
197be906b56SAndreas Gohr        // Look for the colspan elements and increment the colspan on the
198be906b56SAndreas Gohr        // previous non-empty opening cell. Once done, delete all the cells
199be906b56SAndreas Gohr        // that contain colspans
200be906b56SAndreas Gohr        for ($key = 0; $key < count($this->tableCalls); ++$key) {
201be906b56SAndreas Gohr            $call = $this->tableCalls[$key];
202be906b56SAndreas Gohr
203be906b56SAndreas Gohr            switch ($call[0]) {
204be906b56SAndreas Gohr                case 'table_open':
205be906b56SAndreas Gohr                    if ($this->countTableHeadRows) {
206be906b56SAndreas Gohr                        array_splice($this->tableCalls, $key+1, 0, array(
207be906b56SAndreas Gohr                                                          array('tablethead_open', array(), $call[2])));
208be906b56SAndreas Gohr                    }
209be906b56SAndreas Gohr                    break;
210be906b56SAndreas Gohr
211be906b56SAndreas Gohr                case 'tablerow_open':
212be906b56SAndreas Gohr                    $lastRow++;
213be906b56SAndreas Gohr                    $lastCell = 0;
214be906b56SAndreas Gohr                    break;
215be906b56SAndreas Gohr
216be906b56SAndreas Gohr                case 'tablecell_open':
217be906b56SAndreas Gohr                case 'tableheader_open':
218be906b56SAndreas Gohr                    $lastCell++;
219be906b56SAndreas Gohr                    $cellKey[$lastRow][$lastCell] = $key;
220be906b56SAndreas Gohr                    break;
221be906b56SAndreas Gohr
222be906b56SAndreas Gohr                case 'table_align':
223be906b56SAndreas Gohr                    $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
224be906b56SAndreas Gohr                    $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
225be906b56SAndreas Gohr                    // If the cell is empty, align left
226be906b56SAndreas Gohr                    if ($prev && $next) {
227be906b56SAndreas Gohr                        $this->tableCalls[$key-1][1][1] = 'left';
228be906b56SAndreas Gohr
229be906b56SAndreas Gohr                        // If the previous element was a cell open, align right
230be906b56SAndreas Gohr                    } elseif ($prev) {
231be906b56SAndreas Gohr                        $this->tableCalls[$key-1][1][1] = 'right';
232be906b56SAndreas Gohr
233be906b56SAndreas Gohr                        // If the next element is the close of an element, align either center or left
234be906b56SAndreas Gohr                    } elseif ($next) {
235be906b56SAndreas Gohr                        if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
236be906b56SAndreas Gohr                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
237be906b56SAndreas Gohr                        } else {
238be906b56SAndreas Gohr                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
239be906b56SAndreas Gohr                        }
240be906b56SAndreas Gohr                    }
241be906b56SAndreas Gohr
242be906b56SAndreas Gohr                    // Now convert the whitespace back to cdata
243be906b56SAndreas Gohr                    $this->tableCalls[$key][0] = 'cdata';
244be906b56SAndreas Gohr                    break;
245be906b56SAndreas Gohr
246be906b56SAndreas Gohr                case 'colspan':
247be906b56SAndreas Gohr                    $this->tableCalls[$key-1][1][0] = false;
248be906b56SAndreas Gohr
249be906b56SAndreas Gohr                    for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
250be906b56SAndreas Gohr                        if ($this->tableCalls[$i][0] == 'tablecell_open' ||
251be906b56SAndreas Gohr                            $this->tableCalls[$i][0] == 'tableheader_open'
252be906b56SAndreas Gohr                        ) {
253be906b56SAndreas Gohr                            if (false !== $this->tableCalls[$i][1][0]) {
254be906b56SAndreas Gohr                                $this->tableCalls[$i][1][0]++;
255be906b56SAndreas Gohr                                break;
256be906b56SAndreas Gohr                            }
257be906b56SAndreas Gohr                        }
258be906b56SAndreas Gohr                    }
259be906b56SAndreas Gohr
260be906b56SAndreas Gohr                    $toDelete[] = $key-1;
261be906b56SAndreas Gohr                    $toDelete[] = $key;
262be906b56SAndreas Gohr                    $toDelete[] = $key+1;
263be906b56SAndreas Gohr                    break;
264be906b56SAndreas Gohr
265be906b56SAndreas Gohr                case 'rowspan':
266be906b56SAndreas Gohr                    if ($this->tableCalls[$key-1][0] == 'cdata') {
267be906b56SAndreas Gohr                        // ignore rowspan if previous call was cdata (text mixed with :::)
268be906b56SAndreas Gohr                        // we don't have to check next call as that wont match regex
269be906b56SAndreas Gohr                        $this->tableCalls[$key][0] = 'cdata';
270be906b56SAndreas Gohr                    } else {
271be906b56SAndreas Gohr                        $spanning_cell = null;
272be906b56SAndreas Gohr
273be906b56SAndreas Gohr                        // can't cross thead/tbody boundary
274be906b56SAndreas Gohr                        if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
275be906b56SAndreas Gohr                            for ($i = $lastRow-1; $i > 0; $i--) {
276be906b56SAndreas Gohr                                if ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
277be906b56SAndreas Gohr                                    $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
278be906b56SAndreas Gohr                                ) {
279be906b56SAndreas Gohr                                    if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
280be906b56SAndreas Gohr                                        $spanning_cell = $i;
281be906b56SAndreas Gohr                                        break;
282be906b56SAndreas Gohr                                    }
283be906b56SAndreas Gohr                                }
284be906b56SAndreas Gohr                            }
285be906b56SAndreas Gohr                        }
286be906b56SAndreas Gohr                        if (is_null($spanning_cell)) {
287be906b56SAndreas Gohr                            // No spanning cell found, so convert this cell to
288be906b56SAndreas Gohr                            // an empty one to avoid broken tables
289be906b56SAndreas Gohr                            $this->tableCalls[$key][0] = 'cdata';
290be906b56SAndreas Gohr                            $this->tableCalls[$key][1][0] = '';
291*277113f1SAndreas Gohr                            break;
292be906b56SAndreas Gohr                        }
293be906b56SAndreas Gohr                        $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
294be906b56SAndreas Gohr
295be906b56SAndreas Gohr                        $this->tableCalls[$key-1][1][2] = false;
296be906b56SAndreas Gohr
297be906b56SAndreas Gohr                        $toDelete[] = $key-1;
298be906b56SAndreas Gohr                        $toDelete[] = $key;
299be906b56SAndreas Gohr                        $toDelete[] = $key+1;
300be906b56SAndreas Gohr                    }
301be906b56SAndreas Gohr                    break;
302be906b56SAndreas Gohr
303be906b56SAndreas Gohr                case 'tablerow_close':
304be906b56SAndreas Gohr                    // Fix broken tables by adding missing cells
305be906b56SAndreas Gohr                    $moreCalls = array();
306be906b56SAndreas Gohr                    while (++$lastCell < $this->maxCols) {
307be906b56SAndreas Gohr                        $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
308be906b56SAndreas Gohr                        $moreCalls[] = array('cdata', array(''), $call[2]);
309be906b56SAndreas Gohr                        $moreCalls[] = array('tablecell_close', array(), $call[2]);
310be906b56SAndreas Gohr                    }
311be906b56SAndreas Gohr                    $moreCallsLength = count($moreCalls);
312be906b56SAndreas Gohr                    if ($moreCallsLength) {
313be906b56SAndreas Gohr                        array_splice($this->tableCalls, $key, 0, $moreCalls);
314be906b56SAndreas Gohr                        $key += $moreCallsLength;
315be906b56SAndreas Gohr                    }
316be906b56SAndreas Gohr
317be906b56SAndreas Gohr                    if ($this->countTableHeadRows == $lastRow) {
318be906b56SAndreas Gohr                        array_splice($this->tableCalls, $key+1, 0, array(
319be906b56SAndreas Gohr                            array('tablethead_close', array(), $call[2])));
320be906b56SAndreas Gohr                    }
321be906b56SAndreas Gohr                    break;
322be906b56SAndreas Gohr            }
323be906b56SAndreas Gohr        }
324be906b56SAndreas Gohr
325be906b56SAndreas Gohr        // condense cdata
326be906b56SAndreas Gohr        $cnt = count($this->tableCalls);
327be906b56SAndreas Gohr        for ($key = 0; $key < $cnt; $key++) {
328be906b56SAndreas Gohr            if ($this->tableCalls[$key][0] == 'cdata') {
329be906b56SAndreas Gohr                $ckey = $key;
330be906b56SAndreas Gohr                $key++;
331be906b56SAndreas Gohr                while ($this->tableCalls[$key][0] == 'cdata') {
332be906b56SAndreas Gohr                    $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
333be906b56SAndreas Gohr                    $toDelete[] = $key;
334be906b56SAndreas Gohr                    $key++;
335be906b56SAndreas Gohr                }
336be906b56SAndreas Gohr                continue;
337be906b56SAndreas Gohr            }
338be906b56SAndreas Gohr        }
339be906b56SAndreas Gohr
340be906b56SAndreas Gohr        foreach ($toDelete as $delete) {
341be906b56SAndreas Gohr            unset($this->tableCalls[$delete]);
342be906b56SAndreas Gohr        }
343be906b56SAndreas Gohr        $this->tableCalls = array_values($this->tableCalls);
344be906b56SAndreas Gohr    }
345be906b56SAndreas Gohr}
346