Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 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 <colinodell@gmail.com>
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;
991 lars 25
use League\CommonMark\Exception\CommonMarkException;
148 lars 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
    public function __construct(EnvironmentInterface $environment)
70
    {
71
        $this->environment = $environment;
72
    }
73
 
74
    private function initialize(): void
75
    {
76
        $this->referenceMap       = new ReferenceMap();
77
        $this->lineNumber         = 0;
78
        $this->activeBlockParsers = [];
79
        $this->closedBlockParsers = [];
80
 
81
        $this->maxNestingLevel = $this->environment->getConfiguration()->get('max_nesting_level');
82
    }
83
 
84
    /**
991 lars 85
     * @throws CommonMarkException
148 lars 86
     */
87
    public function parse(string $input): Document
88
    {
89
        $this->initialize();
90
 
91
        $documentParser = new DocumentBlockParser($this->referenceMap);
92
        $this->activateBlockParser($documentParser);
93
 
94
        $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
95
        $this->environment->dispatch($preParsedEvent);
96
        $markdownInput = $preParsedEvent->getMarkdown();
97
 
98
        foreach ($markdownInput->getLines() as $lineNumber => $line) {
99
            $this->lineNumber = $lineNumber;
100
            $this->parseLine($line);
101
        }
102
 
103
        // finalizeAndProcess
104
        $this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber);
105
        $this->processInlines();
106
 
107
        $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
108
 
109
        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
    private function parseLine(string $line): void
117
    {
118
        $this->cursor = new Cursor($line);
119
 
120
        $matches = $this->parseBlockContinuation();
121
        if ($matches === null) {
122
            return;
123
        }
124
 
125
        $unmatchedBlocks = \count($this->activeBlockParsers) - $matches;
126
        $blockParser     = $this->activeBlockParsers[$matches - 1];
127
        $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
        $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
132
        while ($tryBlockStarts) {
133
            // this is a little performance optimization
134
            if ($this->cursor->isBlank()) {
135
                $this->cursor->advanceToEnd();
136
                break;
137
            }
138
 
139
            if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
140
                break;
141
            }
142
 
143
            $blockStart = $this->findBlockStart($blockParser);
144
            if ($blockStart === null || $blockStart->isAborting()) {
145
                $this->cursor->advanceToNextNonSpaceOrTab();
146
                break;
147
            }
148
 
149
            if (($state = $blockStart->getCursorState()) !== null) {
150
                $this->cursor->restoreState($state);
151
            }
152
 
153
            $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
            if ($unmatchedBlocks > 0) {
157
                $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1);
158
                $unmatchedBlocks = 0;
159
            }
160
 
161
            if ($blockStart->isReplaceActiveBlockParser()) {
162
                $this->prepareActiveBlockParserForReplacement();
163
            }
164
 
165
            foreach ($blockStart->getBlockParsers() as $newBlockParser) {
166
                $blockParser    = $this->addChild($newBlockParser);
167
                $tryBlockStarts = $newBlockParser->isContainer();
168
            }
169
        }
170
 
171
        // What remains at the offset is a text line. Add the text to the appropriate block.
172
 
173
        // First check for a lazy paragraph continuation:
174
        if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
175
            $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
176
        } else {
177
            // finalize any blocks not matched
178
            if ($unmatchedBlocks > 0) {
179
                $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber);
180
            }
181
 
182
            if (! $blockParser->isContainer()) {
183
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
184
            } elseif (! $this->cursor->isBlank()) {
185
                $this->addChild(new ParagraphParser());
186
                $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
187
            }
188
        }
189
    }
190
 
191
    private function parseBlockContinuation(): ?int
192
    {
193
        // For each containing block, try to parse the associated line start.
194
        // The document will always match, so we can skip the first block parser and start at 1 matches
195
        $matches = 1;
196
        for ($i = 1; $i < \count($this->activeBlockParsers); $i++) {
197
            $blockParser   = $this->activeBlockParsers[$i];
198
            $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser());
199
            if ($blockContinue === null) {
200
                break;
201
            }
202
 
203
            if ($blockContinue->isFinalize()) {
204
                $this->closeBlockParsers(\count($this->activeBlockParsers) - $i, $this->lineNumber);
205
 
206
                return null;
207
            }
208
 
209
            if (($state = $blockContinue->getCursorState()) !== null) {
210
                $this->cursor->restoreState($state);
211
            }
212
 
213
            $matches++;
214
        }
