Completed
Push — master ( 585e6a...d168ab )
by Colin
04:06 queued 22s
created

DocParser::getEnvironment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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