Completed
Push — master ( 9026dd...ccfcc7 )
by Colin
15s
created

src/DocParser.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark;
16
17
use League\CommonMark\Block\Element\AbstractBlock;
18
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
19
use League\CommonMark\Block\Element\Document;
20
use League\CommonMark\Block\Element\Paragraph;
21
use League\CommonMark\Block\Element\StringContainerInterface;
22
23
final class DocParser implements DocParserInterface
24
{
25
    /**
26
     * @var EnvironmentInterface
27
     */
28
    private $environment;
29
30
    /**
31
     * @var InlineParserEngine
32
     */
33
    private $inlineParserEngine;
34
35
    /**
36
     * @var int|float
37
     */
38
    private $maxNestingLevel;
39
40
    /**
41
     * @param EnvironmentInterface $environment
42
     */
43 2079
    public function __construct(EnvironmentInterface $environment)
44
    {
45 2079
        $this->environment = $environment;
46 2079
        $this->inlineParserEngine = new InlineParserEngine($environment);
47 2079
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', INF);
48 2079
    }
49
50
    /**
51
     * @return EnvironmentInterface
52
     *
53
     * @deprecated
54
     */
55 3
    public function getEnvironment(): EnvironmentInterface
56
    {
57 3
        @trigger_error('DocParser::getEnvironment() has been deprecated and will be removed in a future release.', E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
58
59 3
        return $this->environment;
60
    }
61
62
    /**
63
     * @param string $input
64
     *
65
     * @return string[]
66
     */
67 2067
    private function preProcessInput(string $input): array
68
    {
69 2067
        $lines = \preg_split('/\r\n|\n|\r/', $input);
70
71
        // Remove any newline which appears at the very end of the string.
72
        // We've already split the document by newlines, so we can simply drop
73
        // any empty element which appears on the end.
74 2067
        if (\end($lines) === '') {
75 2013
            \array_pop($lines);
76
        }
77
78 2067
        return $lines;
79
    }
80
81
    /**
82
     * @param string $input
83
     *
84
     * @return Document
85
     */
86 2067
    public function parse(string $input): Document
87
    {
88 2067
        $context = new Context(new Document(), $this->environment);
89
90 2067
        $lines = $this->preProcessInput($input);
91 2067
        foreach ($lines as $line) {
92 2067
            $context->setNextLine($line);
93 2067
            $this->incorporateLine($context);
94
        }
95
96 2067
        $lineCount = \count($lines);
97 2067
        while ($tip = $context->getTip()) {
98 2067
            $tip->finalize($context, $lineCount);
99
        }
100
101 2067
        $this->processInlines($context);
102
103 2067
        $this->processDocument($context);
104
105 2067
        return $context->getDocument();
106
    }
107
108 2067
    private function incorporateLine(ContextInterface $context)
109
    {
110 2067
        $context->getBlockCloser()->resetTip();
111 2067
        $context->setBlocksParsed(false);
112
113 2067
        $cursor = new Cursor($context->getLine());
114
115 2067
        $this->resetContainer($context, $cursor);
116 2067
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
117
118 2067
        $this->parseBlocks($context, $cursor);
119
120
        // What remains at the offset is a text line.  Add the text to the appropriate container.
121
        // First check for a lazy paragraph continuation:
122 2067
        if ($this->handleLazyParagraphContinuation($context, $cursor)) {
123 36
            return;
124
        }
125
126
        // not a lazy continuation
127
        // finalize any blocks not matched
128 2067
        $context->getBlockCloser()->closeUnmatchedBlocks();
129
130
        // Determine whether the last line is blank, updating parents as needed
131 2067
        $this->setAndPropagateLastLineBlank($context, $cursor);
132
133
        // Handle any remaining cursor contents
134 2067
        if ($context->getContainer() instanceof StringContainerInterface) {
135 759
            $context->getContainer()->handleRemainingContents($context, $cursor);
136 1818
        } elseif (!$cursor->isBlank()) {
137
            // Create paragraph container for line
138 1737
            $p = new Paragraph();
139 1737
            $context->addBlock($p);
140 1737
            $cursor->advanceToNextNonSpaceOrTab();
141 1737
            $p->addLine($cursor->getRemainder());
142
        }
143 2067
    }
144
145 2067
    private function processDocument(ContextInterface $context)
146
    {
147 2067
        foreach ($this->environment->getDocumentProcessors() as $documentProcessor) {
148
            $documentProcessor->processDocument($context->getDocument());
149
        }
150 2067
    }
151
152 2067
    private function processInlines(ContextInterface $context)
153
    {
154 2067
        $walker = $context->getDocument()->walker();
155
156 2067
        while ($event = $walker->next()) {
157 2067
            if (!$event->isEntering()) {
158 2067
                continue;
159
            }
160
161 2067
            $node = $event->getNode();
162 2067
            if ($node instanceof AbstractStringContainerBlock) {
163 2025
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
164
            }
165
        }
166 2067
    }
167
168
    /**
169
     * Sets the container to the last open child (or its parent)
170
     *
171
     * @param ContextInterface $context
172
     * @param Cursor           $cursor
173
     */
174 2067
    private function resetContainer(ContextInterface $context, Cursor $cursor)
175
    {
176 2067
        $container = $context->getDocument();
177
178 2067
        while ($lastChild = $container->lastChild()) {
179 1125
            if (!($lastChild instanceof AbstractBlock)) {
180
                break;
181
            }
182
183 1125
            if (!$lastChild->isOpen()) {
184 459
                break;
185
            }
186
187 1116
            $container = $lastChild;
188 1116
            if (!$container->matchesNextLine($cursor)) {
189 714
                $container = $container->parent(); // back up to the last matching block
190 714
                break;
191
            }
192
        }
193
194 2067
        $context->setContainer($container);
195 2067
    }
196
197
    /**
198
     * Parse blocks
199
     *
200
     * @param ContextInterface $context
201
     * @param Cursor           $cursor
202
     */
203 2067
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
204
    {
205 2067
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
206 2067
            $parsed = false;
207 2067
            foreach ($this->environment->getBlockParsers() as $parser) {
208 2067
                if ($parser->parse($context, $cursor)) {
209 825
                    $parsed = true;
210 1239
                    break;
211
                }
212
            }
213
214 2067
            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
215 2040
                $context->setBlocksParsed(true);
216 2040
                break;
217
            }
218
        }
219 2067
    }
