Passed
Push — 2.0 ( 040f68...c13575 )
by Colin
02:06
created

MarkdownParser::processInlines()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 3
c 3
b 0
f 0
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
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
37
final class MarkdownParser implements MarkdownParserInterface
38
{
39
    /** @psalm-readonly */
40
    private EnvironmentInterface $environment;
41
42
    /** @psalm-readonly-allow-private-mutation */
43
    private int $maxNestingLevel;
44
45
    /** @psalm-readonly-allow-private-mutation */
46
    private ReferenceMap $referenceMap;
47
48
    /** @psalm-readonly-allow-private-mutation */
49
    private int $lineNumber = 0;
50
51
    /** @psalm-readonly-allow-private-mutation */
52
    private Cursor $cursor;
53
54
    /**
55
     * @var array<int, BlockContinueParserInterface>
56
     *
57
     * @psalm-readonly-allow-private-mutation
58
     */
59
    private array $activeBlockParsers = [];
60
61
    /**
62
     * @var array<int, BlockContinueParserWithInlinesInterface>
63
     *
64
     * @psalm-readonly-allow-private-mutation
65
     */
66
    private array $closedBlockParsers = [];
67
68 2166
    public function __construct(EnvironmentInterface $environment)
69
    {
70 2166
        $this->environment = $environment;
71 2166
    }
72
73 2152
    private function initialize(): void
74
    {
75 2152
        $this->referenceMap       = new ReferenceMap();
76 2152
        $this->lineNumber         = 0;
77 2152
        $this->activeBlockParsers = [];
78 2152
        $this->closedBlockParsers = [];
79
80 2152
        $this->maxNestingLevel = $this->environment->getConfiguration()->get('max_nesting_level');
81 2144
    }
82
83
    /**
84
     * @throws \RuntimeException
85
     */
86 2152
    public function parse(string $input): Document
87
    {
88 2152
        $this->initialize();
89
90 2144
        $documentParser = new DocumentBlockParser($this->referenceMap);
91 2144
        $this->activateBlockParser($documentParser);
92
93 2144
        $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
94 2138
        $this->environment->dispatch($preParsedEvent);
95 2136
        $markdownInput = $preParsedEvent->getMarkdown();
96
97 2136
        foreach ($markdownInput->getLines() as $lineNumber => $line) {
98 2136
            $this->lineNumber = $lineNumber;
99 2136
            $this->parseLine($line);
100
        }
101
102
        // finalizeAndProcess
103 2136
        $this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber);
104 2136
        $this->processInlines();
105
106 2136
        $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
107
108 2136
        return $documentParser->getBlock();
109
    }
110
111
    /**
112
     * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each
113
     * line of input, then finalizing the document.
114
     */
115 2136
    private function parseLine(string $line): void
116
    {
117 2136
        $this->cursor = new Cursor($line);
118
119 2136
        $matches = $this->parseBlockContinuation();
120 2136
        if ($matches === null) {
121 64
            return;
122
        }
123
124 2136
        $unmatchedBlocks = \count($this->activeBlockParsers) - $matches;
125 2136
        $blockParser     = $this->activeBlockParsers[$matches - 1];
126 2136
        $startedNewBlock = false;
127
128
        // Unless last matched container is a code block, try new container starts,
129
        // adding children to the last matched container:
130 2136
        $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
131 2136
        while ($tryBlockStarts) {
132
            // this is a little performance optimization
133 2136
            if ($this->cursor->isBlank()) {
134 596
                $this->cursor->advanceToEnd();
135 596
                break;
136
            }
137
138 2136
            if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
139 4
                break;
140
            }
141
142 2136
            $blockStart = $this->findBlockStart($blockParser);
143 2136
            if ($blockStart === null || $blockStart->isAborting()) {
144 1858
                $this->cursor->advanceToNextNonSpaceOrTab();
145 1858
                break;
146
            }
147
148 824
            if (($state = $blockStart->getCursorState()) !== null) {
149 824
                $this->cursor->restoreState($state);
150
            }
151
152 824
            $startedNewBlock = true;
153
154
            // We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now.
155 824
            if ($unmatchedBlocks > 0) {
156 204
                $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1);
157 204
                $unmatchedBlocks = 0;
158
            }
159
160 824
            if ($blockStart->isReplaceActiveBlockParser()) {
161 130
                $this->prepareActiveBlockParserForReplacement();
162
            }
163
164 824
            foreach ($blockStart->getBlockParsers() as $newBlockParser) {
165 824
                $blockParser    = $this->addChild($newBlockParser);
166 824
                $tryBlockStarts = $newBlockParser->isContainer();
167
            }
168
        }
169
170
        // What remains at the offset is a text line. Add the text to the appropriate block.
171
172
        // First check for a lazy paragraph continuation:
173 2136
        if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
174 346
            $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
175
        } else {
176
            // finalize any blocks not matched
177 2136
            if ($unmatchedBlocks > 0) {
178 620
                $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber);
179
            }
