Completed
Push — 1.5 ( 4431e8...60cbc0 )
by Colin
01:24
created

TableParser::parse()   C

Complexity

Conditions 10
Paths 7

Size

Total Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 10.6771

Importance

Changes 0
Metric Value
dl 0
loc 67
ccs 30
cts 37
cp 0.8108
c 0
b 0
f 0
rs 6.8533
cc 10
nc 7
nop 2
crap 10.6771

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This is part of the league/commonmark package.
7
 *
8
 * (c) Martin Hasoň <[email protected]>
9
 * (c) Webuni s.r.o. <[email protected]>
10
 * (c) Colin O'Dell <[email protected]>
11
 *
12
 * For the full copyright and license information, please view the LICENSE
13
 * file that was distributed with this source code.
14
 */
15
16
namespace League\CommonMark\Extension\Table;
17
18
use League\CommonMark\Block\Element\Document;
19
use League\CommonMark\Block\Element\Paragraph;
20
use League\CommonMark\Block\Parser\BlockParserInterface;
21
use League\CommonMark\Context;
22
use League\CommonMark\ContextInterface;
23
use League\CommonMark\Cursor;
24
use League\CommonMark\EnvironmentAwareInterface;
25
use League\CommonMark\EnvironmentInterface;
26
27
final class TableParser implements BlockParserInterface, EnvironmentAwareInterface
28
{
29
    /**
30
     * @var EnvironmentInterface
31
     */
32
    private $environment;
33
34 156
    public function parse(ContextInterface $context, Cursor $cursor): bool
35
    {
36 156
        $container = $context->getContainer();
37 156
        if (!$container instanceof Paragraph) {
38 156
            return false;
39
        }
40
41 87
        $lines = $container->getStrings();
42 87
        if (count($lines) !== 1) {
43 18
            return false;
44
        }
45
46 87
        if (\strpos($lines[0], '|') === false) {
47 18
            return false;
48
        }
49
50 75
        $oldState = $cursor->saveState();
51 75
        $cursor->advanceToNextNonSpaceOrTab();
52 75
        $columns = $this->parseColumns($cursor);
53
54 75
        if (empty($columns)) {
55
            $cursor->restoreState($oldState);
56
57
            return false;
58
        }
59
60 75
        $head = $this->parseRow(trim((string) array_pop($lines)), $columns, TableCell::TYPE_HEAD);
61 75
        if (null === $head) {
62 3
            $cursor->restoreState($oldState);
63
64 3
            return false;
65
        }
66
67 48
        $table = new Table(function (Cursor $cursor, Table $table) use ($columns): bool {
68
            // The next line cannot be a new block start
69
            // This is a bit inefficient, but it's the only feasible way to check
70
            // given the current v1 API.
71 69
            if (self::isANewBlock($this->environment, $cursor->getLine())) {
72 6
                return false;
73
            }
74
75 69
            $row = $this->parseRow(\trim($cursor->getLine()), $columns);
76 69
            if (null === $row) {
77 18
                return false;
78
            }
79
80 69
            $table->getBody()->appendChild($row);
81
82 69
            return true;
83 72
        });
84
85 72
        $table->getHead()->appendChild($head);
86
87 72
        if (count($lines) >= 1) {
88
            $paragraph = new Paragraph();
89
            foreach ($lines as $line) {
90
                $paragraph->addLine($line);
91
            }
92
93
            $context->replaceContainerBlock($paragraph);
94
            $context->addBlock($table);
95
        } else {
96 72
            $context->replaceContainerBlock($table);
97
        }
98
99 72
        return true;
100
    }
101
102
    /**
103
     * @param string             $line
104
     * @param array<int, string> $columns
105
     * @param string             $type
106
     *
107
     * @return TableRow|null
108
     */
109 75
    private function parseRow(string $line, array $columns, string $type = TableCell::TYPE_BODY): ?TableRow
110
    {
111 75
        $cells = $this->split(new Cursor(\trim($line)));
112
113 75
        if (empty($cells)) {
114 18
            return null;
115
        }
116
117
        // The header row must match the delimiter row in the number of cells
118 75
        if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
119 3
            return null;
120
        }
121
122 72
        $i = 0;
123 72
        $row = new TableRow();
124 72
        foreach ($cells as $i => $cell) {
125 72
            if (!array_key_exists($i, $columns)) {
126 9
                return $row;
127
            }
128
129 72
            $row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
130
        }
131
132 72
        for ($j = count($columns) - 1; $j > $i; --$j) {
133 12
            $row->appendChild(new TableCell('', $type, null));
134
        }
135
136 72
        return $row;
137
    }
