Completed
Push — master ( 6c5f37...ab4467 )
by Colin
04:26 queued 03:25
created

MarkdownParser::updateReferenceMap()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
11
 *  - (c) John MacFarlane
12
 *
13
 * For the full copyright and license information, please view the LICENSE
14
 * file that was distributed with this source code.
15
 */
16
17
namespace League\CommonMark\Parser;
18
19
use League\CommonMark\Environment\EnvironmentInterface;
20
use League\CommonMark\Event\DocumentParsedEvent;
21
use League\CommonMark\Event\DocumentPreParsedEvent;
22
use League\CommonMark\Input\MarkdownInput;
23
use League\CommonMark\Node\Block\Document;
24
use League\CommonMark\Node\Block\Paragraph;
25
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
26
use League\CommonMark\Parser\Block\BlockStart;
27
use League\CommonMark\Parser\Block\BlockStartParserInterface;
28
use League\CommonMark\Parser\Block\DocumentBlockParser;
29
use League\CommonMark\Parser\Block\ParagraphParser;
30
use League\CommonMark\Reference\ReferenceInterface;
31
use League\CommonMark\Reference\ReferenceMap;
32
use League\CommonMark\Util\RegexHelper;
33
34
final class MarkdownParser implements MarkdownParserInterface
35
{
36
    /**
37
     * @var EnvironmentInterface
38
     *
39
     * @psalm-readonly
40
     */
41
    private $environment;
42
43
    /**
44
     * @var int|float
45
     *
46
     * @psalm-readonly
47
     */
48
    private $maxNestingLevel;
49
50
    /**
51
     * @var ReferenceMap
52
     *
53
     * @psalm-readonly-allow-private-mutation
54
     */
55
    private $referenceMap;
56
57
    /**
58
     * @var int
59
     *
60
     * @psalm-readonly-allow-private-mutation
61
     */
62
    private $lineNumber = 0;
63
64
    /**
65
     * @var Cursor
66
     *
67
     * @psalm-readonly-allow-private-mutation
68
     */
69
    private $cursor;
70
71
    /**
72
     * @var array<int, BlockContinueParserInterface>
73
     *
74
     * @psalm-readonly-allow-private-mutation
75
     */
76
    private $allBlockParsers = [];
77
78
    /**
79
     * @var array<int, BlockContinueParserInterface>
80
     *
81
     * @psalm-readonly-allow-private-mutation
82
     */
83
    private $activeBlockParsers = [];
84
85 2511
    public function __construct(EnvironmentInterface $environment)
86
    {
87 2511
        $this->environment     = $environment;
88 2511
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \INF);
89 2511
    }
90
91 2502
    private function initialize(): void
92
    {
93 2502
        $this->referenceMap       = new ReferenceMap();
94 2502
        $this->lineNumber         = 0;
95 2502
        $this->allBlockParsers    = [];
96 2502
        $this->activeBlockParsers = [];
97 2502
    }
98
99
    /**
100
     * @throws \RuntimeException
101
     */
102 2502
    public function parse(string $input): Document
103
    {
104 2502
        $this->initialize();
105
106 2502
        $documentParser = new DocumentBlockParser($this->referenceMap);
107 2502
        $this->activateBlockParser($documentParser);
108
109 2502
        $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
110 2496
        $this->environment->dispatch($preParsedEvent);
111 2496
        $markdown = $preParsedEvent->getMarkdown();
112
113 2496
        foreach ($markdown->getLines() as $line) {
114 2496
            ++$this->lineNumber;
115 2496
            $this->incorporateLine($line);
116
        }
117
118 2496
        $this->finalizeBlocks($this->getActiveBlockParsers(), $this->lineNumber);
119 2496
        $this->processInlines();
120
121 2496
        $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
122
123 2493
        return $documentParser->getBlock();
124
    }
125
126
    /**
127
     * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each
128
     * line of input, then finalizing the document.
129
     */
