Completed
Push — refactor-parsing ( adbb6b...fbe6de )
by Colin
08:21 queued 07:01
created

MarkdownParser::incorporateLine()   F

Complexity

Conditions 22
Paths 141

Size

Total Lines 90

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 22

Importance

Changes 0
Metric Value
dl 0
loc 90
ccs 51
cts 51
cp 1
rs 3.825
c 0
b 0
f 0
cc 22
nc 141
nop 1
crap 22

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
/*
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\Parser;
16
17
use League\CommonMark\Environment\EnvironmentInterface;
18
use League\CommonMark\Event\DocumentParsedEvent;
19
use League\CommonMark\Event\DocumentPreParsedEvent;
20
use League\CommonMark\Input\MarkdownInput;
21
use League\CommonMark\Node\Block\Document;
22
use League\CommonMark\Node\Block\Paragraph;
23
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
24
use League\CommonMark\Parser\Block\BlockStart;
25
use League\CommonMark\Parser\Block\BlockStartParserInterface;
26
use League\CommonMark\Parser\Block\DocumentBlockParser;
27
use League\CommonMark\Parser\Block\ParagraphParser;
28
use League\CommonMark\Reference\ReferenceInterface;
29
use League\CommonMark\Reference\ReferenceMap;
30
use League\CommonMark\Util\RegexHelper;
31
32
final class MarkdownParser implements MarkdownParserInterface
33
{
34
    /** @var EnvironmentInterface */
35
    private $environment;
36
37
    /** @var int|float */
38
    private $maxNestingLevel;
39
40
    /** @var ReferenceMap */
41
    private $referenceMap;
42
43
    /** @var int */
44
    private $lineNumber = 0;
45
46
    /** @var Cursor */
47
    private $cursor;
48
49
    /** @var array<int, BlockContinueParserInterface> */
50
    private $allBlockParsers = [];
51
52
    /** @var array<int, BlockContinueParserInterface> */
53
    private $activeBlockParsers = [];
54
55
    /**
56
     * @param EnvironmentInterface $environment
57
     */
58 2508
    public function __construct(EnvironmentInterface $environment)
59
    {
60 2508
        $this->environment = $environment;
61 2508
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \INF);
62 2508
    }
63
64 2499
    private function initialize(): void
65
    {
66 2499
        $this->referenceMap = new ReferenceMap();
67 2499
        $this->lineNumber = 0;
68 2499
        $this->allBlockParsers = [];
69 2499
        $this->activeBlockParsers = [];
70 2499
    }
71
72
    /**
73
     * @param string $input
74
     *
75
     * @throws \RuntimeException
76
     *
77
     * @return Document
78
     */
79 2499
    public function parse(string $input): Document
80
    {
81 2499
        $this->initialize();
82
83 2499
        $documentParser = new DocumentBlockParser($this->referenceMap);
84 2499
        $this->activateBlockParser($documentParser);
85
86 2499
        $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
87 2493
        $this->environment->dispatch($preParsedEvent);
88 2493
        $markdown = $preParsedEvent->getMarkdown();
89
90 2493
        foreach ($markdown->getLines() as $line) {
91 2493
            ++$this->lineNumber;
92 2493
            $this->incorporateLine($line);
93
        }
94
95 2493
        $this->finalizeBlocks($this->getActiveBlockParsers(), $this->lineNumber);
96 2493
        $this->processInlines();
97
98 2493
        $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
99
100 2490
        return $documentParser->getBlock();
101
    }
102
103
    /**
104
     * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each
105
     * line of input, then finalizing the document.
106
     *
107
     * @param string $line
108
     */
109 2493
    private function incorporateLine(string $line): void
