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.
Completed
Push — develop ( cf0abc...328c62 )
by Miguel Angel
02:54
created

TableExtra::processFirstColumnCells()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 20
ccs 12
cts 12
cp 1
rs 9.4286
cc 3
eloc 10
nc 3
nop 1
crap 3
1
<?php
2
/*
3
 * This file is part of the trefoil application.
4
 *
5
 * (c) Miguel Angel Gabriel <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Trefoil\Helpers;
12
13
use Easybook\Parsers\ParserInterface;
14
15
/**
16
 * This class:
17
 * - Transforms a "simple" HTML table into a "complex" table,
18
 *   where "simple" means "without rowspan or colspan cells".
19
 * - For headless tables, transforms the <td> cells in first column
20
 *   within <strong> tags into <th> cells (making a vertical head). 
21
 *
22
 * Complex tables functionality details:
23
 * ------------------------------------
24
 * 
25
 * It is designed to allow HTML tables generated from Markdown content
26
 * to have the extra funcionality of rowspan or colspan without having
27
 * to modify the parser.
28
 *
29
 * For the transformations to work, cell contents must follow some simple
30
 * rules:
31
 *
32
 * - A cell containing only ["] (a single double quote) or ['] a single
33
 *   single quote => rowspanned cell(meaning it is joined with the same
34
 *   cell of the preceding row). The difference between using double
35
 *   or single quotes is the vertical alignment:
36
 *      - double quote: middle alignmet.
37
 *      - single quote: top alignment.
38
 *
39
 * - An empty cell => colspanned cell (meaning it is joined with the same
40
 *   cell of the preceding column.
41
 *
42
 */
