Completed
Push — stable ( b83837...3c8749 )
by
unknown
08:20 queued 05:46
created

Table::writeCall()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace dokuwiki\Parsing\Handler;
4
5
class 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) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
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