Completed
Push — 0.18-dev ( 9f2bf2...c34984 )
by Colin
103:43 queued 100:20
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\Document;
19
use League\CommonMark\Block\Element\InlineContainerInterface;
20
use League\CommonMark\Block\Element\Paragraph;
21
22
class DocParser
23
{
24
    /**
25
     * @var Environment
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 Environment $environment
41
     */
42 1950
    public function __construct(Environment $environment)
43
    {
44 1950
        $this->environment = $environment;
45 1950
        $this->inlineParserEngine = new InlineParserEngine($environment);
46 1950
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', INF);
47 1950
    }
48
49
    /**
50
     * @return Environment
51
     */
52 1950
    public function getEnvironment()
53
    {
54 1950
        return $this->environment;
55
    }
56
57
    /**
58
     * @param string $input
59
     *
60
     * @return string[]
61
     */
62 1941
    private function preProcessInput($input)
63
    {
64 1941
        $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 1941
        if (end($lines) === '') {
70 1926
            array_pop($lines);
71
        }
72
73 1941
        return $lines;
74
    }
75
76
    /**
77
     * @param string $input
78
     *
79
     * @return Document
80
     */
81 1941
    public function parse($input)
82
    {
83 1941
        $context = new Context(new Document(), $this->getEnvironment());
84 1941
        $context->setEncoding(mb_detect_encoding($input, 'ASCII,UTF-8', true) ?: 'ISO-8859-1');
85
86 1941
        $lines = $this->preProcessInput($input);
87 1941
        foreach ($lines as $line) {
88 1938
            $context->setNextLine($line);
89 1938
            $this->incorporateLine($context);
90
        }
91
92 1941
        $lineCount = count($lines);
93 1941
        while ($tip = $context->getTip()) {
94 1941
            $tip->finalize($context, $lineCount);
95
        }
96
97 1941
        $this->processInlines($context);
98
99 1941
        $this->processDocument($context);
100
101 1941
        return $context->getDocument();
102
    }
103
104 1938
    private function incorporateLine(ContextInterface $context)
105
    {
106 1938
        $context->getBlockCloser()->resetTip();
107 1938
        $context->setBlocksParsed(false);
108
109 1938
        $cursor = new Cursor($context->getLine(), $context->getEncoding());
110
111 1938
        $this->resetContainer($context, $cursor);
112 1938
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
113
114 1938
        $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 1938
        if ($this->isLazyParagraphContinuation($context, $cursor)) {
119
            // lazy paragraph continuation
120 33
            $context->getTip()->addLine($cursor->getRemainder());
121
122 33
            return;
123
        }
124
125
        // not a lazy continuation
126
        // finalize any blocks not matched
127 1938
        $context->getBlockCloser()->closeUnmatchedBlocks();
128
129
        // Determine whether the last line is blank, updating parents as needed
130 1938
        $this->setAndPropagateLastLineBlank($context, $cursor);
131
132
        // Handle any remaining cursor contents
133 1938
        if ($context->getContainer()->acceptsLines()) {
134 726
            $context->getContainer()->handleRemainingContents($context, $cursor);
135 1692
        } elseif (!$cursor->isBlank()) {
136
            // Create paragraph container for line
137 1614
            $context->addBlock(new Paragraph());
138 1614
            $cursor->advanceToNextNonSpaceOrTab();
139 1614
            $context->getTip()->addLine($cursor->getRemainder());
140
        }
141 1938
    }
142
143 1941
    private function processDocument(ContextInterface $context)
144
    {
145 1941
        foreach ($this->getEnvironment()->getDocumentProcessors() as $documentProcessor) {
146
            $documentProcessor->processDocument($context->getDocument());
147
        }
148 1941
    }
149
150 1941
    private function processInlines(ContextInterface $context)
151
    {
152 1941
        $walker = $context->getDocument()->walker();
153
154 1941
        while ($event = $walker->next()) {
155 1941
            if (!$event->isEntering()) {
156 1941
                continue;
157
            }
158
159 1941
            $node = $event->getNode();
160 1941
            if ($node instanceof InlineContainerInterface) {
161 1662
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
162
            }
163
        }
164 1941
    }
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 1938
    private function resetContainer(ContextInterface $context, Cursor $cursor)
173
    {
174 1938
        $container = $context->getDocument();
175
176 1938
        while ($lastChild = $container->lastChild()) {
177 1080
            if (!($lastChild instanceof AbstractBlock)) {
178
                break;
179
            }
180
181 1080
            if (!$lastChild->isOpen()) {
182 450
                break;
183
            }
184
185 1071
            $container = $lastChild;
186 1071
            if (!$container->matchesNextLine($cursor)) {
187 693
                $container = $container->parent(); // back up to the last matching block
188 693
                break;
189
            }
190
        }
191
192 1938
        $context->setContainer($container);
193 1938
    }
194
195
    /**
196
     * Parse blocks
197
     *
198
     * @param ContextInterface $context
199
     * @param Cursor           $cursor
200
     */
201 1938
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
202
    {
203 1938
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
204 1938
            $parsed = false;
205 1938
            foreach ($this->environment->getBlockParsers() as $parser) {
206 1938
                if ($parser->parse($context, $cursor)) {
207 804
                    $parsed = true;
208 1560
                    break;
209
                }
210
            }
211
212 1938
            if (!$parsed || $context->getContainer()->acceptsLines() || $context->getTip()->getDepth() >= $this->maxNestingLevel) {
213 1911
                $context->setBlocksParsed(true);
214 1911
                break;
215
            }
216
        }
217 1938
    }
218
219
    /**
220
     * @param ContextInterface $context
221
     * @param Cursor           $cursor
222
     *
223
     * @return bool
224
     */
225 1938
    private function isLazyParagraphContinuation(ContextInterface $context, Cursor $cursor)
226
    {
227 1938
        return $context->getTip() instanceof Paragraph &&
228 1938
            !$context->getBlockCloser()->areAllClosed() &&
229 1938
            !$cursor->isBlank() &&
230 1938
            count($context->getTip()->getStrings()) > 0;
231
    }
232
233
    /**
234
     * @param ContextInterface $context
235
     * @param Cursor           $cursor
236
     */
237 1938
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
238
    {
239 1938
        $container = $context->getContainer();
240
241 1938
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
242 462
            if ($lastChild instanceof AbstractBlock) {
243 462
                $lastChild->setLastLineBlank(true);
244
            }
245
        }
246
247 1938
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
248
249
        // Propagate lastLineBlank up through parents:
250 1938
        while ($container instanceof AbstractBlock) {
251 1938
            $container->setLastLineBlank($lastLineBlank);
252 1938
            $container = $container->parent();
253
        }
254 1938
    }
255
}
256