vendor/league/commonmark/src/Parser/MarkdownParser.php line 142

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  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.  * Additional code based on commonmark-java (https://github.com/commonmark/commonmark-java)
  12.  *  - (c) Atlassian Pty Ltd
  13.  *
  14.  * For the full copyright and license information, please view the LICENSE
  15.  * file that was distributed with this source code.
  16.  */
  17. namespace League\CommonMark\Parser;
  18. use League\CommonMark\Environment\EnvironmentInterface;
  19. use League\CommonMark\Event\DocumentParsedEvent;
  20. use League\CommonMark\Event\DocumentPreParsedEvent;
  21. use League\CommonMark\Input\MarkdownInput;
  22. use League\CommonMark\Node\Block\Document;
  23. use League\CommonMark\Node\Block\Paragraph;
  24. use League\CommonMark\Parser\Block\BlockContinueParserInterface;
  25. use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface;
  26. use League\CommonMark\Parser\Block\BlockStart;
  27. use League\CommonMark\Parser\Block\BlockStartParserInterface;
  28. use League\CommonMark\Parser\Block\DocumentBlockParser;
  29. use League\CommonMark\Parser\Block\ParagraphParser;
  30. use League\CommonMark\Reference\ReferenceInterface;
  31. use League\CommonMark\Reference\ReferenceMap;
  32. final class MarkdownParser implements MarkdownParserInterface
  33. {
  34.     /** @psalm-readonly */
  35.     private EnvironmentInterface $environment;
  36.     /** @psalm-readonly-allow-private-mutation */
  37.     private int $maxNestingLevel;
  38.     /** @psalm-readonly-allow-private-mutation */
  39.     private ReferenceMap $referenceMap;
  40.     /** @psalm-readonly-allow-private-mutation */
  41.     private int $lineNumber 0;
  42.     /** @psalm-readonly-allow-private-mutation */
  43.     private Cursor $cursor;
  44.     /**
  45.      * @var array<int, BlockContinueParserInterface>
  46.      *
  47.      * @psalm-readonly-allow-private-mutation
  48.      */
  49.     private array $activeBlockParsers = [];
  50.     /**
  51.      * @var array<int, BlockContinueParserWithInlinesInterface>
  52.      *
  53.      * @psalm-readonly-allow-private-mutation
  54.      */
  55.     private array $closedBlockParsers = [];
  56.     public function __construct(EnvironmentInterface $environment)
  57.     {
  58.         $this->environment $environment;
  59.     }
  60.     private function initialize(): void
  61.     {
  62.         $this->referenceMap       = new ReferenceMap();
  63.         $this->lineNumber         0;
  64.         $this->activeBlockParsers = [];
  65.         $this->closedBlockParsers = [];
  66.         $this->maxNestingLevel $this->environment->getConfiguration()->get('max_nesting_level');
  67.     }
  68.     /**
  69.      * @throws \RuntimeException
  70.      */
  71.     public function parse(string $input): Document
  72.     {
  73.         $this->initialize();
  74.         $documentParser = new DocumentBlockParser($this->referenceMap);
  75.         $this->activateBlockParser($documentParser);
  76.         $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
  77.         $this->environment->dispatch($preParsedEvent);
  78.         $markdownInput $preParsedEvent->getMarkdown();
  79.         foreach ($markdownInput->getLines() as $lineNumber => $line) {
  80.             $this->lineNumber $lineNumber;
  81.             $this->parseLine($line);
  82.         }
  83.         // finalizeAndProcess
  84.         $this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber);
  85.         $this->processInlines();
  86.         $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
  87.         return $documentParser->getBlock();
  88.     }
  89.     /**
  90.      * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each
  91.      * line of input, then finalizing the document.
  92.      */
  93.     private function parseLine(string $line): void
  94.     {
  95.         $this->cursor = new Cursor($line);
  96.         $matches $this->parseBlockContinuation();
  97.         if ($matches === null) {
  98.             return;
  99.         }
  100.         $unmatchedBlocks \count($this->activeBlockParsers) - $matches;
  101.         $blockParser     $this->activeBlockParsers[$matches 1];
  102.         $startedNewBlock false;
  103.         // Unless last matched container is a code block, try new container starts,
  104.         // adding children to the last matched container:
  105.         $tryBlockStarts $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
  106.         while ($tryBlockStarts) {
  107.             // this is a little performance optimization
  108.             if ($this->cursor->isBlank()) {
  109.                 $this->cursor->advanceToEnd();
  110.                 break;
  111.             }
  112.             if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
  113.                 break;
  114.             }
  115.             $blockStart $this->findBlockStart($blockParser);
  116.             if ($blockStart === null || $blockStart->isAborting()) {
  117.                 $this->cursor->advanceToNextNonSpaceOrTab();
  118.                 break;
  119.             }
  120.             if (($state $blockStart->getCursorState()) !== null) {
  121.                 $this->cursor->restoreState($state);
  122.             }
  123.             $startedNewBlock true;
  124.             // We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now.
  125.             if ($unmatchedBlocks 0) {
  126.                 $this->closeBlockParsers($unmatchedBlocks$this->lineNumber 1);
  127.                 $unmatchedBlocks 0;
  128.             }
  129.             if ($blockStart->isReplaceActiveBlockParser()) {
  130.                 $this->prepareActiveBlockParserForReplacement();
  131.             }
  132.             foreach ($blockStart->getBlockParsers() as $newBlockParser) {
  133.                 $blockParser    $this->addChild($newBlockParser);
  134.                 $tryBlockStarts $newBlockParser->isContainer();
  135.             }
  136.         }
  137.         // What remains at the offset is a text line. Add the text to the appropriate block.
  138.         // First check for a lazy paragraph continuation:
  139.         if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
  140.             $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
  141.         } else {
  142.             // finalize any blocks not matched
  143.             if ($unmatchedBlocks 0) {
  144.                 $this->closeBlockParsers($unmatchedBlocks$this->lineNumber);
  145.             }
  146.             if (! $blockParser->isContainer()) {
  147.                 $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
  148.             } elseif (! $this->cursor->isBlank()) {
  149.                 $this->addChild(new ParagraphParser());
  150.                 $this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
  151.             }
  152.         }
  153.     }
  154.     private function parseBlockContinuation(): ?int
  155.     {
  156.         // For each containing block, try to parse the associated line start.
  157.         // The document will always match, so we can skip the first block parser and start at 1 matches
  158.         $matches 1;
  159.         for ($i 1$i \count($this->activeBlockParsers); $i++) {
  160.             $blockParser   $this->activeBlockParsers[$i];
  161.             $blockContinue $blockParser->tryContinue(clone $this->cursor$this->getActiveBlockParser());
  162.             if ($blockContinue === null) {
  163.                 break;
  164.             }
  165.             if ($blockContinue->isFinalize()) {
  166.                 $this->closeBlockParsers(\count($this->activeBlockParsers) - $i$this->lineNumber);
  167.                 return null;
  168.             }
  169.             if (($state $blockContinue->getCursorState()) !== null) {
  170.                 $this->cursor->restoreState($state);
  171.             }
  172.             $matches++;
  173.         }
  174.         return $matches;
  175.     }
  176.     private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
  177.     {
  178.         $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
  179.         foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
  180.             \assert($blockStartParser instanceof BlockStartParserInterface);
  181.             if (($result $blockStartParser->tryStart(clone $this->cursor$matchedBlockParser)) !== null) {
  182.                 return $result;
  183.             }
  184.         }
  185.         return null;
  186.     }
  187.     private function closeBlockParsers(int $countint $endLineNumber): void
  188.     {
  189.         for ($i 0$i $count$i++) {
  190.             $blockParser $this->deactivateBlockParser();
  191.             $this->finalize($blockParser$endLineNumber);
  192.             // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
  193.             if ($blockParser instanceof BlockContinueParserWithInlinesInterface) {
  194.                 // Remember for inline parsing
  195.                 $this->closedBlockParsers[] = $blockParser;
  196.             }
  197.         }
  198.     }
  199.     /**
  200.      * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings,
  201.      * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference
  202.      * definitions.
  203.      */
  204.     private function finalize(BlockContinueParserInterface $blockParserint $endLineNumber): void
  205.     {
  206.         if ($blockParser instanceof ParagraphParser) {
  207.             $this->updateReferenceMap($blockParser->getReferences());
  208.         }
  209.         $blockParser->getBlock()->setEndLine($endLineNumber);
  210.         $blockParser->closeBlock();
  211.     }
  212.     /**
  213.      * Walk through a block & children recursively, parsing string content into inline content where appropriate.
  214.      */
  215.     private function processInlines(): void
  216.     {
  217.         $p = new InlineParserEngine($this->environment$this->referenceMap);
  218.         foreach ($this->closedBlockParsers as $blockParser) {
  219.             $blockParser->parseInlines($p);
  220.         }
  221.     }
  222.     /**
  223.      * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
  224.      * its parent, and so on til we find a block that can accept children.
  225.      */
  226.     private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
  227.     {
  228.         $blockParser->getBlock()->setStartLine($this->lineNumber);
  229.         while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
  230.             $this->closeBlockParsers(1$this->lineNumber 1);
  231.         }
  232.         $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
  233.         $this->activateBlockParser($blockParser);
  234.         return $blockParser;
  235.     }
  236.     private function activateBlockParser(BlockContinueParserInterface $blockParser): void
  237.     {
  238.         $this->activeBlockParsers[] = $blockParser;
  239.     }
  240.     private function deactivateBlockParser(): BlockContinueParserInterface
  241.     {
  242.         $popped \array_pop($this->activeBlockParsers);
  243.         if ($popped === null) {
  244.             throw new \RuntimeException('The last block parser should not be deactivated');
  245.         }
  246.         return $popped;
  247.     }
  248.     private function prepareActiveBlockParserForReplacement(): void
  249.     {
  250.         // Note that we don't want to parse inlines or finalize this block, as it's getting replaced.
  251.         $old $this->deactivateBlockParser();
  252.         if ($old instanceof ParagraphParser) {
  253.             $this->updateReferenceMap($old->getReferences());
  254.         }
  255.         $old->getBlock()->detach();
  256.     }
  257.     /**
  258.      * @param ReferenceInterface[] $references
  259.      */
  260.     private function updateReferenceMap(iterable $references): void
  261.     {
  262.         foreach ($references as $reference) {
  263.             if (! $this->referenceMap->contains($reference->getLabel())) {
  264.                 $this->referenceMap->add($reference);
  265.             }
  266.         }
  267.     }
  268.     public function getActiveBlockParser(): BlockContinueParserInterface
  269.     {
  270.         $active \end($this->activeBlockParsers);
  271.         if ($active === false) {
  272.             throw new \RuntimeException('No active block parsers are available');
  273.         }
  274.         return $active;
  275.     }
  276. }