Passed
Push — table-start-line ( fd6132 )
by Colin
03:11
created

MarkdownParser::addChild()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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