Completed
Push — 2.0 ( bbf1d5...d95bff )
by Colin
20s queued 12s
created

MarkdownParser::parseLine()   D

Complexity

Conditions 19
Paths 71

Size

Total Lines 76
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 19

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 42
c 2
b 1
f 0
dl 0
loc 76
ccs 42
cts 42
cp 1
rs 4.5166
cc 19
nc 71
nop 1
crap 19

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