Completed
Push — feature/resolve-parent-class ( 1a8a55...248da8 )
by Colin
35:17 queued 33:54
created

DocParser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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
use League\CommonMark\Event\DocumentParsedEvent;
23
use League\CommonMark\Event\DocumentPreParsedEvent;
24
use League\CommonMark\Exception\UnexpectedEncodingException;
25
26
final class DocParser implements DocParserInterface
27
{
28
    /**
29
     * @var EnvironmentInterface
30
     */
31
    private $environment;
32
33
    /**
34
     * @var InlineParserEngine
35
     */
36
    private $inlineParserEngine;
37
38
    /**
39
     * @var int|float
40
     */
41
    private $maxNestingLevel;
42
43
    /**
44
     * @param EnvironmentInterface $environment
45
     */
46 2472
    public function __construct(EnvironmentInterface $environment)
47
    {
48 2472
        $this->environment = $environment;
49 2472
        $this->inlineParserEngine = new InlineParserEngine($environment);
50 2472
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \INF);
51 2472
    }
52
53
    /**
54
     * @param string $input
55
     *
56
     * @return string[]
57
     */
58 2457
    private function preProcessInput(string $input): array
59
    {
60
        /** @var string[] $lines */
61 2457
        $lines = \preg_split('/\r\n|\n|\r/', $input);
62
63
        // Remove any newline which appears at the very end of the string.
64
        // We've already split the document by newlines, so we can simply drop
65
        // any empty element which appears on the end.
66 2457
        if (\end($lines) === '') {
67 2187
            \array_pop($lines);
68
        }
69
70 2457
        return $lines;
71
    }
72
73
    /**
74
     * @param string $input
75
     *
76
     * @throws \RuntimeException
77
     *
78
     * @return Document
79
     */
80 2463
    public function parse(string $input): Document
81
    {
82 2463
        $document = new Document();
83
84 2463
        $this->environment->dispatch(new DocumentPreParsedEvent($document));
85
86 2463
        $context = new Context($document, $this->environment);
87
88 2463
        $this->assertValidUTF8($input);
89 2457
        $lines = $this->preProcessInput($input);
90 2457
        foreach ($lines as $line) {
91 2457
            $context->setNextLine($line);
92 2457
            $this->incorporateLine($context);
93
        }
94
95 2457
        $lineCount = \count($lines);
96 2457
        while ($tip = $context->getTip()) {
97 2457
            $tip->finalize($context, $lineCount);
98
        }
99
100 2457
        $this->processInlines($context);
101
102 2457
        $this->environment->dispatch(new DocumentParsedEvent($document));
103
104 2454
        return $document;
105
    }
106
107 2457
    private function incorporateLine(ContextInterface $context)
108
    {
109 2457
        $context->getBlockCloser()->resetTip();
110 2457
        $context->setBlocksParsed(false);
111
112 2457
        $cursor = new Cursor($context->getLine());
113
114 2457
        $this->resetContainer($context, $cursor);
115 2457
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
116
117 2457
        $this->parseBlocks($context, $cursor);
118
119
        // What remains at the offset is a text line.  Add the text to the appropriate container.
120
        // First check for a lazy paragraph continuation:
121 2457
        if ($this->handleLazyParagraphContinuation($context, $cursor)) {
122 36
            return;
123
        }
124
125
        // not a lazy continuation
126
        // finalize any blocks not matched
127 2457
        $context->getBlockCloser()->closeUnmatchedBlocks();
128
129
        // Determine whether the last line is blank, updating parents as needed
130 2457
        $this->setAndPropagateLastLineBlank($context, $cursor);
131
132
        // Handle any remaining cursor contents
133 2457
        if ($context->getContainer() instanceof StringContainerInterface) {
134 888
            $context->getContainer()->handleRemainingContents($context, $cursor);
135 2187
        } elseif (!$cursor->isBlank()) {
136
            // Create paragraph container for line
137 2106
            $p = new Paragraph();
138 2106
            $context->addBlock($p);
139 2106
            $cursor->advanceToNextNonSpaceOrTab();
140 2106
            $p->addLine($cursor->getRemainder());
141
        }
142 2457
    }
143
144 2457
    private function processInlines(ContextInterface $context)
145
    {
146 2457
        $walker = $context->getDocument()->walker();
147
148 2457
        while ($event = $walker->next()) {
149 2457
            if (!$event->isEntering()) {
150 2457
                continue;
151
            }
152
153 2457
            $node = $event->getNode();
154 2457
            if ($node instanceof AbstractStringContainerBlock) {
155 2415
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
156
            }
157
        }
158 2457
    }
159
160
    /**
161
     * Sets the container to the last open child (or its parent)
162
     *
163
     * @param ContextInterface $context
164
     * @param Cursor           $cursor
165
     */
166 2457
    private function resetContainer(ContextInterface $context, Cursor $cursor)
167
    {
168 2457
        $container = $context->getDocument();
169
170 2457
        while ($lastChild = $container->lastChild()) {
171 1269
            if (!($lastChild instanceof AbstractBlock)) {
172
                break;
173
            }
174
175 1269
            if (!$lastChild->isOpen()) {
176 513
                break;
177
            }
178
179 1260
            $container = $lastChild;
180 1260
            if (!$container->matchesNextLine($cursor)) {
181 822
                $container = $container->parent(); // back up to the last matching block
182 822
                break;
183
            }
184
        }
185
186 2457
        $context->setContainer($container);
187 2457
    }
188
189
    /**
190
     * Parse blocks
191
     *
192
     * @param ContextInterface $context
193
     * @param Cursor           $cursor
194
     */
195 2457
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
196
    {
197 2457
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
198 2457
            $parsed = false;
199 2457
            foreach ($this->environment->getBlockParsers() as $parser) {
200 2457
                if ($parser->parse($context, $cursor)) {
201 942
                    $parsed = true;
202 1447
                    break;
203
                }
204
            }
205
206 2457
            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
207 2430
                $context->setBlocksParsed(true);
208 2430
                break;
209
            }
210
        }
211 2457
    }
212
213
    /**
214
     * @param ContextInterface $context
215
     * @param Cursor           $cursor
216
     *
217
     * @return bool
218
     */
219 2457
    private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
220
    {
221 2457
        $tip = $context->getTip();
222
223 2457
        if ($tip instanceof Paragraph &&
224 2457
            !$context->getBlockCloser()->areAllClosed() &&
225 2457
            !$cursor->isBlank() &&
226 2457
            \count($tip->getStrings()) > 0) {
227
228
            // lazy paragraph continuation
229 36
            $tip->addLine($cursor->getRemainder());
230
231 36
            return true;
232
        }
233
234 2457
        return false;
235
    }
236
237
    /**
238
     * @param ContextInterface $context
239
     * @param Cursor           $cursor
240
     */
241 2457
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
242
    {
243 2457
        $container = $context->getContainer();
244
245 2457
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
246 579
            if ($lastChild instanceof AbstractBlock) {
247 579
                $lastChild->setLastLineBlank(true);
248
            }
249
        }
250
251 2457
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
252
253
        // Propagate lastLineBlank up through parents:
254 2457
        while ($container instanceof AbstractBlock && $container->endsWithBlankLine() !== $lastLineBlank) {
255 729
            $container->setLastLineBlank($lastLineBlank);
256 729
            $container = $container->parent();
257
        }
258 2457
    }
259
260 2463
    private function assertValidUTF8(string $input)
261
    {
262 2463
        if (!\mb_check_encoding($input, 'UTF-8')) {
263 6
            throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected');
264
        }
265 2457
    }
266
}
267