Completed
Push — master ( d3fffa...285358 )
by Colin
33:06 queued 55s
created

DocParser::processDocument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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