238 lines
7.6 KiB
PHP
238 lines
7.6 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the league/commonmark package.
|
|
*
|
|
* (c) Colin O'Dell <colinodell@gmail.com>
|
|
*
|
|
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
|
* - (c) John MacFarlane
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace League\CommonMark;
|
|
|
|
use League\CommonMark\Block\Element\AbstractBlock;
|
|
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
|
|
use League\CommonMark\Block\Element\Document;
|
|
use League\CommonMark\Block\Element\Paragraph;
|
|
use League\CommonMark\Block\Element\StringContainerInterface;
|
|
use League\CommonMark\Event\DocumentParsedEvent;
|
|
use League\CommonMark\Event\DocumentPreParsedEvent;
|
|
use League\CommonMark\Input\MarkdownInput;
|
|
|
|
final class DocParser implements DocParserInterface
|
|
{
|
|
/**
|
|
* @var EnvironmentInterface
|
|
*/
|
|
private $environment;
|
|
|
|
/**
|
|
* @var InlineParserEngine
|
|
*/
|
|
private $inlineParserEngine;
|
|
|
|
/**
|
|
* @var int|float
|
|
*/
|
|
private $maxNestingLevel;
|
|
|
|
/**
|
|
* @param EnvironmentInterface $environment
|
|
*/
|
|
public function __construct(EnvironmentInterface $environment)
|
|
{
|
|
$this->environment = $environment;
|
|
$this->inlineParserEngine = new InlineParserEngine($environment);
|
|
$this->maxNestingLevel = $environment->getConfig('max_nesting_level', \PHP_INT_MAX);
|
|
|
|
if (\is_float($this->maxNestingLevel)) {
|
|
if ($this->maxNestingLevel === \INF) {
|
|
@\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);
|
|
} else {
|
|
@\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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $input
|
|
*
|
|
* @throws \RuntimeException
|
|
*
|
|
* @return Document
|
|
*/
|
|
public function parse(string $input): Document
|
|
{
|
|
$document = new Document();
|
|
|
|
$preParsedEvent = new DocumentPreParsedEvent($document, new MarkdownInput($input));
|
|
$this->environment->dispatch($preParsedEvent);
|
|
$markdown = $preParsedEvent->getMarkdown();
|
|
|
|
$context = new Context($document, $this->environment);
|
|
|
|
foreach ($markdown->getLines() as $line) {
|
|
$context->setNextLine($line);
|
|
$this->incorporateLine($context);
|
|
}
|
|
|
|
$lineCount = $markdown->getLineCount();
|
|
while ($tip = $context->getTip()) {
|
|
$tip->finalize($context, $lineCount);
|
|
}
|
|
|
|
$this->processInlines($context);
|
|
|
|
$this->environment->dispatch(new DocumentParsedEvent($document));
|
|
|
|
return $document;
|
|
}
|
|
|
|
private function incorporateLine(ContextInterface $context): void
|
|
{
|
|
$context->getBlockCloser()->resetTip();
|
|
$context->setBlocksParsed(false);
|
|
|
|
$cursor = new Cursor($context->getLine());
|
|
|
|
$this->resetContainer($context, $cursor);
|
|
$context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
|
|
|
|
$this->parseBlocks($context, $cursor);
|
|
|
|
// What remains at the offset is a text line. Add the text to the appropriate container.
|
|
// First check for a lazy paragraph continuation:
|
|
if ($this->handleLazyParagraphContinuation($context, $cursor)) {
|
|
return;
|
|
}
|
|
|
|
// not a lazy continuation
|
|
// finalize any blocks not matched
|
|
$context->getBlockCloser()->closeUnmatchedBlocks();
|
|
|
|
// Determine whether the last line is blank, updating parents as needed
|
|
$this->setAndPropagateLastLineBlank($context, $cursor);
|
|
|
|
// Handle any remaining cursor contents
|
|
if ($context->getContainer() instanceof StringContainerInterface) {
|
|
$context->getContainer()->handleRemainingContents($context, $cursor);
|
|
} elseif (!$cursor->isBlank()) {
|
|
// Create paragraph container for line
|
|
$p = new Paragraph();
|
|
$context->addBlock($p);
|
|
$cursor->advanceToNextNonSpaceOrTab();
|
|
$p->addLine($cursor->getRemainder());
|
|
}
|
|
}
|
|
|
|
private function processInlines(ContextInterface $context): void
|
|
{
|
|
$walker = $context->getDocument()->walker();
|
|
|
|
while ($event = $walker->next()) {
|
|
if (!$event->isEntering()) {
|
|
continue;
|
|
}
|
|
|
|
$node = $event->getNode();
|
|
if ($node instanceof AbstractStringContainerBlock) {
|
|
$this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the container to the last open child (or its parent)
|
|
*
|
|
* @param ContextInterface $context
|
|
* @param Cursor $cursor
|
|
*/
|
|
private function resetContainer(ContextInterface $context, Cursor $cursor): void
|
|
{
|
|
$container = $context->getDocument();
|
|
|
|
while ($lastChild = $container->lastChild()) {
|
|
if (!($lastChild instanceof AbstractBlock)) {
|
|
break;
|
|
}
|
|
|
|
if (!$lastChild->isOpen()) {
|
|
break;
|
|
}
|
|
|
|
$container = $lastChild;
|
|
if (!$container->matchesNextLine($cursor)) {
|
|
$container = $container->parent(); // back up to the last matching block
|
|
break;
|
|
}
|
|
}
|
|
|
|
$context->setContainer($container);
|
|
}
|
|
|
|
/**
|
|
* Parse blocks
|
|
*
|
|
* @param ContextInterface $context
|
|
* @param Cursor $cursor
|
|
*/
|
|
private function parseBlocks(ContextInterface $context, Cursor $cursor): void
|
|
{
|
|
while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
|
|
$parsed = false;
|
|
foreach ($this->environment->getBlockParsers() as $parser) {
|
|
if ($parser->parse($context, $cursor)) {
|
|
$parsed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$parsed || $context->getContainer() instanceof StringContainerInterface || (($tip = $context->getTip()) && $tip->getDepth() >= $this->maxNestingLevel)) {
|
|
$context->setBlocksParsed(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function handleLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
|
|
{
|
|
$tip = $context->getTip();
|
|
|
|
if ($tip instanceof Paragraph &&
|
|
!$context->getBlockCloser()->areAllClosed() &&
|
|
!$cursor->isBlank() &&
|
|
\count($tip->getStrings()) > 0) {
|
|
|
|
// lazy paragraph continuation
|
|
$tip->addLine($cursor->getRemainder());
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor): void
|
|
{
|
|
$container = $context->getContainer();
|
|
|
|
if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
|
|
if ($lastChild instanceof AbstractBlock) {
|
|
$lastChild->setLastLineBlank(true);
|
|
}
|
|
}
|
|
|
|
$lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
|
|
|
|
// Propagate lastLineBlank up through parents:
|
|
while ($container instanceof AbstractBlock && $container->endsWithBlankLine() !== $lastLineBlank) {
|
|
$container->setLastLineBlank($lastLineBlank);
|
|
$container = $container->parent();
|
|
}
|
|
}
|
|
}
|