GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Table::getBodyCellsCount()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
namespace Trefoil\Helpers;
4
5
/**
6
 * Class Table abstracts the representation of a table (as in "HTML table").
7
 *
8
 * This class extends ArrayObject, so an instance can be manipulated as an
9
 * array.
10
 *      Example: $table['tbody'][1][2] => cell[1][2] in the <tbody> section.
11
 *
12
 * Extended functionality:
13
 * - Transforms a "simple" HTML table into a "complex" table,
14
 *   where "simple" means "without rowspan or colspan cells".
15
 *
16
 * Complex tables functionality details:
17
 * ------------------------------------
18
 *
19
 * It is designed to allow HTML tables generated from Markdown content
20
 * to have the extra functionality of rowspan or colspan without having
21
 * to modify the parser.
22
 *
23
 * For the transformations to work, cell contents must follow some simple
24
 * rules:
25
 *
26
 * - A cell containing only ["] (a single double quote) or ['] a single
27
 *   single quote => rowspanned cell(meaning it is joined with the same
28
 *   cell of the preceding row). The difference between using double
29
 *   or single quotes is the vertical alignment:
30
 *      - double quote: middle alignmet.
31
 *      - single quote: top alignment.
32
 *
33
 * - An empty cell => colspanned cell (meaning it is joined with the same
34
 *   cell of the preceding column.
35
 *
36
 * @package Trefoil\Helpers
37
 */
