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