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