38
class Table extends \ArrayObject
39
{
40
    /**
41
     * Create <tbody> section if the table does not have it
42
     */
43
    const CREATE_TBODY = 0b00000001;
44
45
    /**
46
     * @param $htmlTable string HTML <table> tag
47
     */
48 7
    public function fromHtml($htmlTable)
49
    {
50 7
        $this->parseHtmlTable($htmlTable);
51 7
    }
52
53
    /**
54
     * @return string Table rendered to HTML
55
     */
56 16
    public function toHtml()
57
    {
58 16
        return $this->renderTableToHtml();
59
    }
60
61
    /**
62
     * @param     $htmlTable
63
     * @param int $flags
64
     */
65 7
    protected function parseHtmlTable($htmlTable, $flags = self::CREATE_TBODY)
66
    {
67
        // init the ArrayObject
68 7
        $this->exchangeArray([]);
69
70 7
        $this['thead'] = $this->extractHtmlRows($htmlTable, 'thead');
71 7
        $this['tbody'] = $this->extractHtmlRows($htmlTable, 'tbody');
72
73 7
        if (!$this['thead'] && !$this['tbody']) {
74 3
            if ($flags & self::CREATE_TBODY) {
75 3
                $this['tbody'] = $this->extractHtmlRows($htmlTable, 'table');
76
77 3
                return;
78
            }
79
80
            $this['table'] = $this->extractHtmlRows($htmlTable, 'table');
81
        }
82 4
    }
83
84
    /**
85
     * @param string $contents
86
     * @param int    $column
87
     */
88 8
    public function addHeadingCell($contents, $column = null)
89
    {
90 8
        if ($column === null) {
91
            $column = count($this['thead'][0]);
92
        }
93
94 8
        $this['thead'][0][$column] = [
95 8
            'tag'      => 'th',
96
            'contents' => $contents
97 8
        ];
98 8
    }
99
100
    /**
101
     * @param int $column
102
     *
103
     * @return null|array cell
104
     */
105 8
    public function getHeadingCell($column)
106
    {
107 8
        if (isset($this['thead'][0][$column])) {
108 8
            return $this['thead'][0][$column];
109
        }
110
111 8
        return null;
112
    }
113
114
    /**
115
     * @param array $cell
116
     * @param int   $column
117
     */
118
    public function setHeadingCell(array $cell, $column)
119
    {
120
        $this['thead'][0][$column] = $cell;
121
    }
122
123 8
    public function addBodyRow()
124
    {
125 8
        if (!isset($this['tbody'])) {
126 8
            $this['tbody'] = [];
127 8
        }
128
129 8
        $this['tbody'][] = [];
130 8
    }
131
132 8
    public function getBodyRowsCount()
133
    {
134 8
        if (isset($this['tbody'])) {
135 8
            return count($this['tbody']);
136
        }
137
138 8
        return 0;
139
    }
140
141 8
    public function getBodyCellsCount($row)
142
    {
143 8
        if (isset($this['tbody'][$row])) {
144 8
            return count($this['tbody'][$row]);
145
        }
146
147 8
        return 0;
148
    }
149
150
    /**
151
     * @param      $contents
152
     * @param null $row
153
     * @param null $column
154
     *
155
     * @return array
156
     */
157 8
    public function addBodyCell($contents, $row = null, $column = null)
158
    {
159 8
        if (!isset($this['tbody'])) {
160
            $this['tbody'][] = [];
161
        }
162
163 8
        if ($row === null) {
164 8
            $row = count($this['tbody']) - 1;
165 8
        }
166
167 8
        if ($column === null) {
168 8
            $column = count($this['tbody'][$row]);
169 8
        }
170
171
        // modify if existing, new otherwise
172 8
        if (isset($this['tbody'][$row][$column])) {
173 4
            $this['tbody'][$row][$column]['contents'] = $contents;
174 4
        } else {
175 8
            $this['tbody'][$row][$column] = [
176 8
                'tag'      => 'td',
177
                'contents' => $contents
178 8
            ];
179
        }
180
181 8
        return ['row' => $row, 'column' => $column];
182
    }
183
184 8
    public function setBodyCellExtra($extra, $row, $column)
185
    {
186 8
        $this['tbody'][$row][$column]['extra'] = $extra;
187 8
    }
188
189 8 View Code Duplication
    public function getBodyCellExtra($row, $column)
190
    {
191 8
        if (!isset($this['tbody'][$row][$column]['extra'])) {
192 8
            return null;
193
        }
194
195
        return $this['tbody'][$row][$column]['extra'];
196
    }
197
198
    public function setColspan($colspan, $row, $column)
199
    {
200
        $this['tbody'][$row][$column]['colspan'] = $colspan;
201
    }
202
203
    public function setRowsspan($rowsspan, $row, $column)
204
    {
205
        $this['tbody'][$row][$column]['rowspan'] = $rowsspan;
206
    }
207
208
209
    /**
210
     * @param $row
211
     * @param $column
212
     *
213
     * @return null|array cell
214
     */
215 View Code Duplication
    public function getBodyCell($row, $column)
216
    {
217
        if (isset($this['tbody'][$row][$column])) {
218
            return $this['tbody'][$row][$column];
219
        }
220
221
        return null;
222
    }
223
224
    /**
225
     * @param $cell array
226
     * @param $row
227
     * @param $column
228
     */
229
    public function setBodyCell(array $cell, $row, $column)
230
    {
231
        $this['tbody'][$row][$column] = $cell;
232
    }
233
234
    /**
235
     * @param        $htmlTable
236
     * @param string $tag
237
     *
238
     * @return array of rows
239
     */
240 7
    protected function extractHtmlRows($htmlTable, $tag = 'tbody')
241
    {
242
        // extract section
243 7
        $regExp = sprintf('/<%s>(?<contents>.*)<\/%s>/Ums', $tag, $tag);
244 7
        preg_match_all($regExp, $htmlTable, $matches, PREG_SET_ORDER);
245
246 7
        if (!isset($matches[0]['contents'])) {
247 3
            return array();
248
        }
249
250
        // extract all rows from section
251 6
        $thead = $matches[0]['contents'];
252 6
        $regExp = '/<tr>(?<contents>.*)<\/tr>/Ums';
253 6
        preg_match_all($regExp, $thead, $matches, PREG_SET_ORDER);
254
255 6
        if (!isset($matches[0]['contents'])) {
256
            return array();
257
        }
258
259
        // extract columns from each row
260 6
        $rows = array();
261 6
        foreach ($matches as $matchRow) {
262
263 6
            $tr = $matchRow['contents'];
264 6
            $regExp = '/<(?<tag>t[hd])(?<attr>.*)>(?<contents>.*)<\/t[hd]>/Ums';
265 6
            preg_match_all($regExp, $tr, $matchesCol, PREG_SET_ORDER);
266
267 6
            $cols = array();
268 6
            if ($matchesCol) {
269 6
                foreach ($matchesCol as $matchCol) {
270 6
                    $cols[] = array(
271 6
                        'tag'        => $matchCol['tag'],
272 6
                        'attributes' => $this->extractAttributes($matchCol['attr']),
273 6
                        'contents'   => $matchCol['contents']
274 6
                    );
275 6
                }
276 6
            }
277
278 6
            $rows[] = $cols;
279 6
        }
280
281 6
        return $rows;
282
    }
283
284
    /**
285
     * @return bool True if the table does not have any rows
286
     */
287 4
    public function isEmpty()
288
    {
289 4
        if (isset($this['thead']) && $this['thead']) {
290 2
            return false;
291
        }
292
293 2
        if (isset($this['tbody']) && $this['tbody']) {
294 1
            return false;
295
        }
296
297 1
        if (isset($this['table']) && $this['table']) {
298
            return false;
299
        }
300
301 1
        return true;
302
    }
303
304
    /**
305
     * @return string
306
     *
307
     */
308 16
    protected function renderTableToHtml()
309
    {
310 16
        $html = '';
311
312 16 View Code Duplication
        if (isset($this['thead']) && $this['thead']) {
313 12
            $html .= '<thead>';
314 12
            $html .= $this->renderHtmlRows($this['thead']);
315 12
            $html .= '</thead>';
316 12
        }
317
318 16 View Code Duplication
        if (isset($this['tbody']) && $this['tbody']) {
319 14
            $html .= '<tbody>';
320 14
            $html .= $this->renderHtmlRows($this['tbody']);
321 14
            $html .= '</tbody>';
322 14
        }
323
324 16
        if (isset($this['table']) && $this['table']) {
325
            $html .= $this->renderHtmlRows($this['table']);
326
        }
327
328 16
        if (empty($html)) {
329 2
            return '';
330
        }
331
332 14
        return '<table>' . $html . '</table>';
333
    }
334
335
    /**
336
     * @param array $rows
337
     *
338
     * @return string
339
     */
340 14
    protected function renderHtmlRows(array $rows)
341
    {
342 14
        $html = '';
343
344 14
        $rows = $this->processSpannedCells($rows);
345
346 14
        foreach ($rows as $row) {
347 14
            $html .= '<tr>';
348
349 14
            foreach ($row as $col) {
350 14
                if (!isset($col['ignore'])) {
351 14
                    $rowspan = isset($col['rowspan']) ? sprintf('rowspan="%s"', $col['rowspan']) : '';
352 14
                    $colspan = isset($col['colspan']) ? sprintf('colspan="%s"', $col['colspan']) : '';
353
354 14
                    $attributes = isset($col['attributes']) ? $this->renderAttributes($col['attributes']) : '';
355
356 14
                    $html .= sprintf(
357 14
                        '<%s %s %s %s>%s</%s>',
358 14
                        $col['tag'],
359 14
                        $rowspan,
360 14
                        $colspan,
361 14
                        $attributes,
362 14
                        $col['contents'],
363 14
                        $col['tag']
364 14
                    );
365 14
                }
366 14
            }
367
368 14
            $html .= '</tr>';
369 14
        }
370
371 14
        return $html;
372
    }
373
374
    /**
375
     * @param string $string
376
     *
377
     * @return array of attributes
378
     */
379 6 View Code Duplication
    protected function extractAttributes($string)
380
    {
381 6
        $regExp = '/(?<attr>.*)="(?<value>.*)"/Us';
382 6
        preg_match_all($regExp, $string, $attrMatches, PREG_SET_ORDER);
383
384 6
        $attributes = array();
385 6
        if ($attrMatches) {
386 1
            foreach ($attrMatches as $attrMatch) {
387 1
                $attributes[trim($attrMatch['attr'])] = $attrMatch['value'];
388 1
            }
389 1
        }
390
391 6
        return $attributes;
392
    }
393
394
    /**
395
     * @param array $attributes
396
     *
397
     * @return string rendered attributes
398
     */
399 12 View Code Duplication
    protected function renderAttributes(array $attributes)
400
    {
401 12
        $html = '';
402
403 12
        foreach ($attributes as $name => $value) {
404 9
            $html .= sprintf('%s="%s" ', $name, $value);
405 12
        }
406
407 12
        return $html;
408
    }
409
410
    /**
411
     * Process spanned rows, creating the right HTML markup.
412
     *
413
     * @param array $rows
414
     *
415
     * @return array Processed rows
416
     */
417 14
    protected function processSpannedCells(array $rows)
418
    {
419
        // several kinds of double quote character
420
        $doubleQuotes = array(
421 14
            '"',
422 14
            '&quot;',
423 14
            '&#34;',
424 14
            '&ldquo;',
425 14
            '&#8220;',
426 14
            '&rdquo;',
427
            '&#8221;'
428 14
        );
429
430
        // several kinds of single quote character
431
        $singleQuotes = array(
432 14
            "'",
433 14
            '&apos;',
434 14
            '&#39;',
435 14
            '&lsquo;',
436 14
            '&#8216;',
437 14
            '&rsquo;',
438 14
            '&#8217;',
439 14
        );
440
441 14
        $newRows = $rows;
442 14
        foreach ($rows as $rowIndex => $row) {
443
444 14
            foreach ($row as $colIndex => $col) {
445
446
                // an empty cell => colspanned cell
447 14
                if (trim($col['contents']) === "") {
448
449
                    // find the primary colspanned cell (same row)
450 3
                    $colspanCol = -1;
451 3
                    for ($j = $colIndex - 1; $j >= 0; $j--) {
452 3
                        if (!isset($newRows[$rowIndex][$j]['ignore']) ||
453 1
                            (isset($newRows[$rowIndex][$j]['ignore']) && $j == 0)
454 3
                        ) {
455 3
                            $colspanCol = $j;
456 3
                            break;
457
                        }
458 1
                    }
459
460 3
                    if ($colspanCol >= 0) {
461
                        // increment colspan counter
462 3
                        if (!isset($newRows[$rowIndex][$colspanCol]['colspan'])) {
463 3
                            $newRows[$rowIndex][$colspanCol]['colspan'] = 1;
464 3
                        }
465 3
                        $newRows[$rowIndex][$colspanCol]['colspan']++;
466
467
                        // ignore this cell
468 3
                        $newRows[$rowIndex][$colIndex]['ignore'] = true;
469 3
                    }
470
471 3
                    continue;
472
                }
473
474
                // a cell with only '"' as contents => rowspanned cell (same column)
475
                // consider several kind of double quote character
476
                // and the single quote character as a top alignment marker
477 14
                if (in_array($col['contents'], $doubleQuotes) ||
478 14
                    in_array($col['contents'], $singleQuotes)
479 14
                ) {
480
481
                    // find the primary rowspanned cell
482 9
                    $rowspanRow = -1;
483 9
                    for ($i = $rowIndex - 1; $i >= 0; $i--) {
484 9
                        if (!isset($newRows[$i][$colIndex]['ignore'])) {
485 9
                            $rowspanRow = $i;
486 9
                            break;
487
                        }
488 6
                    }
489
490 9
                    if ($rowspanRow >= 0) {
491
                        // increment rowspan counter
492 9
                        if (!isset($newRows[$rowspanRow][$colIndex]['rowspan'])) {
493 9
                            $newRows[$rowspanRow][$colIndex]['rowspan'] = 1;
494
495
                            // set vertical alignment to 'middle' for double quote or
496
                            // 'top' for single quote 
497 9
                            if (!isset($newRows[$rowspanRow][$colIndex]['attributes']['style'])) {
498 9
                                $newRows[$rowspanRow][$colIndex]['attributes']['style'] = '';
499 9
                            } else {
500
                                $newRows[$rowspanRow][$colIndex]['attributes']['style'] .= ';';
501
                            }
502 9
                            $newRows[$rowspanRow][$colIndex]['attributes']['style'] .= 'vertical-align: middle;';
503 9
                            if (in_array($col['contents'], $singleQuotes)) {
504 7
                                $newRows[$rowspanRow][$colIndex]['attributes']['style'] .= 'vertical-align: top;';
505 7
                            }
506 9
                        }
507 9
                        $newRows[$rowspanRow][$colIndex]['rowspan']++;
508
509 9
                        $newRows[$rowIndex][$colIndex]['ignore'] = true;
510 9
                    }
511 9
                }
512 14
            }
513 14
        }
514
515 14
        return $newRows;
516
    }
517
}
518
519