MarkdownParser::incorporateLine()   F
last analyzed

Complexity

Conditions 22
Paths 141

Size

Total Lines 89
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 22

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 51
dl 0
loc 89
ccs 50
cts 50
cp 1
rs 3.825
c 1
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
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 2847
    public function __construct(EnvironmentInterface $environment)
86
    {
87 2847
        $this->environment     = $environment;
88 2847
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \INF);
89 2847
    }
90
91 2838
    private function initialize(): void
92
    {
93 2838
        $this->referenceMap       = new ReferenceMap();
94 2838
        $this->lineNumber         = 0;
95 2838
        $this->allBlockParsers    = [];
96 2838
        $this->activeBlockParsers = [];
97 2838
    }
98
99
    /**
100
     * @throws \RuntimeException
101
     */
102 2838
    public function parse(string $input): Document
103
    {
104 2838
        $this->initialize();
105
106 2838
        $documentParser = new DocumentBlockParser($this->referenceMap);
107 2838
        $this->activateBlockParser($documentParser);
108
109 2838
        $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
110 2832
        $this->environment->dispatch($preParsedEvent);
111 2829
        $markdown = $preParsedEvent->getMarkdown();
112
113 2829
        foreach ($markdown->getLines() as $line) {
114 2829
            ++$this->lineNumber;
115 2829
            $this->incorporateLine($line);
116
        }
117
118 2829
        $this->finalizeBlocks($this->getActiveBlockParsers(), $this->lineNumber);
119 2829
        $this->processInlines();
120
121 2829
        $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
122
123 2826
        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 2829
    private function incorporateLine(string $line): void
131
    {
132 2829
        $this->cursor = new Cursor($line);
133
134 2829
        $matches = 1;
135 2829
        foreach ($this->getActiveBlockParsers(1) as $blockParser) {
136
            \assert($blockParser instanceof BlockContinueParserInterface);
137 1389
            $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser());
138 1389
            if ($blockContinue === null) {
139 906
                break;
140
            }
141
142 948
            if ($blockContinue->isFinalize()) {
143 90
                $this->finalizeAndClose($blockParser, $this->lineNumber);
144
145 90
                return;
146
            }
147
148 942
            if (($state = $blockContinue->getCursorState()) !== null) {
149 942
                $this->cursor->restoreState($state);
150
            }
151
152 942
            $matches++;
153
        }
154
155 2829
        $unmatchedBlockParsers  = $this->getActiveBlockParsers($matches);
156 2829
        $lastMatchedBlockParser = $this->getActiveBlockParsers()[$matches - 1];
157 2829
        $blockParser            = $lastMatchedBlockParser;
158 2829
        $allClosed              = \count($unmatchedBlockParsers) === 0;
159
160
        // Unless last matched container is a code block, try new container starts
161 2829
        $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
162 2829
        while ($tryBlockStarts) {
163
            // this is a little performance optimization
164 2829
            if ($this->cursor->isBlank()) {
165 699
                $this->cursor->advanceToEnd();
166 699
                break;
167
            }
168
169 2829
            if (! $this->cursor->isIndented() && RegexHelper::isLetter($this->cursor->getNextNonSpaceCharacter())) {
170 1344
                $this->cursor->advanceToNextNonSpaceOrTab();
171 1344
                break;
172
            }
173
174 2199
            if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
175 3
                break;
176
            }
177
178 2199
            $blockStart = $this->findBlockStart($blockParser);
179 2199
            if ($blockStart === null) {
180 1317
                $this->cursor->advanceToNextNonSpaceOrTab();
181 1317
                break;
182
            }
183
184 1035
            if (($state = $blockStart->getCursorState()) !== null) {
185 1035
                $this->cursor->restoreState($state);
186
            }
187
188 1035
            if (! $allClosed) {
189 258
                $this->finalizeBlocks($unmatchedBlockParsers, $this->lineNumber - 1);
190 258
                $allClosed = true;
191
            }
192
193 1035
            if ($blockStart->isReplaceActiveBlockParser()) {
194 141
                $this->prepareActiveBlockParserForReplacement();
195
            }
196
197 1035
            foreach ($blockStart->getBlockParsers() as $newBlockParser) {
198 1035
                $blockParser    = $this->addChild($newBlockParser);
199 1035
                $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 2829
        if (! $allClosed && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
207 69
            $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
208
        } else {
209
            // finalize any blocks not matched
210 2829
            if (! $allClosed) {
211 738
                $this->finalizeBlocks($unmatchedBlockParsers, $this->lineNumber);
212
            }
213
214 2829
            if (! $blockParser->isContainer()) {
215 1032
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
216 2505
            } elseif (! $this->cursor->isBlank()) {
217 2457
                $this->addChild(new ParagraphParser());
218 2457
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
219
            }
220
        }
221 2829
    }
222
223 2199
    private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
224
    {
225 2199
        $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
226
227 2199
        foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
228
            \assert($blockStartParser instanceof BlockStartParserInterface);
229 2196
            if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) {
230 1035
                return $result;
231
            }
232
        }
