Completed
Push — 1.6 ( bb055d )
by Colin
01:28
created

DocParser::handleLazyParagraphContinuation()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.3888
c 0
b 0
f 0
nc 2
cc 5
nop 2
crap 5
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 2937
    public function __construct(EnvironmentInterface $environment)
47
    {
48 2937
        $this->environment = $environment;
49 2937
        $this->inlineParserEngine = new InlineParserEngine($environment);
50 2937
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \PHP_INT_MAX);
51
52 2937
        if (\is_float($this->maxNestingLevel)) {
53
            if ($this->maxNestingLevel === \INF) {
54
                @\trigger_error('Using the "INF" constant for the "max_nesting_level" configuration option is deprecated in league/commonmark 1.6 and will not be allowed in 2.0; use "PHP_INT_MAX" instead', \E_USER_DEPRECATED);
55
            } else {
56
                @\trigger_error('Using a float for the "max_nesting_level" configuration option is deprecated in league/commonmark 1.6 and will not be allowed in 2.0', \E_USER_DEPRECATED);
57
            }
58
        }
59 2937
    }
60
61
    /**
62
     * @param string $input
63
     *
64
     * @throws \RuntimeException
65
     *
66
     * @return Document
67
     */
68 2916
    public function parse(string $input): Document
69
    {
70 2916
        $document = new Document();
71
72 2916
        $preParsedEvent = new DocumentPreParsedEvent($document, new MarkdownInput($input));
73 2907
        $this->environment->dispatch($preParsedEvent);
74 2901
        $markdown = $preParsedEvent->getMarkdown();
75
76 2901
        $context = new Context($document, $this->environment);
77
78 2901
        foreach ($markdown->getLines() as $line) {
79 2901
            $context->setNextLine($line);
80 2901
            $this->incorporateLine($context);
81
        }
82
83 2901
        $lineCount = $markdown->getLineCount();
84 2901
        while ($tip = $context->getTip()) {
85 2901
            $tip->finalize($context, $lineCount);
86
        }
87
88 2901
        $this->processInlines($context);
89
90 2901
        $this->environment->dispatch(new DocumentParsedEvent($document));
91
92 2898
        return $document;
93
    }
94
95 2901
    private function incorporateLine(ContextInterface $context): void
96
    {
97 2901
        $context->getBlockCloser()->resetTip();
98 2901
        $context->setBlocksParsed(false);
99
100 2901
        $cursor = new Cursor($context->getLine());
101
102 2901
        $this->resetContainer($context, $cursor);
103 2901
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
104
105 2901
        $this->parseBlocks($context, $cursor);
106
107
        // What remains at the offset is a text line.  Add the text to the appropriate container.
108
        // First check for a lazy paragraph continuation:
109 2901
        if ($this->handleLazyParagraphContinuation($context, $cursor)) {
110 48
            return;
111
        }
112
113
        // not a lazy continuation
114
        // finalize any blocks not matched
115 2901
        $context->getBlockCloser()->closeUnmatchedBlocks();
116
117
        // Determine whether the last line is blank, updating parents as needed
118 2901
        $this->setAndPropagateLastLineBlank($context, $cursor);
119
120
        // Handle any remaining cursor contents
121 2901
        if ($context->getContainer() instanceof StringContainerInterface) {
122 969
            $context->getContainer()->handleRemainingContents($context, $cursor);
123 2613
        } elseif (!$cursor->isBlank()) {
124
            // Create paragraph container for line
125 2523
            $p = new Paragraph();
126 2523
            $context->addBlock($p);
127 2523
            $cursor->advanceToNextNonSpaceOrTab();
128 2523
            $p->addLine($cursor->getRemainder());
129
        }
130 2901
    }
131
132 2901
    private function processInlines(ContextInterface $context): void
133
    {
134 2901
        $walker = $context->getDocument()->walker();
135
136 2901
        while ($event = $walker->next()) {
137 2901
            if (!$event->isEntering()) {
138 2901
                continue;
139
            }
140
141 2901
            $node = $event->getNode();
142 2901
            if ($node instanceof AbstractStringContainerBlock) {
143 2859
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
144
            }
145
        }
146 2901
    }
147
148
    /**
149
     * Sets the container to the last open child (or its parent)
150
     *
151
     * @param ContextInterface $context
152
     * @param Cursor           $cursor
153
     */
154 2901
    private function resetContainer(ContextInterface $context, Cursor $cursor): void
155
    {
156 2901
        $container = $context->getDocument();
157
158 2901
        while ($lastChild = $container->lastChild()) {
159 1356
            if (!($lastChild instanceof AbstractBlock)) {
160
                break;
161
            }
162
163 1356
            if (!$lastChild->isOpen()) {
164 576
                break;
165
            }
166
167 1347
            $container = $lastChild;
168 1347
            if (!$container->matchesNextLine($cursor)) {
169 897
                $container = $container->parent(); // back up to the last matching block
170 897
                break;
171
            }
172
        }
173
174 2901
        $context->setContainer($container);
175 2901
    }
176
177
    /**
178
     * Parse blocks
179
     *
180
     * @param ContextInterface $context
181
     * @param Cursor           $cursor
182
     */
183 2901
    private function parseBlocks(ContextInterface $context, Cursor $cursor): void
184
    {
185 2901
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
186 2901
            $parsed = false;
187 2901
            foreach ($this->environment->getBlockParsers() as $parser) {
188 2901
                if ($parser->parse($context, $cursor)) {
189 1029
                    $parsed = true;
190 1653
                    break;
191
                }
192
            }
193
194 2901
            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
195 2874
                $context->setBlocksParsed(true);
196 2874
                break;
197
            }
198
        }
199 2901
    }
200
201 2901
    private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
202
    {
203 2901
        $tip = $context->getTip();
204
205 2901
        if ($tip instanceof Paragraph &&
206 2901
            !$context->getBlockCloser()->areAllClosed() &&
207 2901
            !$cursor->isBlank() &&
208 2901
            \count($tip->getStrings()) > 0) {
209
210
            // lazy paragraph continuation
211 48
            $tip->addLine($cursor->getRemainder());
212
213 48
            return true;
214
        }
215
216 2901
        return false;
217
    }
218
219 2901
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor): void
220
    {
221 2901
        $container = $context->getContainer();
222
223 2901
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
224 645
            if ($lastChild instanceof AbstractBlock) {
225 645
                $lastChild->setLastLineBlank(true);
226
            }
227
        }
228
229 2901
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
230
231
        // Propagate lastLineBlank up through parents:
232 2901
        while ($container instanceof AbstractBlock && $container->endsWithBlankLine() !== $lastLineBlank) {
233 798
            $container->setLastLineBlank($lastLineBlank);
234 798
            $container = $container->parent();
235
        }
236 2901
    }
237
}
238