Completed
Push — master ( 4c1ed7...f9702c )
by Colin
24s queued 11s
created

DocParser::resetContainer()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0113

Importance

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