Completed
Push — master ( d37c6d...936e33 )
by Colin
23s queued 10s
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 2040
    public function __construct(EnvironmentInterface $environment)
44
    {
45 2040
        $this->environment = $environment;
46 2040
        $this->inlineParserEngine = new InlineParserEngine($environment);
47 2040
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', INF);
48 2040
    }
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 2028
    private function preProcessInput(string $input): array
68
    {
69 2028
        $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 2028
        if (\end($lines) === '') {
75 2013
            \array_pop($lines);
76
        }
77
78 2028
        return $lines;
79
    }
80
81
    /**
82
     * @param string $input
83
     *
84
     * @return Document
85
     */
86 2028
    public function parse(string $input): Document
87
    {
88 2028
        $context = new Context(new Document(), $this->environment);
89 2028
        $context->setEncoding(\mb_detect_encoding($input, 'ASCII,UTF-8', true) ?: 'ISO-8859-1');
90
91 2028
        $lines = $this->preProcessInput($input);
92 2028
        foreach ($lines as $line) {
93 2028
            $context->setNextLine($line);
94 2028
            $this->incorporateLine($context);
95
        }
96
97 2028
        $lineCount = \count($lines);
98 2028
        while ($tip = $context->getTip()) {
99 2028
            $tip->finalize($context, $lineCount);
100
        }
101
102 2028
        $this->processInlines($context);
103
104 2028
        $this->processDocument($context);
105
106 2028
        return $context->getDocument();
107
    }
108
109 2028
    private function incorporateLine(ContextInterface $context)
110
    {
111 2028
        $context->getBlockCloser()->resetTip();
112 2028
        $context->setBlocksParsed(false);
113
114 2028
        $cursor = new Cursor($context->getLine(), $context->getEncoding());
115
116 2028
        $this->resetContainer($context, $cursor);
117 2028
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
118
119 2028
        $this->parseBlocks($context, $cursor);
120
121
        // What remains at the offset is a text line.  Add the text to the appropriate container.
122
        // First check for a lazy paragraph continuation:
123 2028
        if ($this->handleLazyParagraphContinuation($context, $cursor)) {
124 36
            return;
125
        }
126
127
        // not a lazy continuation
128
        // finalize any blocks not matched
129 2028
        $context->getBlockCloser()->closeUnmatchedBlocks();
130
131
        // Determine whether the last line is blank, updating parents as needed
132 2028
        $this->setAndPropagateLastLineBlank($context, $cursor);
133
134
        // Handle any remaining cursor contents
135 2028
        if ($context->getContainer() instanceof StringContainerInterface) {
136 759
            $context->getContainer()->handleRemainingContents($context, $cursor);
137 1779
        } elseif (!$cursor->isBlank()) {
138
            // Create paragraph container for line
139 1698
            $p = new Paragraph();
140 1698
            $context->addBlock($p);
141 1698
            $cursor->advanceToNextNonSpaceOrTab();
142 1698
            $p->addLine($cursor->getRemainder());
143
        }
144 2028
    }
145
146 2028
    private function processDocument(ContextInterface $context)
147
    {
148 2028
        foreach ($this->environment->getDocumentProcessors() as $documentProcessor) {
149
            $documentProcessor->processDocument($context->getDocument());
150
        }
151 2028
    }
152
153 2028
    private function processInlines(ContextInterface $context)
154
    {
155 2028
        $walker = $context->getDocument()->walker();
156
157 2028
        while ($event = $walker->next()) {
158 2028
            if (!$event->isEntering()) {
159 2028
                continue;
160
            }
161
162 2028
            $node = $event->getNode();
163 2028
            if ($node instanceof AbstractStringContainerBlock) {
164 1986
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
165
            }
166
        }
167 2028
    }
168
169
    /**
170
     * Sets the container to the last open child (or its parent)
171
     *
172
     * @param ContextInterface $context
173
     * @param Cursor           $cursor
174
     */
175 2028
    private function resetContainer(ContextInterface $context, Cursor $cursor)
176
    {
177 2028
        $container = $context->getDocument();
178
179 2028
        while ($lastChild = $container->lastChild()) {
180 1125
            if (!($lastChild instanceof AbstractBlock)) {
181
                break;
182
            }
183
184 1125
            if (!$lastChild->isOpen()) {
185 459
                break;
186
            }
187
188 1116
            $container = $lastChild;
189 1116
            if (!$container->matchesNextLine($cursor)) {
190 714
                $container = $container->parent(); // back up to the last matching block
191 714
                break;
192
            }
193
        }
194
195 2028
        $context->setContainer($container);
196 2028
    }
197
198
    /**
199
     * Parse blocks
200
     *
201
     * @param ContextInterface $context
202
     * @param Cursor           $cursor
203
     */
204 2028
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
205
    {
206 2028
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
207 2028
            $parsed = false;
208 2028
            foreach ($this->environment->getBlockParsers() as $parser) {
209 2028
                if ($parser->parse($context, $cursor)) {
210 825
                    $parsed = true;
211 1226
                    break;
212
                }
213
            }
214
215 2028
            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
216 2001
                $context->setBlocksParsed(true);
217 2001
                break;
218
            }
219
        }
220 2028
    }
221
222
    /**
223
     * @param ContextInterface $context
224
     * @param Cursor           $cursor
225
     *
226
     * @return bool
227
     */
228 2028
    private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
229
    {
230 2028
        $tip = $context->getTip();
231
232 2028
        if ($tip instanceof Paragraph &&
233 2028
            !$context->getBlockCloser()->areAllClosed() &&
234 2028
            !$cursor->isBlank() &&
235 2028
            \count($tip->getStrings()) > 0) {
236
237
            // lazy paragraph continuation
238 36
            $tip->addLine($cursor->getRemainder());
239
240 36
            return true;
241
        }
242
243 2028
        return false;
244
    }
245
246
    /**
247
     * @param ContextInterface $context
248
     * @param Cursor           $cursor
249
     */
250 2028
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
251
    {
252 2028
        $container = $context->getContainer();
253
254 2028
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
255 474
            if ($lastChild instanceof AbstractBlock) {
256 474
                $lastChild->setLastLineBlank(true);
257
            }
258
        }
259
260 2028
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
261
262
        // Propagate lastLineBlank up through parents:
263 2028
        while ($container instanceof AbstractBlock) {
264 2028
            $container->setLastLineBlank($lastLineBlank);
265 2028
            $container = $container->parent();
266
        }
267 2028
    }
268
}
269