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