138
139
    /**
140
     * @param Cursor $cursor
141
     *
142
     * @return array<int, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
143
     */
144 75
    private function split(Cursor $cursor): array
145
    {
146 75
        if ($cursor->getCharacter() === '|') {
147 54
            $cursor->advanceBy(1);
148
        }
149
150 75
        $cells = [];
151 75
        $sb = '';
152
153 75
        while (!$cursor->isAtEnd()) {
154 75
            switch ($c = $cursor->getCharacter()) {
155 75
                case '\\':
156 12
                    if ($cursor->peek() === '|') {
157
                        // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
158
                        // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
159
                        // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
160 12
                        $sb .= '|';
161 12
                        $cursor->advanceBy(1);
162
                    } else {
163
                        // Preserve backslash before other characters or at end of line.
164 3
                        $sb .= '\\';
165
                    }
166 12
                    break;
167 75
                case '|':
168 75
                    $cells[] = $sb;
169 75
                    $sb = '';
170 75
                    break;
171
                default:
172 75
                    $sb .= $c;
173
            }
174 75
            $cursor->advanceBy(1);
175
        }
176
177 75
        if ($sb !== '') {
178 36
            $cells[] = $sb;
179
        }
180
181 75
        return $cells;
182
    }
183
184
    /**
185
     * @param Cursor $cursor
186
     *
187
     * @return array<int, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
188
     */
189 75
    private function parseColumns(Cursor $cursor): array
190
    {
191 75
        $columns = [];
192 75
        $pipes = 0;
193 75
        $valid = false;
194
195 75
        while (!$cursor->isAtEnd()) {
196 75
            switch ($c = $cursor->getCharacter()) {
197 75
                case '|':
198 75
                    $cursor->advanceBy(1);
199 75
                    $pipes++;
200 75
                    if ($pipes > 1) {
201
                        // More than one adjacent pipe not allowed
202
                        return [];
203
                    }
204
205
                    // Need at least one pipe, even for a one-column table
206 75
                    $valid = true;
207 75
                    break;
208 75
                case '-':
209 66
                case ':':
210 75
                    if ($pipes === 0 && !empty($columns)) {
211
                        // Need a pipe after the first column (first column doesn't need to start with one)
212
                        return [];
213
                    }
214 75
                    $left = false;
215 75
                    $right = false;
216 75
                    if ($c === ':') {
217 12
                        $left = true;
218 12
                        $cursor->advanceBy(1);
219
                    }
220 75
                    if ($cursor->match('/^-+/') === null) {
221
                        // Need at least one dash
222
                        return [];
223
                    }
224 75
                    if ($cursor->getCharacter() === ':') {
225 9
                        $right = true;
226 9
                        $cursor->advanceBy(1);
227
                    }
228 75
                    $columns[] = $this->getAlignment($left, $right);
229
                    // Next, need another pipe
230 75
                    $pipes = 0;
231 75
                    break;
232 63
                case ' ':
233
                case "\t":
234
                    // White space is allowed between pipes and columns
235 63
                    $cursor->advanceToNextNonSpaceOrTab();
236 63
                    break;
237
                default:
238
                    // Any other character is invalid
239
                    return [];
240
            }
241
        }
242
243 75
        if (!$valid) {
244
            return [];
245
        }
246
247 75
        return $columns;
248
    }
249
250 75
    private static function getAlignment(bool $left, bool $right): ?string
251
    {
252 75
        if ($left && $right) {
253 9
            return TableCell::ALIGN_CENTER;
254 75
        } elseif ($left) {
255 6
            return TableCell::ALIGN_LEFT;
256 75
        } elseif ($right) {
257 9
            return TableCell::ALIGN_RIGHT;
258
        }
259
260 69
        return null;
261
    }
262
263 162
    public function setEnvironment(EnvironmentInterface $environment)
264
    {
265 162
        $this->environment = $environment;
266 162
    }
267
268 69
    private static function isANewBlock(EnvironmentInterface $environment, string $line): bool
269
    {
270 69
        $context = new Context(new Document(), $environment);
271 69
        $context->setNextLine($line);
272 69
        $cursor = new Cursor($line);
273
274
        /** @var BlockParserInterface $parser */
275 69
        foreach ($environment->getBlockParsers() as $parser) {
276 69
            if ($parser->parse($context, $cursor)) {
277 27
                return true;
278
            }
279
        }
280
281 69
        return false;
282
    }
283
}
284