43
class TableExtra
44
{
45
    /** @var  ParserInterface */
46
    protected $markdownParser;
47
48
    /**
49
     * @param ParserInterface $markdownParser
50
     */
51 1
    public function setMarkdownParser($markdownParser)
52
    {
53 1
        $this->markdownParser = $markdownParser;
54 1
    }
55
56
    /**
57
     * Processes all tables in the html string
58
     *
59
     * @param string $htmlString
60
     *
61
     * @return string
62
     */
63 3
    public function processAllTables($htmlString)
64
    {
65 3
        $regExp = '/';
66 3
        $regExp .= '(?<table><table.*<\/table>)';
67 3
        $regExp .= '/Ums'; // Ungreedy, multiline, dotall
68
69
        // PHP 5.3 compat
70 3
        $me = $this;
71
72 3
        $callback = function ($matches) use ($me) {
73 3
            $table = $me->internalParseTable($matches['table']);
74 3
            if (!$table) {
75
                return $matches[0];
76
            }
77 3
            $table = $me->internalProcessExtraTable($table);
78 3
            $html = $me->internalRenderTable($table);
79
80 3
            return $html;
81 3
        };
82
83 3
        $output = preg_replace_callback($regExp, $callback, $htmlString);
84
85 3
        return $output;
86
    }
87
88
    /**
89
     * @param $tableHtml
90
     *
91
     * @return array
92
     *
93
     * @internal Should be protected but made public for PHP 5.3 compat
94
     */
95 3
    public function internalParseTable($tableHtml)
96
    {
97 3
        $table = array();
98
99 3
        $table['thead'] = $this->extractRows($tableHtml, 'thead');
100 3
        $table['tbody'] = $this->extractRows($tableHtml, 'tbody');
101
102 3
        if (!$table['thead'] && !$table['tbody']) {
103 1
            $table['table'] = $this->extractRows($tableHtml, 'table');
104 1
        }
105
106 3
        return $table;
107
    }
108
109 3
    protected function extractRows($tableHtml, $tag = 'tbody')
110
    {
111
        // extract section
112 3
        $regExp = sprintf('/<%s>(?<contents>.*)<\/%s>/Ums', $tag, $tag);
113 3
        preg_match_all($regExp, $tableHtml, $matches, PREG_SET_ORDER);
114
115 3
        if (!isset($matches[0]['contents'])) {
116 1
            return array();
117
        }
118
119
        // extract all rows from section
120 3
        $thead = $matches[0]['contents'];
121 3
        $regExp = '/<tr>(?<contents>.*)<\/tr>/Ums';
122 3
        preg_match_all($regExp, $thead, $matches, PREG_SET_ORDER);
123
124 3
        if (!isset($matches[0]['contents'])) {
125
            return array();
126
        }
127
128
        // extract columns from each row
129 3
        $rows = array();
130 3
        foreach ($matches as $matchRow) {
131
132 3
            $tr = $matchRow['contents'];
133 3
            $regExp = '/<(?<tag>t[hd])(?<attr>.*)>(?<contents>.*)<\/t[hd]>/Ums';
134 3
            preg_match_all($regExp, $tr, $matchesCol, PREG_SET_ORDER);
135
136 3
            $cols = array();
137 3
            if ($matchesCol) {
138 3
                foreach ($matchesCol as $matchCol) {
139 3
                    $cols[] = array(
140 3
                        'tag'        => $matchCol['tag'],
141 3
                        'attributes' => $this->extractAttributes($matchCol['attr']),
142 3
                        'contents'   => $matchCol['contents']
143 3
                    );
144 3
                }
145 3
            }
146
147 3
            $rows[] = $cols;
148 3
        }
149
150 3
        return $rows;
151
    }
152
153
    /**
154
     * @param array $table
155
     *
156
     * @return array
157
     *
158
     * @internal Should be protected but made public for PHP 5.3 compat
159
     */
160 3
    public function internalProcessExtraTable(array $table)
161
    {
162
        // process and adjusts table definition
163
164 3
        $headless = true;
165 3
        foreach ($table['thead'] as $row) {
166 2
            foreach ($row as $cell) {
167 2
                if (empty($cell['contents'])) {
168 2
                    $headless = true;
169 2
                    break 2;
170
                }
171 2
            }
172 3
        }
173
174
        // headless table
175 3
        if ($headless && $table['tbody']) {
176 2
            $table['tbody'] = $this->processFirstColumnCells($table['tbody']);
177 2
        }
178
179
        // table with head and body
180 3
        if ($table['thead'] || $table['tbody']) {
181
182 2
            $table['thead'] = $this->processMultilineCells($table['thead']);
183 2
            $table['thead'] = $this->processSpannedCells($table['thead']);
184
185 2
            $table['tbody'] = $this->processMultilineCells($table['tbody']);
186 2
            $table['tbody'] = $this->processSpannedCells($table['tbody']);
187
188 2
            return $table;
189
        }
190
191
        // table without head or body
192 1
        $table['table'] = $this->processMultilineCells($table['table']);
193 1
        $table['table'] = $this->processSpannedCells($table['table']);
194
195 1
        return $table;
196
    }
197
198
    /**
199
     * Join the cells that belong to multiline cells.
200
     *
201
     * @param $rows
202
     *
203
     * @return array Processed table rows
204
     */
205 3
    protected function processMultilineCells(array $rows)
206
    {
207 3
        $newRows = $rows;
208 3
        foreach ($newRows as $rowIndex => $row) {
209
210 3
            foreach ($row as $colIndex => $col) {
211 3
                $cell = $newRows[$rowIndex][$colIndex];
212 3
                $cellText = rtrim($cell['contents']);
213
214 3
                if (substr($cellText, -1, 1) === '+') {
215
                    // continued cell
216 1
                    $newCell = array();
217 1
                    $newCell[] = substr($cellText, 0, -1);
218
219
                    // find all the continuation cells (same col)
220 1
                    for ($nextRowIndex = $rowIndex + 1; $nextRowIndex < count($newRows); $nextRowIndex++) {
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...
221
222 1
                        $nextCell = $newRows[$nextRowIndex][$colIndex];
223 1
                        $cellText = rtrim($nextCell['contents']);
224
225 1
                        $newRows[$nextRowIndex][$colIndex]['contents'] = '';
226
227
                        // continued cell?
228 1
                        $continued = (substr($cellText, -1, 1) === '+');
229
230 1
                        if ($continued) {
231
                            // clean the ending (+)
232 1
                            $cellText = substr($cellText, 0, -1);
233 1
                        }
234
235
                        // save cleaned text
236 1
                        $newCell[] = $cellText;
237
238 1
                        if (!$continued) {
239
                            // no more continuations
240 1
                            break;
241
                        }
242 1
                    }
243
244 1
                    if ($this->markdownParser) {
245 1
                        $parsedCell = $this->markdownParser->transform(join("\n\n", $newCell));
246 1
                    } else {
247
                        // safe default
248
                        $parsedCell = join("<br/>", $newCell);
249
                    }
250
251 1
                    $newRows[$rowIndex][$colIndex]['contents'] = $parsedCell;
252 1
                }
253 3
            }
254 3
        }
255
256
        // remove empty rows left by the process
257 3
        $newRows2 = array();
258 3
        foreach ($newRows as $rowIndex => $row) {
259
260 3
            $emptyRow = true;
261 3
            foreach ($row as $colIndex => $col) {
262 3
                $cellText = trim($col['contents']);
263 3
                if (!empty($cellText)) {
264 3
                    $emptyRow = false;
265 3
                }
266 3
            }
267
268 3
            if (!$emptyRow) {
269 3
                $newRows2[] = $row;
270 3
            }
271 3
        }
272
273 3
        return $newRows2;
274
    }
275
276
    /**
277
     * Converts <td> cells into <th> for first column cells that are fully bold.
278
     *
279
     * @param array $rows
280
     *
281
     * @return array Processed rows
282
     */
283 2
    protected function processFirstColumnCells(array $rows)
284
    {
285 2
        $newRows = $rows;
286 2
        foreach ($newRows as $rowIndex => $row) {
287
288
            // examine first cell ir row
289 2
            $cell = $newRows[$rowIndex][0];
290 2
            $cellText = rtrim($cell['contents']);
291
292 2
            $regExp = '/^<strong>.*<\/strong>$/Us';
293 2
            if (preg_match($regExp, $cellText, $matches)) {
294 1
                $cell['tag'] = 'th';
295
296
                // change cell to <th>  
297 1
                $newRows[$rowIndex][0]['tag'] = 'th';
298 1
            }
299 2
        }
300
301 2
        return $newRows;
302
    }
303
304
    /**
305
     * Process spanned rows, creating the right HTML markup.
306
     *
307
     * @param array $rows
308
     *
309
     * @return array Processed rows
310
     */
311
    protected
312 3
    function processSpannedCells(array $rows)
313
    {
314 3
        $newRows = $rows;
315 3
        foreach ($rows as $rowIndex => $row) {
316
317 3
            foreach ($row as $colIndex => $col) {
318
319
                // an empty cell => colspanned cell
320 3
                if (!$col['contents']) {
321
322
                    // find the primary colspanned cell (same row)
323 3
                    $colspanCol = -1;
324 3
                    for ($j = $colIndex - 1; $j >= 0; $j--) {
325 3
                        if (!isset($newRows[$rowIndex][$j]['ignore']) ||
326 1
                            (isset($newRows[$rowIndex][$j]['ignore']) && $j == 0)
327 3
                        ) {
328 3
                            $colspanCol = $j;
329 3
                            break;
330
                        }
331 1
                    }
332
333 3
                    if ($colspanCol >= 0) {
334
                        // increment colspan counter
335 3
                        if (!isset($newRows[$rowIndex][$colspanCol]['colspan'])) {
336 3
                            $newRows[$rowIndex][$colspanCol]['colspan'] = 1;
337 3
                        }
338 3
                        $newRows[$rowIndex][$colspanCol]['colspan']++;
339
340
                        // ignore this cell
341 3
                        $newRows[$rowIndex][$colIndex]['ignore'] = true;
342 3
                    }
343
344 3
                    continue;
345
                }
346
347
                // a cell with only '"' as contents => rowspanned cell (same column)
348
                // consider several kind of double quote character
349
                // and the single quote character as a top alignment marker
350
                $quotes = array(
351 3
                    '"',
352 3
                    '&quot;',
353 3
                    '&#34;',
354 3
                    '&ldquo;',
355 3
                    '&#8220;',
356 3
                    '&rdquo;',
357 3
                    '&#8221;',
358
                    "'"
359 3
                );
360 3
                if (in_array($col['contents'], $quotes)) {
361
362
                    // find the primary rowspanned cell
363 3
                    $rowspanRow = -1;
364 3
                    for ($i = $rowIndex - 1; $i >= 0; $i--) {
365 3
                        if (!isset($newRows[$i][$colIndex]['ignore'])) {
366 3
                            $rowspanRow = $i;
367 3
                            break;
368
                        }
369 1
                    }
370
371 3
                    if ($rowspanRow >= 0) {
372
                        // increment rowspan counter
373 3
                        if (!isset($newRows[$rowspanRow][$colIndex]['rowspan'])) {
374 3
                            $newRows[$rowspanRow][$colIndex]['rowspan'] = 1;
375
376
                            // set vertical alignement to 'middle' for double quote or
377
                            // 'top' for single quote 
378 3
                            if (!isset($newRows[$rowspanRow][$colIndex]['attributes']['style'])) {
379 3
                                $newRows[$rowspanRow][$colIndex]['attributes']['style'] = '';
380 3
                            } else {
381
                                $newRows[$rowspanRow][$colIndex]['attributes']['style'] .= ';';
382
                            }
383 3
                            $newRows[$rowspanRow][$colIndex]['attributes']['style'] .= 'vertical-align: middle;';
384 3
                            if ($col['contents'] === "'") {
385 1
                                $newRows[$rowspanRow][$colIndex]['attributes']['style'] .= 'vertical-align: top;';
386 1
                            }
387 3
                        }
388 3
                        $newRows[$rowspanRow][$colIndex]['rowspan']++;
389
390 3
                        $newRows[$rowIndex][$colIndex]['ignore'] = true;
391 3
                    }
392 3
                }
393 3
            }
394 3
        }
395
396 3
        return $newRows;
397
    }
398
399
    /**
400
     * @param array $table
401
     *
402
     * @return string
403
     *
404
     * @internal Should be protected but made public for PHP 5.3 compat
405
     */
406
    public
407 3
    function internalRenderTable(array $table)
408
    {
409 3
        $html = '<table>';
410
411 3 View Code Duplication
        if (isset($table['thead']) && $table['thead']) {
412 2
            $html .= '<thead>';
413 2
            $html .= $this->renderRows($table['thead']);
414 2
            $html .= '</thead>';
415 2
        }
416
417 3 View Code Duplication
        if (isset($table['tbody']) && $table['tbody']) {
418 2
            $html .= '<tbody>';
419 2
            $html .= $this->renderRows($table['tbody']);
420 2
            $html .= '</tbody>';
421 2
        }
422
423 3
        if (isset($table['table']) && $table['table']) {
424 1
            $html .= $this->renderRows($table['table']);
425 1
        }
426
427 3
        $html .= '</table>';
428
429 3
        return $html;
430
    }
431
432
    protected
433 3
    function renderRows($rows)
434
    {
435 3
        $html = '';
436
437 3
        foreach ($rows as $row) {
438 3
            $html .= '<tr>';
439 3
            foreach ($row as $col) {
440 3
                if (!isset($col['ignore'])) {
441 3
                    $rowspan = isset($col['rowspan']) ? sprintf('rowspan="%s"', $col['rowspan']) : '';
442 3
                    $colspan = isset($col['colspan']) ? sprintf('colspan="%s"', $col['colspan']) : '';
443
444 3
                    $attributes = $this->renderAttributes($col['attributes']);
445
446 3
                    $html .= sprintf(
447 3
                        '<%s %s %s %s>%s</%s>',
448 3
                        $col['tag'],
449 3
                        $rowspan,
450 3
                        $colspan,
451 3
                        $attributes,
452 3
                        $col['contents'],
453 3
                        $col['tag']
454 3
                    );
455 3
                }
456 3
            }
457 3
            $html .= '</tr>';
458 3
        }
459
460 3
        return $html;
461
    }
462
463
    /**
464
     * @param string $string
465
     *
466
     * @return array of attributes
467
     */
468 View Code Duplication
    protected
469 3
    function extractAttributes($string)
470
    {
471 3
        $regExp = '/(?<attr>.*)="(?<value>.*)"/Us';
472 3
        preg_match_all($regExp, $string, $attrMatches, PREG_SET_ORDER);
473
474 3
        $attributes = array();
475 3
        if ($attrMatches) {
476 1
            foreach ($attrMatches as $attrMatch) {
477 1
                $attributes[trim($attrMatch['attr'])] = $attrMatch['value'];
478 1
            }
479 1
        }
480
481 3
        return $attributes;
482
    }
483
484 View Code Duplication
    protected
485 3
    function renderAttributes(array $attributes)
486
    {
487 3
        $html = '';
488
489 3
        foreach ($attributes as $name => $value) {
490 3
            $html .= sprintf('%s="%s" ', $name, $value);
491 3
        }
492
493 3
        return $html;
494
    }
495
496
}
497