Completed
Push — master ( a13ee4...ec2d74 )
by Colin
26s queued 10s
created

ListParser::generateListMarkerRegex()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 7
cts 7
cp 1
rs 9.7666
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3
1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark\Block\Parser;
16
17
use League\CommonMark\Block\Element\ListBlock;
18
use League\CommonMark\Block\Element\ListData;
19
use League\CommonMark\Block\Element\ListItem;
20
use League\CommonMark\Block\Element\Paragraph;
21
use League\CommonMark\ContextInterface;
22
use League\CommonMark\Cursor;
23
use League\CommonMark\Util\ConfigurationAwareInterface;
24
use League\CommonMark\Util\ConfigurationInterface;
25
use League\CommonMark\Util\RegexHelper;
26
27
final class ListParser implements BlockParserInterface, ConfigurationAwareInterface
28
{
29
    /** @var ConfigurationInterface|null */
30
    private $config;
31
32
    /** @var string|null */
33
    private $listMarkerRegex;
34
35
    /**
36
     * {@inheritdoc}
37
     */
38 2433
    public function setConfiguration(ConfigurationInterface $configuration)
39
    {
40 2433
        $this->config = $configuration;
41 2433
    }
42
43
    /**
44
     * @param ContextInterface $context
45
     * @param Cursor           $cursor
46
     *
47
     * @return bool
48
     */
49 2220
    public function parse(ContextInterface $context, Cursor $cursor): bool
50
    {
51 2220
        if ($cursor->isIndented() && !($context->getContainer() instanceof ListBlock)) {
52 171
            return false;
53
        }
54
55 2157
        $indent = $cursor->getIndent();
56 2157
        if ($indent >= 4) {
57 12
            return false;
58
        }
59
60 2157
        $tmpCursor = clone $cursor;
61 2157
        $tmpCursor->advanceToNextNonSpaceOrTab();
62 2157
        $rest = $tmpCursor->getRemainder();
63
64 2157
        if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) {
65 465
            $data = new ListData();
66 465
            $data->markerOffset = $indent;
67 465
            $data->type = ListBlock::TYPE_UNORDERED;
68 465
            $data->delimiter = null;
69 465
            $data->bulletChar = $rest[0];
70 465
            $markerLength = 1;
71 1950
        } elseif (($matches = RegexHelper::matchAll('/^(\d{1,9})([.)])/', $rest)) && (!($context->getContainer() instanceof Paragraph) || $matches[1] === '1')) {
72 96
            $data = new ListData();
73 96
            $data->markerOffset = $indent;
74 96
            $data->type = ListBlock::TYPE_ORDERED;
75 96
            $data->start = (int) $matches[1];
76 96
            $data->delimiter = $matches[2];
77 96
            $data->bulletChar = null;
78 96
            $markerLength = \strlen($matches[0]);
79
        } else {
80 1944
            return false;
81
        }
82
83
        // Make sure we have spaces after
84 537
        $nextChar = $tmpCursor->peek($markerLength);
85 537
        if (!($nextChar === null || $nextChar === "\t" || $nextChar === ' ')) {
86 258
            return false;
87
        }
88
89
        // If it interrupts paragraph, make sure first line isn't blank
90 288
        $container = $context->getContainer();
91 288
        if ($container instanceof Paragraph && !RegexHelper::matchAt(RegexHelper::REGEX_NON_SPACE, $rest, $markerLength)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \League\CommonMark\Util\..., $rest, $markerLength) of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. 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...
92 6
            return false;
93
        }
94
95
        // We've got a match! Advance offset and calculate padding
96 282
        $cursor->advanceToNextNonSpaceOrTab(); // to start of marker
97 282
        $cursor->advanceBy($markerLength, true); // to end of marker
98 282
        $data->padding = $this->calculateListMarkerPadding($cursor, $markerLength);
99
100
        // add the list if needed
101 282
        if (!($container instanceof ListBlock) || !$data->equals($container->getListData())) {
102 282
            $context->addBlock(new ListBlock($data));
103
        }
104
105
        // add the list item
106 282
        $context->addBlock(new ListItem($data));
107
108 282
        return true;
109
    }
110
111
    /**
112
     * @param Cursor $cursor
113
     * @param int    $markerLength
114
     *
115
     * @return int
116
     */
117 282
    private function calculateListMarkerPadding(Cursor $cursor, int $markerLength): int
118
    {
119 282
        $start = $cursor->saveState();
120 282
        $spacesStartCol = $cursor->getColumn();
121
122 282
        while ($cursor->getColumn() - $spacesStartCol < 5) {
123 282
            if (!$cursor->advanceBySpaceOrTab()) {
124 273
                break;
125
            }
126
        }
127
128 282
        $blankItem = $cursor->peek() === null;
129 282
        $spacesAfterMarker = $cursor->getColumn() - $spacesStartCol;
130
131 282
        if ($spacesAfterMarker >= 5 || $spacesAfterMarker < 1 || $blankItem) {
132 81
            $cursor->restoreState($start);
133 81
            $cursor->advanceBySpaceOrTab();
134
135 81
            return $markerLength + 1;
136
        }
137
138 219
        return $markerLength + $spacesAfterMarker;
139
    }
140
141 2157
    private function generateListMarkerRegex(): string
142
    {
143
        // No configuration given - use the defaults
144 2157
        if ($this->config === null) {
145 15
            return $this->listMarkerRegex = '/^[*+-]/';
146
        }
147
148 2142
        $markers = $this->config->get('unordered_list_markers', ['*', '+', '-']);
149
150 2142
        if (!\is_array($markers)) {
151 3
            throw new \RuntimeException('Invalid configuration option "unordered_list_markers": value must be an array of strings');
152
        }
153
154 2139
        return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/';
155
    }
156
}
157