110
    {
111 2493
        $this->cursor = new Cursor($line);
112
113 2493
        $matches = 1;
114
        /** @var BlockContinueParserInterface $blockParser */
115 2493
        foreach ($this->getActiveBlockParsers(1) as $blockParser) {
116 1299
            $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser());
117 1299
            if ($blockContinue === null) {
118 825
                break;
119
            }
120
121 900
            if ($blockContinue->isFinalize()) {
122 87
                $this->finalizeAndClose($blockParser, $this->lineNumber);
123
124 87
                return;
125
            }
126
127 894
            if (($state = $blockContinue->getCursorState()) !== null) {
128 894
                $this->cursor->restoreState($state);
0 ignored issues
show
Unused Code introduced by
The call to the method League\CommonMark\Parser\Cursor::restoreState() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
129
            }
130
131 894
            $matches++;
132
        }
133
134 2493
        $unmatchedBlockParsers = $this->getActiveBlockParsers($matches);
135 2493
        $lastMatchedBlockParser = $this->getActiveBlockParsers()[$matches - 1];
136 2493
        $blockParser = $lastMatchedBlockParser;
137 2493
        $allClosed = empty($unmatchedBlockParsers);
138
139
        // Unless last matched container is a code block, try new container starts
140 2493
        $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
141 2493
        while ($tryBlockStarts) {
142
            // this is a little performance optimization
143 2493
            if ($this->cursor->isBlank()) {
144 627
                $this->cursor->advanceToEnd();
145 627
                break;
146 2493
            } elseif (!$this->cursor->isIndented() && RegexHelper::isLetter($this->cursor->getNextNonSpaceCharacter())) {
147 1020
                $this->cursor->advanceToNextNonSpaceOrTab();
148 1020
                break;
149
            }
150
151 2109
            if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
152 3
                break;
153
            }
154
155 2109
            $blockStart = $this->findBlockStart($blockParser);
156 2109
            if ($blockStart === null) {
157 1296
                $this->cursor->advanceToNextNonSpaceOrTab();
158 1296
                break;
159
            }
160
161 951
            if (($state = $blockStart->getCursorState()) !== null) {
162 951
                $this->cursor->restoreState($state);
0 ignored issues
show
Unused Code introduced by
The call to the method League\CommonMark\Parser\Cursor::restoreState() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
163
            }
164
165 951
            if (!$allClosed) {
166 210
                $this->finalizeBlocks($unmatchedBlockParsers, $this->lineNumber - 1);
167 210
                $allClosed = true;
168
            }
169
170 951
            if ($blockStart->isReplaceActiveBlockParser()) {
171 132
                $this->prepareActiveBlockParserForReplacement();
172
            }
173
174 951
            foreach ($blockStart->getBlockParsers() as $newBlockParser) {
175 951
                $blockParser = $this->addChild($newBlockParser);
176 951
                $tryBlockStarts = $newBlockParser->isContainer();
177
            }
178
        }
179
180
        // What remains ath the offset is a text line. Add the text to the appropriate block.
181
182
        // First check for a lazy paragraph continuation:
183 2493
        if (!$allClosed && !$this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
184 48
            $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
185
        } else {
186
            // finalize any blocks not matched
187 2493
            if (!$allClosed) {
188 666
                $this->finalizeBlocks($unmatchedBlockParsers, $this->lineNumber);
189
            }
190
191 2493
            if (!$blockParser->isContainer()) {
192 978
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
193 2178
            } elseif (!$this->cursor->isBlank()) {
194 2130
                $this->addChild(new ParagraphParser());
195 2130
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
196
            }
197
        }
198 2493
    }
199
200 2109
    private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
201
    {
202 2109
        $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
203
204
        /** @var BlockStartParserInterface $blockStartParser */
205 2109
        foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
206 2106
            if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) {
207 951
                return $result;
208
            }
209
        }
210
211 1296
        return null;
212
    }
213
214
    /**
215
     * @param array<int, BlockContinueParserInterface> $blockParsers
216
     * @param int                                      $endLineNumber
217
     */
218 2493
    private function finalizeBlocks(array $blockParsers, int $endLineNumber): void