215
 
216
        return $matches;
217
    }
218
 
219
    private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
220
    {
221
        $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
222
 
223
        foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
224
            \assert($blockStartParser instanceof BlockStartParserInterface);
225
            if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) {
226
                return $result;
227
            }
228
        }
229
 
230
        return null;
231
    }
232
 
233
    private function closeBlockParsers(int $count, int $endLineNumber): void
234
    {
235
        for ($i = 0; $i < $count; $i++) {
236
            $blockParser = $this->deactivateBlockParser();
237
            $this->finalize($blockParser, $endLineNumber);
238
 
239
            // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
240
            if ($blockParser instanceof BlockContinueParserWithInlinesInterface) {
241
                // Remember for inline parsing
242
                $this->closedBlockParsers[] = $blockParser;
243
            }
244
        }
245
    }
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
    private function finalize(BlockContinueParserInterface $blockParser, int $endLineNumber): void
253
    {
254
        if ($blockParser instanceof ParagraphParser) {
255
            $this->updateReferenceMap($blockParser->getReferences());
256
        }
257
 
258
        $blockParser->getBlock()->setEndLine($endLineNumber);
259
        $blockParser->closeBlock();
260
    }
261
 
262
    /**
263
     * Walk through a block & children recursively, parsing string content into inline content where appropriate.
264
     */
265
    private function processInlines(): void
266
    {
267
        $p = new InlineParserEngine($this->environment, $this->referenceMap);
268
 
269
        foreach ($this->closedBlockParsers as $blockParser) {
270
            $blockParser->parseInlines($p);
271
        }
272
    }
273
 
274
    /**
275
     * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
276
     * its parent, and so on til we find a block that can accept children.
277
     */
278
    private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
279
    {
280
        $blockParser->getBlock()->setStartLine($this->lineNumber);
281
 
282
        while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
283
            $this->closeBlockParsers(1, $this->lineNumber - 1);
284
        }
285
 
286
        $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
287
        $this->activateBlockParser($blockParser);
288
 
289
        return $blockParser;
290
    }
291
 
292
    private function activateBlockParser(BlockContinueParserInterface $blockParser): void
293
    {
294
        $this->activeBlockParsers[] = $blockParser;
295
    }
296
 
991 lars 297
    /**
298
     * @throws ParserLogicException
299
     */
148 lars 300
    private function deactivateBlockParser(): BlockContinueParserInterface
301
    {
302
        $popped = \array_pop($this->activeBlockParsers);
303
        if ($popped === null) {
991 lars 304
            throw new ParserLogicException('The last block parser should not be deactivated');
148 lars 305
        }
306
 
307
        return $popped;
308
    }
309
 
310
    private function prepareActiveBlockParserForReplacement(): void
311
    {
312
        // Note that we don't want to parse inlines or finalize this block, as it's getting replaced.
313
        $old = $this->deactivateBlockParser();
314
 
315
        if ($old instanceof ParagraphParser) {
316
            $this->updateReferenceMap($old->getReferences());
317
        }
318
 
319
        $old->getBlock()->detach();
320
    }
321
 
322
    /**
323
     * @param ReferenceInterface[] $references
324
     */
325
    private function updateReferenceMap(iterable $references): void
326
    {
327
        foreach ($references as $reference) {
328
            if (! $this->referenceMap->contains($reference->getLabel())) {
329
                $this->referenceMap->add($reference);
330
            }
331
        }
332
    }
333
 
991 lars 334
    /**
335
     * @throws ParserLogicException
336
     */
148 lars 337
    public function getActiveBlockParser(): BlockContinueParserInterface
338
    {
339
        $active = \end($this->activeBlockParsers);
340
        if ($active === false) {
991 lars 341
            throw new ParserLogicException('No active block parsers are available');
148 lars 342
        }
343
 
344
        return $active;
345
    }
346
}