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