Completed
Push — master ( 40e537...652fd3 )
by Colin
01:41 queued 41s
created

DocParser::parse()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 15
cts 15
cp 1
rs 9.504
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
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
use League\CommonMark\Event\DocumentPreParsedEvent;
24
use League\CommonMark\Input\MarkdownInput;
25
26
final class DocParser implements DocParserInterface
27
{
28
    /**
29
     * @var EnvironmentInterface
30
     */
31
    private $environment;
32
33
    /**
34
     * @var InlineParserEngine
35
     */
36
    private $inlineParserEngine;
37
38
    /**
39
     * @var int|float
40
     */
41
    private $maxNestingLevel;
42
43
    /**
44
     * @param EnvironmentInterface $environment
45
     */
46 2505
    public function __construct(EnvironmentInterface $environment)
47
    {
48 2505
        $this->environment = $environment;
49 2505
        $this->inlineParserEngine = new InlineParserEngine($environment);
50 2505
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \INF);
51 2505
    }
52
53
    /**
54
     * @param string $input
55
     *
56
     * @throws \RuntimeException
57
     *
58
     * @return Document
59
     */
60 2496
    public function parse(string $input): Document
61
    {
62 2496
        $document = new Document();
63
64 2496
        $preParsedEvent = new DocumentPreParsedEvent($document, new MarkdownInput($input));
65 2490
        $this->environment->dispatch($preParsedEvent);
66 2490
        $markdown = $preParsedEvent->getMarkdown();
67
68 2490
        $context = new Context($document, $this->environment);
69
70 2490
        foreach ($markdown->getLines() as $line) {
71 2490
            $context->setNextLine($line);
72 2490
            $this->incorporateLine($context);
73
        }
74
75 2490
        $lineCount = $markdown->getLineCount();
76 2490
        while ($tip = $context->getTip()) {
77 2490
            $tip->finalize($context, $lineCount);
78
        }
79
80 2490
        $this->processInlines($context);
81
82 2490
        $this->environment->dispatch(new DocumentParsedEvent($document));
83
84 2487
        return $document;
85
    }
86
87 2490
    private function incorporateLine(ContextInterface $context): void
88
    {
89 2490
        $context->getBlockCloser()->resetTip();
90 2490
        $context->setBlocksParsed(false);
91
92 2490
        $cursor = new Cursor($context->getLine());
93
94 2490
        $this->resetContainer($context, $cursor);
95 2490
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
96
97 2490
        $this->parseBlocks($context, $cursor);
98
99
        // What remains at the offset is a text line.  Add the text to the appropriate container.
100
        // First check for a lazy paragraph continuation:
101 2490
        if ($this->handleLazyParagraphContinuation($context, $cursor)) {
102 36
            return;
103
        }
104
105
        // not a lazy continuation
106
        // finalize any blocks not matched
107 2490
        $context->getBlockCloser()->closeUnmatchedBlocks();
108
109
        // Determine whether the last line is blank, updating parents as needed
110 2490
        $this->setAndPropagateLastLineBlank($context, $cursor);
111
112
        // Handle any remaining cursor contents
113 2490
        if ($context->getContainer() instanceof StringContainerInterface) {
114 918
            $context->getContainer()->handleRemainingContents($context, $cursor);
115 2214
        } elseif (!$cursor->isBlank()) {
116
            // Create paragraph container for line
117 2130
            $p = new Paragraph();
118 2130
            $context->addBlock($p);
119 2130
            $cursor->advanceToNextNonSpaceOrTab();
120 2130
            $p->addLine($cursor->getRemainder());
121
        }
122 2490
    }
123
124 2490
    private function processInlines(ContextInterface $context): void
125
    {
126 2490
        $walker = $context->getDocument()->walker();
127
128 2490
        while ($event = $walker->next()) {
129 2490
            if (!$event->isEntering()) {
130 2490
                continue;
131
            }
132
133 2490
            $node = $event->getNode();
134 2490
            if ($node instanceof AbstractStringContainerBlock) {
135 2448
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
136
            }
137
        }
138 2490
    }
139
140
    /**
141
     * Sets the container to the last open child (or its parent)
142
     *
143
     * @param ContextInterface $context
144
     * @param Cursor           $cursor
145
     */
146 2490
    private function resetContainer(ContextInterface $context, Cursor $cursor): void
147
    {
148 2490
        $container = $context->getDocument();
149
150 2490
        while ($lastChild = $container->lastChild()) {
151 1299
            if (!($lastChild instanceof AbstractBlock)) {
152
                break;
153
            }
154
155 1299
            if (!$lastChild->isOpen()) {
156 537
                break;
157
            }
158
159 1290
            $container = $lastChild;
160 1290
            if (!$container->matchesNextLine($cursor)) {
161 852
                $container = $container->parent(); // back up to the last matching block
162 852
                break;
163
            }
164
        }
165
166 2490
        $context->setContainer($container);
167 2490
    }
168
169
    /**
170
     * Parse blocks
171
     *
172
     * @param ContextInterface $context
173
     * @param Cursor           $cursor
174
     */
175 2490
    private function parseBlocks(ContextInterface $context, Cursor $cursor): void
176
    {
177 2490
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
178 2490
            $parsed = false;
179 2490
            foreach ($this->environment->getBlockParsers() as $parser) {
180 2490
                if ($parser->parse($context, $cursor)) {
181 972
                    $parsed = true;
182 972
                    break;
183
                }
184
            }
185
186 2490
            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
187 2463
                $context->setBlocksParsed(true);
188 2463
                break;
189
            }
190
        }
191 2490
    }
192
193 2490
    private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
194
    {
195 2490
        $tip = $context->getTip();
196
197 2490
        if ($tip instanceof Paragraph &&
198 2490
            !$context->getBlockCloser()->areAllClosed() &&
199 2490
            !$cursor->isBlank() &&
200 2490
            \count($tip->getStrings()) > 0) {
201
202
            // lazy paragraph continuation
203 36
            $tip->addLine($cursor->getRemainder());
204
205 36
            return true;
206
        }
207
208 2490
        return false;
209
    }
210
211 2490
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor): void
212
    {
213 2490
        $container = $context->getContainer();
214
215 2490
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
216 603
            if ($lastChild instanceof AbstractBlock) {
217 603
                $lastChild->setLastLineBlank(true);
218
            }
219
        }
220
221 2490
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
222
223
        // Propagate lastLineBlank up through parents:
224 2490
        while ($container instanceof AbstractBlock && $container->endsWithBlankLine() !== $lastLineBlank) {
225 753
            $container->setLastLineBlank($lastLineBlank);
226 753
            $container = $container->parent();
227
        }
228 2490
    }
229
}
230