180
181 2136
            if (! $blockParser->isContainer()) {
182 586
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
183 1904
            } elseif (! $this->cursor->isBlank()) {
184 1862
                $this->addChild(new ParagraphParser());
185 1862
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
186
            }
187
        }
188 2136
    }
189
190 2136
    private function parseBlockContinuation(): ?int
191
    {
192
        // For each containing block, try to parse the associated line start.
193
        // The document will always match, so we can skip the first block parser and start at 1 matches
194 2136
        $matches = 1;
195 2136
        for ($i = 1; $i < \count($this->activeBlockParsers); $i++) {
196 1086
            $blockParser   = $this->activeBlockParsers[$i];
197 1086
            $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser());
198 1086
            if ($blockContinue === null) {
199 742
                break;
200
            }
201
202 714
            if ($blockContinue->isFinalize()) {
203 64
                $this->closeBlockParsers(\count($this->activeBlockParsers) - $i, $this->lineNumber);
204
205 64
                return null;
206
            }
207
208 710
            if (($state = $blockContinue->getCursorState()) !== null) {
209 710
                $this->cursor->restoreState($state);
210
            }
211
212 710
            $matches++;
213
        }
214
215 2136
        return $matches;
216
    }
217
218 2136
    private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
219
    {
220 2136
        $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
221
222 2136
        foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
223
            \assert($blockStartParser instanceof BlockStartParserInterface);
224 2136
            if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) {
225 1394
                return $result;
226
            }
227
        }
228
229 980
        return null;
230
    }
231
232 2136
    private function closeBlockParsers(int $count, int $endLineNumber): void
233
    {
234 2136
        for ($i = 0; $i < $count; $i++) {
235 2136
            $blockParser = $this->deactivateBlockParser();
236 2136
            $this->finalize($blockParser, $endLineNumber);
237
238
            // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
239 2136
            if ($blockParser instanceof BlockContinueParserWithInlinesInterface) {
240
                // Remember for inline parsing
241 1932
                $this->closedBlockParsers[] = $blockParser;
242
            }
243
        }
244 2136
    }
245
246
    /**
247
     * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings,
248
     * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference
249
     * definitions.
250
     */
251 2136
    private function finalize(BlockContinueParserInterface $blockParser, int $endLineNumber): void
252
    {
253 2136
        if ($blockParser instanceof ParagraphParser) {
254 1780
            $this->updateReferenceMap($blockParser->getReferences());
255
        }
256
257 2136
        $blockParser->getBlock()->setEndLine($endLineNumber);
258 2136
        $blockParser->closeBlock();
259 2136
    }
260
261
    /**
262
     * Walk through a block & children recursively, parsing string content into inline content where appropriate.
263
     */
264 2136
    private function processInlines(): void
265
    {
266 2136
        $p = new InlineParserEngine($this->environment, $this->referenceMap);
267
268 2136
        foreach ($this->closedBlockParsers as $blockParser) {
269 1932
            $blockParser->parseInlines($p);
270
        }
271 2136
    }
272
273
    /**
274
     * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
275
     * its parent, and so on til we find a block that can accept children.
276
     */
277 2136
    private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
278
    {
279 2136
        $blockParser->getBlock()->setStartLine($this->lineNumber);
280
281 2136
        while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
282 120
            $this->closeBlockParsers(1, $this->lineNumber - 1);
283
        }
284
285 2136
        $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
286 2136
        $this->activateBlockParser($blockParser);
287
288 2136
        return $blockParser;
289
    }
290
291 2144
    private function activateBlockParser(BlockContinueParserInterface $blockParser): void
292
    {
293 2144
        $this->activeBlockParsers[] = $blockParser;
294 2144
    }
295
296 2136
    private function deactivateBlockParser(): BlockContinueParserInterface
297
    {
298 2136
        $popped = \array_pop($this->activeBlockParsers);
299 2136
        if ($popped === null) {
300
            throw new \RuntimeException('The last block parser should not be deactivated');
301
        }
302
303 2136
        return $popped;
304
    }
305
306 130
    private function prepareActiveBlockParserForReplacement(): void
307
    {
308
        // Note that we don't want to parse inlines or finalize this block, as it's getting replaced.
309 130
        $old = $this->deactivateBlockParser();
310
311 130
        if ($old instanceof ParagraphParser) {
312 130
            $this->updateReferenceMap($old->getReferences());
313
        }
314
315 130
        $old->getBlock()->detach();
316 130
    }
317
318
    /**
319
     * @param ReferenceInterface[] $references
320
     */
321 1862
    private function updateReferenceMap(iterable $references): void
322
    {
323 1862
        foreach ($references as $reference) {
324 162
            if (! $this->referenceMap->contains($reference->getLabel())) {
325 162
                $this->referenceMap->add($reference);
326
            }
327
        }
328 1862
    }
329
330 2136
    public function getActiveBlockParser(): BlockContinueParserInterface
331
    {
332 2136
        $active = \end($this->activeBlockParsers);
333 2136
        if ($active === false) {
334
            throw new \RuntimeException('No active block parsers are available');
335
        }
336
337 2136
        return $active;
338
    }
339
}
340