219
    {
220 2493
        foreach (\array_reverse($blockParsers) as $blockParser) {
221 2493
            $this->finalizeAndClose($blockParser, $endLineNumber);
222
        }
223 2493
    }
224
225
    /**
226
     * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings,
227
     * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference
228
     * definitions.
229
     */
230 2493
    private function finalizeAndClose(BlockContinueParserInterface $blockParser, int $endLineNumber): void
231
    {
232 2493
        if ($this->getActiveBlockParser() === $blockParser) {
233 2493
            $this->deactivateBlockParser();
234
        }
235
236 2493
        if ($blockParser instanceof ParagraphParser) {
237 2028
            $this->updateReferenceMap($blockParser->getReferences());
238
        }
239
240 2493
        $blockParser->getBlock()->setEndLine($endLineNumber);
241 2493
        $blockParser->closeBlock();
242 2493
    }
243
244
    /**
245
     * Walk through a block & children recursively, parsing string content into inline content where appropriate.
246
     */
247 2493
    private function processInlines(): void
248
    {
249 2493
        $p = new InlineParserEngine($this->environment, $this->referenceMap);
250
251
        /** @var BlockContinueParserInterface $blockParser */
252 2493
        foreach ($this->allBlockParsers as $blockParser) {
253 2493
            $blockParser->parseInlines($p);
254
        }
255 2493
    }
256
257
    /**
258
     * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
259
     * its parent, and so on til we find a block that can accept children.
260
     */
261 2493
    private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
262
    {
263 2493
        $blockParser->getBlock()->setStartLine($this->lineNumber);
264
265 2493
        while (!$this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
266 123
            $this->finalizeAndClose($this->getActiveBlockParser(), $this->lineNumber - 1);
267
        }
268
269 2493
        $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
270 2493
        $this->activateBlockParser($blockParser);
271
272 2493
        return $blockParser;
273
    }
274
275 2499
    private function activateBlockParser(BlockContinueParserInterface $blockParser): void
276
    {
277 2499
        $this->activeBlockParsers[] = $blockParser;
278 2499
        $this->allBlockParsers[] = $blockParser;
279 2499
    }
280
281 2493
    private function deactivateBlockParser(): BlockContinueParserInterface
282
    {
283 2493
        return \array_pop($this->activeBlockParsers);
284
    }
285
286 132
    private function prepareActiveBlockParserForReplacement(): void
287
    {
288 132
        $old = $this->deactivateBlockParser();
289 132
        $key = \array_search($old, $this->allBlockParsers, true);
290 132
        unset($this->allBlockParsers[$key]);
291
292 132
        if ($old instanceof ParagraphParser) {
293 132
            $this->updateReferenceMap($old->getReferences());
294
        }
295
296 132
        $old->getBlock()->detach();
297 132
    }
298
299
    /**
300
     * @param ReferenceInterface[] $references
301
     */
302 2130
    private function updateReferenceMap(iterable $references): void
303
    {
304 2130
        foreach ($references as $reference) {
305 234
            if (!$this->referenceMap->contains($reference->getLabel())) {
306 234
                $this->referenceMap->add($reference);
307
            }
308
        }
309 2130
    }
310
311
    /**
312
     * @param int|null $offset
313
     *
314
     * @return array<int, BlockContinueParserInterface>
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...
315
     */
316 2493
    private function getActiveBlockParsers(?int $offset = 0): array
317
    {
318 2493
        if (\is_int($offset)) {
319 2493
            return \array_slice($this->activeBlockParsers, $offset);
320
        }
321
322
        return $this->activeBlockParsers;
323
    }
324
325 2493
    public function getActiveBlockParser(): BlockContinueParserInterface
326
    {
327 2493
        $active = \end($this->activeBlockParsers);
328 2493
        if ($active === false) {
329
            throw new \RuntimeException('No active block parsers are available');
330
        }
331
332 2493
        return $active;
333
    }
334
}
335