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