130 2496
    private function incorporateLine(string $line): void
131
    {
132 2496
        $this->cursor = new Cursor($line);
133
134 2496
        $matches = 1;
135 2496
        foreach ($this->getActiveBlockParsers(1) as $blockParser) {
136
            \assert($blockParser instanceof BlockContinueParserInterface);
137 1302
            $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser());
138 1302
            if ($blockContinue === null) {
139 828
                break;
140
            }
141
142 900
            if ($blockContinue->isFinalize()) {
143 87
                $this->finalizeAndClose($blockParser, $this->lineNumber);
144
145 87
                return;
146
            }
147
148 894
            if (($state = $blockContinue->getCursorState()) !== null) {
149 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...
150
            }
151
152 894
            $matches++;
153
        }
154
155 2496
        $unmatchedBlockParsers  = $this->getActiveBlockParsers($matches);
156 2496
        $lastMatchedBlockParser = $this->getActiveBlockParsers()[$matches - 1];
157 2496
        $blockParser            = $lastMatchedBlockParser;
158 2496
        $allClosed              = empty($unmatchedBlockParsers);
159
160
        // Unless last matched container is a code block, try new container starts
161 2496
        $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
162 2496
        while ($tryBlockStarts) {
163
            // this is a little performance optimization
164 2496
            if ($this->cursor->isBlank()) {
165 630
                $this->cursor->advanceToEnd();
166 630
                break;
167
            }
168
169 2496
            if (! $this->cursor->isIndented() && RegexHelper::isLetter($this->cursor->getNextNonSpaceCharacter())) {
170 1023
                $this->cursor->advanceToNextNonSpaceOrTab();
171 1023
                break;
172
            }
173
174 2112
            if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
175 3
                break;
176
            }
177
178 2112
            $blockStart = $this->findBlockStart($blockParser);
179 2112
            if ($blockStart === null) {
180 1299
                $this->cursor->advanceToNextNonSpaceOrTab();
181 1299
                break;
182
            }
183
184 954
            if (($state = $blockStart->getCursorState()) !== null) {
185 954
                $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...
186
            }
187
188 954
            if (! $allClosed) {
189 210
                $this->finalizeBlocks($unmatchedBlockParsers, $this->lineNumber - 1);
190 210
                $allClosed = true;
191
            }
192
193 954
            if ($blockStart->isReplaceActiveBlockParser()) {
194 132
                $this->prepareActiveBlockParserForReplacement();
195
            }
196
197 954
            foreach ($blockStart->getBlockParsers() as $newBlockParser) {
198 954
                $blockParser    = $this->addChild($newBlockParser);
199 954
                $tryBlockStarts = $newBlockParser->isContainer();
200
            }
201
        }
202
203
        // What remains ath the offset is a text line. Add the text to the appropriate block.
204
205
        // First check for a lazy paragraph continuation:
206 2496
        if (! $allClosed && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
207 48
            $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
208
        } else {
209
            // finalize any blocks not matched
210 2496
            if (! $allClosed) {
211 669
                $this->finalizeBlocks($unmatchedBlockParsers, $this->lineNumber);
212
            }
213
214 2496
            if (! $blockParser->isContainer()) {
215 981
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
216 2181
            } elseif (! $this->cursor->isBlank()) {
217 2133
                $this->addChild(new ParagraphParser());
218 2133
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
219
            }
220
        }
221 2496
    }
222
223 2112
    private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
224
    {
225 2112
        $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
226
227 2112
        foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
228
            \assert($blockStartParser instanceof BlockStartParserInterface);
229 2109
            if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) {
230 954
                return $result;
231
            }
232
        }
233
234 1299
        return null;
235
    }
236
237
    /**
238
     * @param array<int, BlockContinueParserInterface> $blockParsers
239
     */
240 2496
    private function finalizeBlocks(array $blockParsers, int $endLineNumber): void
