1<?php
2
3/*
4 * This file is part of the league/commonmark package.
5 *
6 * (c) Colin O'Dell <colinodell@gmail.com>
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
15namespace League\CommonMark;
16
17use League\CommonMark\Block\Element\AbstractBlock;
18use League\CommonMark\Block\Element\AbstractStringContainerBlock;
19use League\CommonMark\Block\Element\Document;
20use League\CommonMark\Block\Element\Paragraph;
21use League\CommonMark\Block\Element\StringContainerInterface;
22use League\CommonMark\Event\DocumentParsedEvent;
23use League\CommonMark\Event\DocumentPreParsedEvent;
24use League\CommonMark\Input\MarkdownInput;
25
26final 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    public function __construct(EnvironmentInterface $environment)
47    {
48        $this->environment = $environment;
49        $this->inlineParserEngine = new InlineParserEngine($environment);
50        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', \PHP_INT_MAX);
51
52        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    }
60
61    /**
62     * @param string $input
63     *
64     * @throws \RuntimeException
65     *
66     * @return Document
67     */
68    public function parse(string $input): Document
69    {
70        $document = new Document();
71
72        $preParsedEvent = new DocumentPreParsedEvent($document, new MarkdownInput($input));
73        $this->environment->dispatch($preParsedEvent);
74        $markdown = $preParsedEvent->getMarkdown();
75
76        $context = new Context($document, $this->environment);
77
78        foreach ($markdown->getLines() as $line) {
79            $context->setNextLine($line);
80            $this->incorporateLine($context);
81        }
82
83        $lineCount = $markdown->getLineCount();
84        while ($tip = $context->getTip()) {
85            $tip->finalize($context, $lineCount);
86        }
87
88        $this->processInlines($context);
89
90        $this->environment->dispatch(new DocumentParsedEvent($document));
91
92        return $document;
93    }
94
95    private function incorporateLine(ContextInterface $context): void
96    {
97        $context->getBlockCloser()->resetTip();
98        $context->setBlocksParsed(false);
99
100        $cursor = new Cursor($context->getLine());
101
102        $this->resetContainer($context, $cursor);
103        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
104
105        $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        if ($this->handleLazyParagraphContinuation($context, $cursor)) {
110            return;
111        }
112
113        // not a lazy continuation
114        // finalize any blocks not matched
115        $context->getBlockCloser()->closeUnmatchedBlocks();
116
117        // Determine whether the last line is blank, updating parents as needed
118        $this->setAndPropagateLastLineBlank($context, $cursor);
119
120        // Handle any remaining cursor contents
121        if ($context->getContainer() instanceof StringContainerInterface) {
122            $context->getContainer()->handleRemainingContents($context, $cursor);
123        } elseif (!$cursor->isBlank()) {
124            // Create paragraph container for line
125            $p = new Paragraph();
126            $context->addBlock($p);
127            $cursor->advanceToNextNonSpaceOrTab();
128            $p->addLine($cursor->getRemainder());
129        }
130    }
131
132    private function processInlines(ContextInterface $context): void
133    {
134        $walker = $context->getDocument()->walker();
135
136        while ($event = $walker->next()) {
137            if (!$event->isEntering()) {
138                continue;
139            }
140
141            $node = $event->getNode();
142            if ($node instanceof AbstractStringContainerBlock) {
143                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
144            }
145        }
146    }
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    private function resetContainer(ContextInterface $context, Cursor $cursor): void
155    {
156        $container = $context->getDocument();
157
158        while ($lastChild = $container->lastChild()) {
159            if (!($lastChild instanceof AbstractBlock)) {
160                break;
161            }
162
163            if (!$lastChild->isOpen()) {
164                break;
165            }
166
167            $container = $lastChild;
168            if (!$container->matchesNextLine($cursor)) {
169                $container = $container->parent(); // back up to the last matching block
170                break;
171            }
172        }
173
174        $context->setContainer($container);
175    }
176
177    /**
178     * Parse blocks
179     *
180     * @param ContextInterface $context
181     * @param Cursor           $cursor
182     */
183    private function parseBlocks(ContextInterface $context, Cursor $cursor): void
184    {
185        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
186            $parsed = false;
187            foreach ($this->environment->getBlockParsers() as $parser) {
188                if ($parser->parse($context, $cursor)) {
189                    $parsed = true;
190                    break;
191                }
192            }
193
194            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
195                $context->setBlocksParsed(true);
196                break;
197            }
198        }
199    }
200
201    private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
202    {
203        $tip = $context->getTip();
204
205        if ($tip instanceof Paragraph &&
206            !$context->getBlockCloser()->areAllClosed() &&
207            !$cursor->isBlank() &&
208            \count($tip->getStrings()) > 0) {
209
210            // lazy paragraph continuation
211            $tip->addLine($cursor->getRemainder());
212
213            return true;
214        }
215
216        return false;
217    }
218
219    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor): void
220    {
221        $container = $context->getContainer();
222
223        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
224            if ($lastChild instanceof AbstractBlock) {
225                $lastChild->setLastLineBlank(true);
226            }
227        }
228
229        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
230
231        // Propagate lastLineBlank up through parents:
232        while ($container instanceof AbstractBlock && $container->endsWithBlankLine() !== $lastLineBlank) {
233            $container->setLastLineBlank($lastLineBlank);
234            $container = $container->parent();
235        }
236    }
237}
238