220
221
    /**
222
     * @param ContextInterface $context
223
     * @param Cursor           $cursor
224
     *
225
     * @return bool
226
     */
227 2067
    private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
228
    {
229 2067
        $tip = $context->getTip();
230
231 2067
        if ($tip instanceof Paragraph &&
232 2067
            !$context->getBlockCloser()->areAllClosed() &&
233 2067
            !$cursor->isBlank() &&
234 2067
            \count($tip->getStrings()) > 0) {
235
236
            // lazy paragraph continuation
237 36
            $tip->addLine($cursor->getRemainder());
238
239 36
            return true;
240
        }
241
242 2067
        return false;
243
    }
244
245
    /**
246
     * @param ContextInterface $context
247
     * @param Cursor           $cursor
248
     */
249 2067
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
250
    {
251 2067
        $container = $context->getContainer();
252
253 2067
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
254 474
            if ($lastChild instanceof AbstractBlock) {
255 474
                $lastChild->setLastLineBlank(true);
256
            }
257
        }
258
259 2067
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
260
261
        // Propagate lastLineBlank up through parents:
262 2067
        while ($container instanceof AbstractBlock) {
263 2067
            $container->setLastLineBlank($lastLineBlank);
264 2067
            $container = $container->parent();
265
        }
266 2067
    }
267
}
268