ListBlockStartParser::parseList()   B
last analyzed

Complexity

Conditions 11
Paths 10

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 11

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 43
ccs 30
cts 30
cp 1
rs 7.3166
c 0
b 0
f 0
cc 11
nc 10
nop 2
crap 11

How to fix   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 file is part of the league/commonmark package.
7
 *
8
 * (c) Colin O'Dell <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace League\CommonMark\Extension\CommonMark\Parser\Block;
15
16
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
17
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
18
use League\CommonMark\Parser\Block\BlockStart;
19
use League\CommonMark\Parser\Block\BlockStartParserInterface;
20
use League\CommonMark\Parser\Cursor;
21
use League\CommonMark\Parser\MarkdownParserStateInterface;
22
use League\CommonMark\Util\RegexHelper;
23
use League\Config\ConfigurationAwareInterface;
24
use League\Config\ConfigurationInterface;
25
26
final class ListBlockStartParser implements BlockStartParserInterface, ConfigurationAwareInterface
27
{
28
    /** @psalm-readonly-allow-private-mutation */
29
    private ?ConfigurationInterface $config = null;
30
31
    /**
32
     * @psalm-var non-empty-string|null
33
     *
34
     * @psalm-readonly-allow-private-mutation
35
     */
36
    private ?string $listMarkerRegex = null;
37
38 2332
    public function setConfiguration(ConfigurationInterface $configuration): void
39
    {
40 2332
        $this->config = $configuration;
41
    }
42
43 1346
    public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
44
    {
45 1346
        if ($cursor->isIndented()) {
46 142
            return BlockStart::none();
47
        }
48
49 1260
        $listData = $this->parseList($cursor, $parserState->getParagraphContent() !== null);
50 1258
        if ($listData === null) {
51 1074
            return BlockStart::none();
52
        }
53
54 216
        $listItemParser = new ListItemParser($listData);
55
56
        // prepend the list block if needed
57 216
        $matched = $parserState->getLastMatchedBlockParser();
58 216
        if (! ($matched instanceof ListBlockParser) || ! $listData->equals($matched->getBlock()->getListData())) {
59 216
            $listBlockParser = new ListBlockParser($listData);
60
            // We start out with assuming a list is tight. If we find a blank line, we set it to loose later.
61
            // TODO for 3.0: Just make them tight by default in the block so we can remove this call
62 216
            $listBlockParser->getBlock()->setTight(true);
63
64 216
            return BlockStart::of($listBlockParser, $listItemParser)->at($cursor);
65
        }
66
67 84
        return BlockStart::of($listItemParser)->at($cursor);
68
    }
69
70 1260
    private function parseList(Cursor $cursor, bool $inParagraph): ?ListData
71
    {
72 1260
        $indent = $cursor->getIndent();
73
74 1260
        $tmpCursor = clone $cursor;
75 1260
        $tmpCursor->advanceToNextNonSpaceOrTab();
76 1260
        $rest = $tmpCursor->getRemainder();
77
78 1260
        if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) {
79 398
            $data               = new ListData();
80 398
            $data->markerOffset = $indent;
81 398
            $data->type         = ListBlock::TYPE_BULLET;
82 398
            $data->delimiter    = null;
83 398
            $data->bulletChar   = $rest[0];
84 398
            $markerLength       = 1;
85 926
        } elseif (($matches = RegexHelper::matchFirst('/^(\d{1,9})([.)])/', $rest)) && (! $inParagraph || $matches[1] === '1')) {
86 70
            $data               = new ListData();
87 70
            $data->markerOffset = $indent;
88 70
            $data->type         = ListBlock::TYPE_ORDERED;
89 70
            $data->start        = (int) $matches[1];
90 70
            $data->delimiter    = $matches[2] === '.' ? ListBlock::DELIM_PERIOD : ListBlock::DELIM_PAREN;
91 70
            $data->bulletChar   = null;
92 70
            $markerLength       = \strlen($matches[0]);
93
        } else {
94 862
            return null;
95
        }
96
97
        // Make sure we have spaces after
98 448
        $nextChar = $tmpCursor->peek($markerLength);
99 448
        if (! ($nextChar === null || $nextChar === "\t" || $nextChar === ' ')) {
100 234
            return null;
101
        }
102
103
        // If it interrupts paragraph, make sure first line isn't blank
104 220
        if ($inParagraph && ! RegexHelper::matchAt(RegexHelper::REGEX_NON_SPACE, $rest, $markerLength)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression League\CommonMark\Util\R..., $rest, $markerLength) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
105 4
            return null;
106
        }
107
108 216
        $cursor->advanceToNextNonSpaceOrTab(); // to start of marker
109 216
        $cursor->advanceBy($markerLength, true); // to end of marker
110 216
        $data->padding = self::calculateListMarkerPadding($cursor, $markerLength);
111
112 216
        return $data;
113
    }
114
115 216
    private static function calculateListMarkerPadding(Cursor $cursor, int $markerLength): int
116
    {
117 216
        $start          = $cursor->saveState();
118 216
        $spacesStartCol = $cursor->getColumn();
119
120 216
        while ($cursor->getColumn() - $spacesStartCol < 5) {
121 216
            if (! $cursor->advanceBySpaceOrTab()) {
122 210
                break;
123
            }
124
        }
125
126 216
        $blankItem         = $cursor->peek() === null;
127 216
        $spacesAfterMarker = $cursor->getColumn() - $spacesStartCol;
128
129 216
        if ($spacesAfterMarker >= 5 || $spacesAfterMarker < 1 || $blankItem) {
130 58
            $cursor->restoreState($start);
131 58
            $cursor->advanceBySpaceOrTab();
132
133 58
            return $markerLength + 1;
134
        }
135
136 174
        return $markerLength + $spacesAfterMarker;
137
    }
138
139
    /**
140
     * @psalm-return non-empty-string
141
     */
142 1260
    private function generateListMarkerRegex(): string
143
    {
144
        // No configuration given - use the defaults
145 1260
        if ($this->config === null) {
146
            return $this->listMarkerRegex = '/^[*+-]/';
147
        }
148
149 1260
        $markers = $this->config->get('commonmark/unordered_list_markers');
150
        \assert(\is_array($markers));
151
152 1258
        return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/';
153
    }
154
}
155