Completed
Push — master ( 15e8d5...9ebb39 )
by Colin
04:59
created

DocParser::processDocument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 4
cts 5
cp 0.8
rs 9.4286
cc 2
eloc 3
nc 2
nop 1
crap 2.032
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 (http://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\InlineContainer;
20
use League\CommonMark\Block\Element\ListBlock;
21
use League\CommonMark\Block\Element\Paragraph;
22
use League\CommonMark\Node\NodeWalker;
23
24
class DocParser
25
{
26
    /**
27
     * @var Environment
28
     */
29
    protected $environment;
30
31
    /**
32
     * @var InlineParserEngine
33
     */
34
    private $inlineParserEngine;
35
36
    /**
37
     * @param Environment $environment
38
     */
39 1872
    public function __construct(Environment $environment)
40
    {
41 1872
        $this->environment = $environment;
42 1872
        $this->inlineParserEngine = new InlineParserEngine($environment);
43 1872
    }
44
45
    /**
46
     * @return Environment
47
     */
48 1872
    public function getEnvironment()
49
    {
50 1872
        return $this->environment;
51
    }
52
53
    /**
54
     * @param string $input
55
     *
56
     * @return string[]
57
     */
58 1872
    private function preProcessInput($input)
59
    {
60 1872
        $lines = preg_split('/\r\n|\n|\r/', $input);
61
62
        // Remove any newline which appears at the very end of the string.
63
        // We've already split the document by newlines, so we can simply drop
64
        // any empty element which appears on the end.
65 1872
        if (end($lines) === '') {
66 1866
            array_pop($lines);
67 1866
        }
68
69 1872
        return $lines;
70
    }
71
72
    /**
73
     * @param string $input
74
     *
75
     * @return Document
76
     */
77 1872
    public function parse($input)
78 3
    {
79 1872
        $context = new Context(new Document(), $this->getEnvironment());
80
81 1872
        $lines = $this->preProcessInput($input);
82 1872
        foreach ($lines as $line) {
83 1872
            $context->setNextLine($line);
84 1872
            $this->incorporateLine($context);
85 1872
        }
86
87 1872
        while ($context->getTip()) {
88 1872
            $context->getTip()->finalize($context, count($lines));
89 1872
        }
90
91 1872
        $this->processInlines($context, $context->getDocument()->walker());
92
93 1872
        $this->processDocument($context);
94
95 1872
        return $context->getDocument();
96
    }
97
98 1872
    private function incorporateLine(ContextInterface $context)
99
    {
100 1872
        $cursor = new Cursor($context->getLine());
101 1872
        $context->getBlockCloser()->resetTip();
102
103 1872
        $context->setBlocksParsed(false);
104
105 1872
        $this->resetContainer($context, $cursor);
106 1872
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
107
108
        // Check to see if we've hit 2nd blank line; if so break out of list:
109 1872
        if ($cursor->isBlank() && $context->getContainer()->endsWithBlankLine()) {
110 39
            $this->breakOutOfLists($context, $context->getContainer());
111 39
        }
112
113 1872
        $this->parseBlocks($context, $cursor);
114
115
        // What remains at the offset is a text line.  Add the text to the appropriate container.
116
        // First check for a lazy paragraph continuation:
117 1872
        if ($this->isLazyParagraphContinuation($context, $cursor)) {
118
            // lazy paragraph continuation
119 30
            $context->getTip()->addLine($cursor->getRemainder());
120
121 30
            return;
122
        }
123
124
        // not a lazy continuation
125
        // finalize any blocks not matched
126 1872
        $context->getBlockCloser()->closeUnmatchedBlocks();
127
128
        // Determine whether the last line is blank, updating parents as needed
129 1872
        $this->setAndPropagateLastLineBlank($context, $cursor);
130
131
        // Handle any remaining cursor contents
132 1872
        if ($context->getContainer()->acceptsLines()) {
133 681
            $context->getContainer()->handleRemainingContents($context, $cursor);
134 1872
        } elseif (!$cursor->isBlank()) {
135
            // Create paragraph container for line
136 1560
            $context->addBlock(new Paragraph());
137 1560
            $cursor->advanceToFirstNonSpace();
138 1560
            $context->getTip()->addLine($cursor->getRemainder());
139 1560
        }
140 1872
    }
141
142 1872
    private function processDocument(ContextInterface $context)
143
    {
144 1872
        foreach ($this->getEnvironment()->getDocumentProcessors() as $documentProcessor) {
145
            $documentProcessor->processDocument($context->getDocument());
146 1872
        }
147 1872
    }
148
149 1872
    private function processInlines(ContextInterface $context, NodeWalker $walker)
150
    {
151 1872
        while (($event = $walker->next()) !== null) {
152 1872
            if (!$event->isEntering()) {
153 1872
                continue;
154
            }
155
156 1872
            $node = $event->getNode();
157 1872
            if ($node instanceof InlineContainer) {
158 1602
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
159 1602
            }
160 1872
        }
161 1872
    }
162
163
    /**
164
     * Break out of all containing lists, resetting the tip of the
165
     * document to the parent of the highest list, and finalizing
166
     * all the lists.  (This is used to implement the "two blank lines
167
     * break out of all lists" feature.)
168
     *
169
     * @param ContextInterface $context
170
     * @param AbstractBlock    $block
171
     */
172 39
    private function breakOutOfLists(ContextInterface $context, AbstractBlock $block)
173
    {
174 39
        $b = $block;
175 39
        $lastList = null;
176
        do {
177 39
            if ($b instanceof ListBlock) {
178 24
                $lastList = $b;
179 24
            }
180 39
            $b = $b->parent();
181 39
        } while ($b);
182
183 39
        if ($lastList) {
184 24
            while ($block !== $lastList) {
185 24
                $block->finalize($context, $context->getLineNumber());
186 24
                $block = $block->parent();
187 24
            }
188 24
            $lastList->finalize($context, $context->getLineNumber());
189 24
        }
190
191 39
        $context->setContainer($context->getTip());
192 39
    }
193
194
    /**
195
     * Sets the container to the last open child (or its parent)
196
     *
197
     * @param ContextInterface $context
198
     * @param Cursor           $cursor
199
     */
200 1872
    private function resetContainer(ContextInterface $context, Cursor $cursor)
201
    {
202 1872
        $context->setContainer($context->getDocument());
203
204 1872
        while ($context->getContainer()->hasChildren()) {
205 1026
            $lastChild = $context->getContainer()->lastChild();
206 1026
            if (!$lastChild->isOpen()) {
207 399
                break;
208
            }
209
210 1017
            $context->setContainer($lastChild);
211 1017
            if (!$context->getContainer()->matchesNextLine($cursor)) {
212 654
                $context->setContainer($context->getContainer()->parent()); // back up to the last matching block
213 654
                break;
214
            }
215 714
        }
216 1872
    }
217
218
    /**
219
     * Parse blocks
220
     *
221
     * @param ContextInterface $context
222
     * @param Cursor           $cursor
223
     */
224 1872
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
225
    {
226 1872
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
227 1872
            $parsed = false;
228 1872
            foreach ($this->environment->getBlockParsers() as $parser) {
229 1872
                if ($parser->parse($context, $cursor)) {
230 747
                    $parsed = true;
231 747
                    break;
232
                }
233 1872
            }
234
235 1872
            if (!$parsed || $context->getContainer()->acceptsLines()) {
236 1848
                $context->setBlocksParsed(true);
237 1848
            }
238 1872
        }
239 1872
    }
240
241
    /**
242
     * @param ContextInterface $context
243
     * @param Cursor           $cursor
244
     *
245
     * @return bool
246
     */
247 1872
    private function isLazyParagraphContinuation(ContextInterface $context, Cursor $cursor)
248
    {
249 1872
        return !$context->getBlockCloser()->areAllClosed() &&
250 1872
            !$cursor->isBlank() &&
251 1872
            $context->getTip() instanceof Paragraph &&
252 1872
            count($context->getTip()->getStrings()) > 0;
253
    }
254
255
    /**
256
     * @param ContextInterface $context
257
     * @param Cursor           $cursor
258
     */
259 1872
    private function setAndPropagateLastLineBlank(ContextInterface $context, $cursor)
260
    {
261 1872
        if ($cursor->isBlank() && $lastChild = $context->getContainer()->lastChild()) {
262 426
            if ($lastChild instanceof AbstractBlock) {
263 426
                $lastChild->setLastLineBlank(true);
264 426
            }
265 426
        }
266
267 1872
        $container = $context->getContainer();
268 1872
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
269
270
        // Propagate lastLineBlank up through parents:
271 1872
        while ($container) {
272 1872
            $container->setLastLineBlank($lastLineBlank);
273 1872
            $container = $container->parent();
274 1872
        }
275 1872
    }
276
}
277