Completed
Push — master ( d4d1b7...0de0ea )
by Colin
01:02
created

MarkdownParser::getActiveBlockParsers()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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