233
234 1317
        return null;
235
    }
236
237
    /**
238
     * @param array<int, BlockContinueParserInterface> $blockParsers
239
     */
240 2829
    private function finalizeBlocks(array $blockParsers, int $endLineNumber): void
241
    {
242 2829
        foreach (\array_reverse($blockParsers) as $blockParser) {
243 2829
            $this->finalizeAndClose($blockParser, $endLineNumber);
244
        }
245 2829
    }
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 2829
    private function finalizeAndClose(BlockContinueParserInterface $blockParser, int $endLineNumber): void
253
    {
254 2829
        if ($this->getActiveBlockParser() === $blockParser) {
255 2829
            $this->deactivateBlockParser();
256
        }
257
258 2829
        if ($blockParser instanceof ParagraphParser) {
259 2352
            $this->updateReferenceMap($blockParser->getReferences());
260
        }
261
262 2829
        $blockParser->getBlock()->setEndLine($endLineNumber);
263 2829
        $blockParser->closeBlock();
264 2829
    }
265
266
    /**
267
     * Walk through a block & children recursively, parsing string content into inline content where appropriate.
268
     */
269 2829
    private function processInlines(): void
270
    {
271 2829
        $p = new InlineParserEngine($this->environment, $this->referenceMap);
272
273 2829
        foreach ($this->allBlockParsers as $blockParser) {
274
            \assert($blockParser instanceof BlockContinueParserInterface);
275 2829
            $blockParser->parseInlines($p);
276
        }
277 2829
    }
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 2829
    private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
284
    {
285 2829
        $blockParser->getBlock()->setStartLine($this->lineNumber);
286
287 2829
        while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
288 135
            $this->finalizeAndClose($this->getActiveBlockParser(), $this->lineNumber - 1);
289
        }
290
291 2829
        $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
292 2829
        $this->activateBlockParser($blockParser);
293
294 2829
        return $blockParser;
295
    }
296
297 2838
    private function activateBlockParser(BlockContinueParserInterface $blockParser): void
298
    {
299 2838
        $this->activeBlockParsers[] = $blockParser;
300 2838
        $this->allBlockParsers[]    = $blockParser;
301 2838
    }
302
303 2829
    private function deactivateBlockParser(): BlockContinueParserInterface
304
    {
305 2829
        $popped = \array_pop($this->activeBlockParsers);
306 2829
        if ($popped === null) {
307
            throw new \RuntimeException('The last block parser should not be deactivated');
308
        }
309
310 2829
        return $popped;
311
    }
312
313 141
    private function prepareActiveBlockParserForReplacement(): void
314
    {
315 141
        $old = $this->deactivateBlockParser();
316 141
        $key = \array_search($old, $this->allBlockParsers, true);
317 141
        unset($this->allBlockParsers[$key]);
318
319 141
        if ($old instanceof ParagraphParser) {
320 141
            $this->updateReferenceMap($old->getReferences());
321
        }
322
323 141
        $old->getBlock()->detach();
324 141
    }
325
326
    /**
327
     * @param ReferenceInterface[] $references
328
     */
329 2457
    private function updateReferenceMap(iterable $references): void
330
    {
331 2457
        foreach ($references as $reference) {
332 237
            if (! $this->referenceMap->contains($reference->getLabel())) {
333 237
                $this->referenceMap->add($reference);
334
            }
335
        }
336 2457
    }
337
338
    /**
339
     * @return array<int, BlockContinueParserInterface>
340
     */
341 2829
    private function getActiveBlockParsers(?int $offset = 0): array
342
    {
343 2829
        if (\is_int($offset)) {
344 2829
            return \array_slice($this->activeBlockParsers, $offset);
345
        }
346
347
        return $this->activeBlockParsers;
348
    }
349
350 2829
    public function getActiveBlockParser(): BlockContinueParserInterface
351
    {
352 2829
        $active = \end($this->activeBlockParsers);
353 2829
        if ($active === false) {
354
            throw new \RuntimeException('No active block parsers are available');
355
        }
356
357 2829
        return $active;
358
    }
359
}
360