241
    {
242 2496
        foreach (\array_reverse($blockParsers) as $blockParser) {
243 2496
            $this->finalizeAndClose($blockParser, $endLineNumber);
244
        }
245 2496
    }
246
247
    /**
248
     * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings,
249
     * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference
250
     * definitions.
251
     */
252 2496
    private function finalizeAndClose(BlockContinueParserInterface $blockParser, int $endLineNumber): void
253
    {
254 2496
        if ($this->getActiveBlockParser() === $blockParser) {
255 2496
            $this->deactivateBlockParser();
256
        }
257
258 2496
        if ($blockParser instanceof ParagraphParser) {
259 2031
            $this->updateReferenceMap($blockParser->getReferences());
260
        }
261
262 2496
        $blockParser->getBlock()->setEndLine($endLineNumber);
263 2496
        $blockParser->closeBlock();
264 2496
    }
265
266
    /**
267
     * Walk through a block & children recursively, parsing string content into inline content where appropriate.
268
     */
269 2496
    private function processInlines(): void
270
    {
271 2496
        $p = new InlineParserEngine($this->environment, $this->referenceMap);
272
273 2496
        foreach ($this->allBlockParsers as $blockParser) {
274
            \assert($blockParser instanceof BlockContinueParserInterface);
275 2496
            $blockParser->parseInlines($p);
276
        }
277 2496
    }
278
279
    /**
280
     * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
281
     * its parent, and so on til we find a block that can accept children.
282
     */
283 2496
    private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
284
    {
285 2496
        $blockParser->getBlock()->setStartLine($this->lineNumber);
286
287 2496
        while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
288 123
            $this->finalizeAndClose($this->getActiveBlockParser(), $this->lineNumber - 1);
289
        }
290
291 2496
        $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
292 2496
        $this->activateBlockParser($blockParser);
293
294 2496
        return $blockParser;
295
    }
296
297 2502
    private function activateBlockParser(BlockContinueParserInterface $blockParser): void
298
    {
299 2502
        $this->activeBlockParsers[] = $blockParser;
300 2502
        $this->allBlockParsers[]    = $blockParser;
301 2502
    }
302
303 2496
    private function deactivateBlockParser(): BlockContinueParserInterface
304
    {
305 2496
        $popped = \array_pop($this->activeBlockParsers);
306 2496
        if ($popped === null) {
307
            throw new \RuntimeException('The last block parser should not be deactivated');
308
        }
309
310 2496
        return $popped;
311
    }
312
313 132
    private function prepareActiveBlockParserForReplacement(): void
314
    {
315 132
        $old = $this->deactivateBlockParser();
316 132
        $key = \array_search($old, $this->allBlockParsers, true);
317 132
        unset($this->allBlockParsers[$key]);
318
319 132
        if ($old instanceof ParagraphParser) {
320 132
            $this->updateReferenceMap($old->getReferences());
321
        }
322
323 132
        $old->getBlock()->detach();
324 132
    }
325
326
    /**
327
     * @param ReferenceInterface[] $references
328
     */
329 2133
    private function updateReferenceMap(iterable $references): void
330
    {
331 2133
        foreach ($references as $reference) {
332 237
            if (! $this->referenceMap->contains($reference->getLabel())) {
333 237
                $this->referenceMap->add($reference);
334
            }
335
        }
336 2133
    }
337
338
    /**
339
     * @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...
340
     */
341 2496
    private function getActiveBlockParsers(?int $offset = 0): array
342
    {
343 2496
        if (\is_int($offset)) {
344 2496
            return \array_slice($this->activeBlockParsers, $offset);
345
        }
346
347
        return $this->activeBlockParsers;
348
    }
349
350 2496
    public function getActiveBlockParser(): BlockContinueParserInterface
351
    {
352 2496
        $active = \end($this->activeBlockParsers);
353 2496
        if ($active === false) {
354
            throw new \RuntimeException('No active block parsers are available');
355
        }
356
357 2496
        return $active;
358
    }
359
}
360