TableParser   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 90%

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 10
dl 0
loc 248
ccs 117
cts 130
cp 0.9
rs 8.64
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
C parse() 0 67 10
B parseRow() 0 29 7
B split() 0 39 7
C parseColumns() 0 60 14
A getAlignment() 0 12 5
A setEnvironment() 0 4 1
A isANewBlock() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like TableParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TableParser, and based on these observations, apply Extract Interface, too.

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 120
    public function parse(ContextInterface $context, Cursor $cursor): bool
35
    {
36 120
        $container = $context->getContainer();
37 120
        if (!$container instanceof Paragraph) {
38 120
            return false;
39
        }
40
41 72
        $lines = $container->getStrings();
42 72
        if (count($lines) !== 1) {
43 9
            return false;
44
        }
45
46 72
        if (\strpos($lines[0], '|') === false) {
47 6
            return false;
48
        }
49
50 72
        $oldState = $cursor->saveState();
51 72
        $cursor->advanceToNextNonSpaceOrTab();
52 72
        $columns = $this->parseColumns($cursor);
53
54 72
        if (empty($columns)) {
55
            $cursor->restoreState($oldState);
56
57
            return false;
58
        }
59
60 72
        $head = $this->parseRow(trim((string) array_pop($lines)), $columns, TableCell::TYPE_HEAD);
61 72
        if (null === $head) {
62 3
            $cursor->restoreState($oldState);
63
64 3
            return false;
65
        }
66
67
        $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 66
            if (self::isANewBlock($this->environment, $cursor->getLine())) {
72 3
                return false;
73
            }
74
75 66
            $row = $this->parseRow(\trim($cursor->getLine()), $columns);
76 66
            if (null === $row) {
77 18
                return false;
78
            }
79
80 66
            $table->getBody()->appendChild($row);
81
82 66
            return true;
83 69
        });
84
85 69
        $table->getHead()->appendChild($head);
86
87 69
        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 69
            $context->replaceContainerBlock($table);
97
        }
98
99 69
        return true;
100
    }
101
102 72
    private function parseRow(string $line, array $columns, string $type = TableCell::TYPE_BODY): ?TableRow
103
    {
104 72
        $cells = $this->split(new Cursor(\trim($line)));
105
106 72
        if (empty($cells)) {
107 18
            return null;
108
        }
109
110
        // The header row must match the delimiter row in the number of cells
111 72
        if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
112 3
            return null;
113
        }
114
115 69
        $i = 0;
116 69
        $row = new TableRow();
117 69
        foreach ($cells as $i => $cell) {
118 69
            if (!array_key_exists($i, $columns)) {
119 9
                return $row;
120
            }
121
122 69
            $row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
123
        }
124
125 69
        for ($j = count($columns) - 1; $j > $i; --$j) {
126 12
            $row->appendChild(new TableCell('', $type, null));
127
        }
128
129 69
        return $row;
130
    }
131
132 72
    private function split(Cursor $cursor): array
133
    {
134 72
        if ($cursor->getCharacter() === '|') {
135 54
            $cursor->advanceBy(1);
136
        }
137
138 72
        $cells = [];
139 72
        $sb = '';
140
141 72
        while (!$cursor->isAtEnd()) {
142 72
            switch ($c = $cursor->getCharacter()) {
143 72
                case '\\':
144 12
                    if ($cursor->peek() === '|') {
145
                        // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
146
                        // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
147
                        // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
148 12
                        $sb .= '|';
149 12
                        $cursor->advanceBy(1);
150
                    } else {
151
                        // Preserve backslash before other characters or at end of line.
152 3
                        $sb .= '\\';
153
                    }
154 12
                    break;
155 72
                case '|':
156 72
                    $cells[] = $sb;
157 72
                    $sb = '';
158 72
                    break;
159
                default:
160 72
                    $sb .= $c;
161
            }
162 72
            $cursor->advanceBy(1);
163
        }
164
165 72
        if ($sb !== '') {
166 33
            $cells[] = $sb;
167
        }
168
169 72
        return $cells;
170
    }
171
172
    /**
173
     * @param Cursor $cursor
174
     *
175
     * @return array
176
     */
177 72
    private function parseColumns(Cursor $cursor): array
178
    {
179 72
        $columns = [];
180 72
        $pipes = 0;
181 72
        $valid = false;
182
183 72
        while (!$cursor->isAtEnd()) {
184 72
            switch ($c = $cursor->getCharacter()) {
185 72
                case '|':
186 72
                    $cursor->advanceBy(1);
187 72
                    $pipes++;
188 72
                    if ($pipes > 1) {
189
                        // More than one adjacent pipe not allowed
190
                        return [];
191
                    }
192
193
                    // Need at least one pipe, even for a one-column table
194 72
                    $valid = true;
195 72
                    break;
196 72
                case '-':
197 66
                case ':':
198 72
                    if ($pipes === 0 && !empty($columns)) {
199
                        // Need a pipe after the first column (first column doesn't need to start with one)
200
                        return [];
201
                    }
202 72
                    $left = false;
203 72
                    $right = false;
204 72
                    if ($c === ':') {
205 12
                        $left = true;
206 12
                        $cursor->advanceBy(1);
207
                    }
208 72
                    if ($cursor->match('/^-+/') === null) {
209
                        // Need at least one dash
210
                        return [];
211
                    }
212 72
                    if ($cursor->getCharacter() === ':') {
213 9
                        $right = true;
214 9
                        $cursor->advanceBy(1);
215
                    }
216 72
                    $columns[] = $this->getAlignment($left, $right);
217
                    // Next, need another pipe
218 72
                    $pipes = 0;
219 72
                    break;
220 63
                case ' ':
221
                case "\t":
222
                    // White space is allowed between pipes and columns
223 63
                    $cursor->advanceToNextNonSpaceOrTab();
224 63
                    break;
225
                default:
226
                    // Any other character is invalid
227
                    return [];
228
            }
229
        }
230
231 72
        if (!$valid) {
232
            return [];
233
        }
234
235 72
        return $columns;
236
    }
237
238 72
    private static function getAlignment(bool $left, bool $right): ?string
239
    {
240 72
        if ($left && $right) {
241 9
            return TableCell::ALIGN_CENTER;
242 72
        } elseif ($left) {
243 6
            return TableCell::ALIGN_LEFT;
244 72
        } elseif ($right) {
245 9
            return TableCell::ALIGN_RIGHT;
246
        }
247
248 66
        return null;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 123
    public function setEnvironment(EnvironmentInterface $environment)
255
    {
256 123
        $this->environment = $environment;
257 123
    }
258
259 66
    private static function isANewBlock(EnvironmentInterface $environment, string $line): bool
260
    {
261 66
        $context = new Context(new Document(), $environment);
262 66
        $context->setNextLine($line);
263 66
        $cursor = new Cursor($line);
264
265
        /** @var BlockParserInterface $parser */
266 66
        foreach ($environment->getBlockParsers() as $parser) {
267 66
            if ($parser->parse($context, $cursor)) {
268 24
                return true;
269
            }
270
        }
271
272 66
        return false;
273
    }
274
}
275