Completed
Push — master ( d2636b...e88807 )
by Colin
02:51
created

DocParser::incorporateLine()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 38
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 38
ccs 20
cts 20
cp 1
rs 8.5806
cc 4
eloc 18
nc 4
nop 1
crap 4
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
use League\CommonMark\Node\NodeWalker;
22
23
class DocParser
24
{
25
    /**
26
     * @var Environment
27
     */
28
    protected $environment;
29
30
    /**
31
     * @var InlineParserEngine
32
     */
33
    private $inlineParserEngine;
34
35
    /**
36
     * @param Environment $environment
37
     */
38 1944
    public function __construct(Environment $environment)
39
    {
40 1944
        $this->environment = $environment;
41 1944
        $this->inlineParserEngine = new InlineParserEngine($environment);
42 1944
    }
43
44
    /**
45
     * @return Environment
46
     */
47 1944
    public function getEnvironment()
48
    {
49 1944
        return $this->environment;
50 2
    }
51
52
    /**
53
     * @param string $input
54
     *
55
     * @return string[]
56
     */
57 1935
    private function preProcessInput($input)
58
    {
59 1935
        $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 1935
        if (end($lines) === '') {
65 1926
            array_pop($lines);
66 1284
        }
67
68 1935
        return $lines;
69
    }
70
71
    /**
72
     * @param string $input
73
     *
74
     * @return Document
75
     */
76 1935
    public function parse($input)
77 2
    {
78 1935
        $context = new Context(new Document(), $this->getEnvironment());
79
80 1935
        $lines = $this->preProcessInput($input);
81 1935
        foreach ($lines as $line) {
82 1932
            $context->setNextLine($line);
83 1932
            $this->incorporateLine($context);
84 1290
        }
85
86 1935
        while ($context->getTip()) {
87 1935
            $context->getTip()->finalize($context, count($lines));
88 1290
        }
89
90 1935
        $this->processInlines($context, $context->getDocument()->walker());
91
92 1935
        $this->processDocument($context);
93
94 1935
        return $context->getDocument();
95
    }
96
97 1932
    private function incorporateLine(ContextInterface $context)
98
    {
99 1932
        $cursor = new Cursor($context->getLine());
100 1932
        $context->getBlockCloser()->resetTip();
101
102 1932
        $context->setBlocksParsed(false);
103
104 1932
        $this->resetContainer($context, $cursor);
105 1932
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
106
107 1932
        $this->parseBlocks($context, $cursor);
108
109
        // What remains at the offset is a text line.  Add the text to the appropriate container.
110
        // First check for a lazy paragraph continuation:
111 1932
        if ($this->isLazyParagraphContinuation($context, $cursor)) {
112
            // lazy paragraph continuation
113 33
            $context->getTip()->addLine($cursor->getRemainder());
114
115 33
            return;
116
        }
117
118
        // not a lazy continuation
119
        // finalize any blocks not matched
120 1932
        $context->getBlockCloser()->closeUnmatchedBlocks();
121
122
        // Determine whether the last line is blank, updating parents as needed
123 1932
        $this->setAndPropagateLastLineBlank($context, $cursor);
124
125
        // Handle any remaining cursor contents
126 1932
        if ($context->getContainer()->acceptsLines()) {
127 726
            $context->getContainer()->handleRemainingContents($context, $cursor);
128 1850
        } elseif (!$cursor->isBlank()) {
129
            // Create paragraph container for line
130 1608
            $context->addBlock(new Paragraph());
131 1608
            $cursor->advanceToNextNonSpaceOrTab();
132 1608
            $context->getTip()->addLine($cursor->getRemainder());
133 1072
        }
134 1932
    }
135
136 1935
    private function processDocument(ContextInterface $context)
137
    {
138 1935
        foreach ($this->getEnvironment()->getDocumentProcessors() as $documentProcessor) {
139
            $documentProcessor->processDocument($context->getDocument());
140 1290
        }
141 1935
    }
142
143 1935
    private function processInlines(ContextInterface $context, NodeWalker $walker)
144
    {
145 1935
        while (($event = $walker->next()) !== null) {
146 1935
            if (!$event->isEntering()) {
147 1935
                continue;
148
            }
149
150 1935
            $node = $event->getNode();
151 1935
            if ($node instanceof InlineContainerInterface) {
152 1656
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
153 1104
            }
154 1290
        }
155 1935
    }
156
157
    /**
158
     * Sets the container to the last open child (or its parent)
159
     *
160
     * @param ContextInterface $context
161
     * @param Cursor           $cursor
162
     */
163 1932
    private function resetContainer(ContextInterface $context, Cursor $cursor)
164
    {
165 1932
        $container = $context->getDocument();
166
167 1932
        while ($container->hasChildren()) {
168 1080
            $lastChild = $container->lastChild();
169 1080
            if (!$lastChild->isOpen()) {
170 450
                break;
171
            }
172
173 1071
            $container = $lastChild;
174 1071
            if (!$container->matchesNextLine($cursor)) {
175 693
                $container = $container->parent(); // back up to the last matching block
176 693
                break;
177
            }
178 500
        }
179
180 1932
        $context->setContainer($container);
181 1932
    }
182
183
    /**
184
     * Parse blocks
185
     *
186
     * @param ContextInterface $context
187
     * @param Cursor           $cursor
188
     */
189 1932
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
190
    {
191 1932
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
192 1932
            $parsed = false;
193 1932
            foreach ($this->environment->getBlockParsers() as $parser) {
194 1932
                if ($parser->parse($context, $cursor)) {
195 798
                    $parsed = true;
196 1176
                    break;
197
                }
198 1288
            }
199
200 1932
            if (!$parsed || $context->getContainer()->acceptsLines()) {
201 1905
                $context->setBlocksParsed(true);
202 1905
                break;
203
            }
204 260
        }
205 1932
    }
206
207
    /**
208
     * @param ContextInterface $context
209
     * @param Cursor           $cursor
210
     *
211
     * @return bool
212
     */
213 1932
    private function isLazyParagraphContinuation(ContextInterface $context, Cursor $cursor)
214
    {
215 1932
        return !$context->getBlockCloser()->areAllClosed() &&
216 1932
            !$cursor->isBlank() &&
217 1932
            $context->getTip() instanceof Paragraph &&
218 1932
            count($context->getTip()->getStrings()) > 0;
219
    }
220
221
    /**
222
     * @param ContextInterface $context
223
     * @param Cursor           $cursor
224
     */
225 1932
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
226
    {
227 1932
        $container = $context->getContainer();
228
229 1932
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
230 462
            if ($lastChild instanceof AbstractBlock) {
231 462
                $lastChild->setLastLineBlank(true);
232 308
            }
233 308
        }
234
235 1932
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
236
237
        // Propagate lastLineBlank up through parents:
238 1932
        while ($container) {
239 1932
            $container->setLastLineBlank($lastLineBlank);
240 1932
            $container = $container->parent();
241 1288
        }
242 1932
    }
243
}
244