vendor/league/commonmark/src/Environment/Environment.php line 431

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.  * For the full copyright and license information, please view the LICENSE
  12.  * file that was distributed with this source code.
  13.  */
  14. namespace League\CommonMark\Environment;
  15. use League\CommonMark\Delimiter\DelimiterParser;
  16. use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
  17. use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
  18. use League\CommonMark\Event\DocumentParsedEvent;
  19. use League\CommonMark\Event\ListenerData;
  20. use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
  21. use League\CommonMark\Extension\ConfigurableExtensionInterface;
  22. use League\CommonMark\Extension\ExtensionInterface;
  23. use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
  24. use League\CommonMark\Normalizer\SlugNormalizer;
  25. use League\CommonMark\Normalizer\TextNormalizerInterface;
  26. use League\CommonMark\Normalizer\UniqueSlugNormalizer;
  27. use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface;
  28. use League\CommonMark\Parser\Block\BlockStartParserInterface;
  29. use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser;
  30. use League\CommonMark\Parser\Inline\InlineParserInterface;
  31. use League\CommonMark\Renderer\NodeRendererInterface;
  32. use League\CommonMark\Util\HtmlFilter;
  33. use League\CommonMark\Util\PrioritizedList;
  34. use League\Config\Configuration;
  35. use League\Config\ConfigurationAwareInterface;
  36. use League\Config\ConfigurationInterface;
  37. use Nette\Schema\Expect;
  38. use Psr\EventDispatcher\EventDispatcherInterface;
  39. use Psr\EventDispatcher\ListenerProviderInterface;
  40. use Psr\EventDispatcher\StoppableEventInterface;
  41. final class Environment implements EnvironmentInterfaceEnvironmentBuilderInterfaceListenerProviderInterface
  42. {
  43.     /**
  44.      * @var ExtensionInterface[]
  45.      *
  46.      * @psalm-readonly-allow-private-mutation
  47.      */
  48.     private array $extensions = [];
  49.     /**
  50.      * @var ExtensionInterface[]
  51.      *
  52.      * @psalm-readonly-allow-private-mutation
  53.      */
  54.     private array $uninitializedExtensions = [];
  55.     /** @psalm-readonly-allow-private-mutation */
  56.     private bool $extensionsInitialized false;
  57.     /**
  58.      * @var PrioritizedList<BlockStartParserInterface>
  59.      *
  60.      * @psalm-readonly
  61.      */
  62.     private PrioritizedList $blockStartParsers;
  63.     /**
  64.      * @var PrioritizedList<InlineParserInterface>
  65.      *
  66.      * @psalm-readonly
  67.      */
  68.     private PrioritizedList $inlineParsers;
  69.     /** @psalm-readonly */
  70.     private DelimiterProcessorCollection $delimiterProcessors;
  71.     /**
  72.      * @var array<string, PrioritizedList<NodeRendererInterface>>
  73.      *
  74.      * @psalm-readonly-allow-private-mutation
  75.      */
  76.     private array $renderersByClass = [];
  77.     /**
  78.      * @var PrioritizedList<ListenerData>
  79.      *
  80.      * @psalm-readonly-allow-private-mutation
  81.      */
  82.     private PrioritizedList $listenerData;
  83.     private ?EventDispatcherInterface $eventDispatcher null;
  84.     /** @psalm-readonly */
  85.     private Configuration $config;
  86.     private ?TextNormalizerInterface $slugNormalizer null;
  87.     /**
  88.      * @param array<string, mixed> $config
  89.      */
  90.     public function __construct(array $config = [])
  91.     {
  92.         $this->config self::createDefaultConfiguration();
  93.         $this->config->merge($config);
  94.         $this->blockStartParsers   = new PrioritizedList();
  95.         $this->inlineParsers       = new PrioritizedList();
  96.         $this->listenerData        = new PrioritizedList();
  97.         $this->delimiterProcessors = new DelimiterProcessorCollection();
  98.         // Performance optimization: always include a block "parser" that aborts parsing if a line starts with a letter
  99.         // and is therefore unlikely to match any lines as a block start.
  100.         $this->addBlockStartParser(new SkipLinesStartingWithLettersParser(), 249);
  101.     }
  102.     public function getConfiguration(): ConfigurationInterface
  103.     {
  104.         return $this->config->reader();
  105.     }
  106.     /**
  107.      * @deprecated Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.
  108.      *
  109.      * @param array<string, mixed> $config
  110.      */
  111.     public function mergeConfig(array $config): void
  112.     {
  113.         @\trigger_error('Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.'\E_USER_DEPRECATED);
  114.         $this->assertUninitialized('Failed to modify configuration.');
  115.         $this->config->merge($config);
  116.     }
  117.     public function addBlockStartParser(BlockStartParserInterface $parserint $priority 0): EnvironmentBuilderInterface
  118.     {
  119.         $this->assertUninitialized('Failed to add block start parser.');
  120.         $this->blockStartParsers->add($parser$priority);
  121.         $this->injectEnvironmentAndConfigurationIfNeeded($parser);
  122.         return $this;
  123.     }
  124.     public function addInlineParser(InlineParserInterface $parserint $priority 0): EnvironmentBuilderInterface
  125.     {
  126.         $this->assertUninitialized('Failed to add inline parser.');
  127.         $this->inlineParsers->add($parser$priority);
  128.         $this->injectEnvironmentAndConfigurationIfNeeded($parser);
  129.         return $this;
  130.     }
  131.     public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
  132.     {
  133.         $this->assertUninitialized('Failed to add delimiter processor.');
  134.         $this->delimiterProcessors->add($processor);
  135.         $this->injectEnvironmentAndConfigurationIfNeeded($processor);
  136.         return $this;
  137.     }
  138.     public function addRenderer(string $nodeClassNodeRendererInterface $rendererint $priority 0): EnvironmentBuilderInterface
  139.     {
  140.         $this->assertUninitialized('Failed to add renderer.');
  141.         if (! isset($this->renderersByClass[$nodeClass])) {
  142.             $this->renderersByClass[$nodeClass] = new PrioritizedList();
  143.         }
  144.         $this->renderersByClass[$nodeClass]->add($renderer$priority);
  145.         $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
  146.         return $this;
  147.     }
  148.     /**
  149.      * {@inheritDoc}
  150.      */
  151.     public function getBlockStartParsers(): iterable
  152.     {
  153.         if (! $this->extensionsInitialized) {
  154.             $this->initializeExtensions();
  155.         }
  156.         return $this->blockStartParsers->getIterator();
  157.     }
  158.     public function getDelimiterProcessors(): DelimiterProcessorCollection
  159.     {
  160.         if (! $this->extensionsInitialized) {
  161.             $this->initializeExtensions();
  162.         }
  163.         return $this->delimiterProcessors;
  164.     }
  165.     /**
  166.      * {@inheritDoc}
  167.      */
  168.     public function getRenderersForClass(string $nodeClass): iterable
  169.     {
  170.         if (! $this->extensionsInitialized) {
  171.             $this->initializeExtensions();
  172.         }
  173.         // If renderers are defined for this specific class, return them immediately
  174.         if (isset($this->renderersByClass[$nodeClass])) {
  175.             return $this->renderersByClass[$nodeClass];
  176.         }
  177.         /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
  178.         while (\class_exists($parent ??= $nodeClass) && $parent \get_parent_class($parent)) {
  179.             if (! isset($this->renderersByClass[$parent])) {
  180.                 continue;
  181.             }
  182.             // "Cache" this result to avoid future loops
  183.             return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
  184.         }
  185.         return [];
  186.     }
  187.     /**
  188.      * {@inheritDoc}
  189.      */
  190.     public function getExtensions(): iterable
  191.     {
  192.         return $this->extensions;
  193.     }
  194.     /**
  195.      * Add a single extension
  196.      *
  197.      * @return $this
  198.      */
  199.     public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
  200.     {
  201.         $this->assertUninitialized('Failed to add extension.');
  202.         $this->extensions[]              = $extension;
  203.         $this->uninitializedExtensions[] = $extension;
  204.         if ($extension instanceof ConfigurableExtensionInterface) {
  205.             $extension->configureSchema($this->config);
  206.         }
  207.         return $this;
  208.     }
  209.     private function initializeExtensions(): void
  210.     {
  211.         // Initialize the slug normalizer
  212.         $this->getSlugNormalizer();
  213.         // Ask all extensions to register their components
  214.         while (\count($this->uninitializedExtensions) > 0) {
  215.             foreach ($this->uninitializedExtensions as $i => $extension) {
  216.                 $extension->register($this);
  217.                 unset($this->uninitializedExtensions[$i]);
  218.             }
  219.         }
  220.         $this->extensionsInitialized true;
  221.         // Create the special delimiter parser if any processors were registered
  222.         if ($this->delimiterProcessors->count() > 0) {
  223.             $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
  224.         }
  225.     }
  226.     private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
  227.     {
  228.         if ($object instanceof EnvironmentAwareInterface) {
  229.             $object->setEnvironment($this);
  230.         }
  231.         if ($object instanceof ConfigurationAwareInterface) {
  232.             $object->setConfiguration($this->config->reader());
  233.         }
  234.     }
  235.     /**
  236.      * @deprecated Instantiate the environment and add the extension yourself
  237.      *
  238.      * @param array<string, mixed> $config
  239.      */
  240.     public static function createCommonMarkEnvironment(array $config = []): Environment
  241.     {
  242.         $environment = new self($config);
  243.         $environment->addExtension(new CommonMarkCoreExtension());
  244.         return $environment;
  245.     }
  246.     /**
  247.      * @deprecated Instantiate the environment and add the extension yourself
  248.      *
  249.      * @param array<string, mixed> $config
  250.      */
  251.     public static function createGFMEnvironment(array $config = []): Environment
  252.     {
  253.         $environment = new self($config);
  254.         $environment->addExtension(new CommonMarkCoreExtension());
  255.         $environment->addExtension(new GithubFlavoredMarkdownExtension());
  256.         return $environment;
  257.     }
  258.     public function addEventListener(string $eventClass, callable $listenerint $priority 0): EnvironmentBuilderInterface
  259.     {
  260.         $this->assertUninitialized('Failed to add event listener.');
  261.         $this->listenerData->add(new ListenerData($eventClass$listener), $priority);
  262.         if (\is_object($listener)) {
  263.             $this->injectEnvironmentAndConfigurationIfNeeded($listener);
  264.         } elseif (\is_array($listener) && \is_object($listener[0])) {
  265.             $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
  266.         }
  267.         return $this;
  268.     }
  269.     /**
  270.      * {@inheritDoc}
  271.      */
  272.     public function dispatch(object $event)
  273.     {
  274.         if (! $this->extensionsInitialized) {
  275.             $this->initializeExtensions();
  276.         }
  277.         if ($this->eventDispatcher !== null) {
  278.             return $this->eventDispatcher->dispatch($event);
  279.         }
  280.         foreach ($this->getListenersForEvent($event) as $listener) {
  281.             if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
  282.                 return $event;
  283.             }
  284.             $listener($event);
  285.         }
  286.         return $event;
  287.     }
  288.     public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
  289.     {
  290.         $this->eventDispatcher $dispatcher;
  291.     }
  292.     /**
  293.      * {@inheritDoc}
  294.      *
  295.      * @return iterable<callable>
  296.      */
  297.     public function getListenersForEvent(object $event): iterable
  298.     {
  299.         foreach ($this->listenerData as $listenerData) {
  300.             \assert($listenerData instanceof ListenerData);
  301.             /** @psalm-suppress ArgumentTypeCoercion */
  302.             if (! \is_a($event$listenerData->getEvent())) {
  303.                 continue;
  304.             }
  305.             yield function (object $event) use ($listenerData) {
  306.                 if (! $this->extensionsInitialized) {
  307.                     $this->initializeExtensions();
  308.                 }
  309.                 return \call_user_func($listenerData->getListener(), $event);
  310.             };
  311.         }
  312.     }
  313.     /**
  314.      * @return iterable<InlineParserInterface>
  315.      */
  316.     public function getInlineParsers(): iterable
  317.     {
  318.         if (! $this->extensionsInitialized) {
  319.             $this->initializeExtensions();
  320.         }
  321.         return $this->inlineParsers->getIterator();
  322.     }
  323.     public function getSlugNormalizer(): TextNormalizerInterface
  324.     {
  325.         if ($this->slugNormalizer === null) {
  326.             $normalizer $this->config->get('slug_normalizer/instance');
  327.             \assert($normalizer instanceof TextNormalizerInterface);
  328.             $this->injectEnvironmentAndConfigurationIfNeeded($normalizer);
  329.             if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) {
  330.                 $normalizer = new UniqueSlugNormalizer($normalizer);
  331.             }
  332.             if ($normalizer instanceof UniqueSlugNormalizer) {
  333.                 if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) {
  334.                     $this->addEventListener(DocumentParsedEvent::class, [$normalizer'clearHistory'], -1000);
  335.                 }
  336.             }
  337.             $this->slugNormalizer $normalizer;
  338.         }
  339.         return $this->slugNormalizer;
  340.     }
  341.     /**
  342.      * @throws \RuntimeException
  343.      */
  344.     private function assertUninitialized(string $message): void
  345.     {
  346.         if ($this->extensionsInitialized) {
  347.             throw new \RuntimeException($message ' Extensions have already been initialized.');
  348.         }
  349.     }
  350.     public static function createDefaultConfiguration(): Configuration
  351.     {
  352.         return new Configuration([
  353.             'html_input' => Expect::anyOf(HtmlFilter::STRIPHtmlFilter::ALLOWHtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
  354.             'allow_unsafe_links' => Expect::bool(true),
  355.             'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
  356.             'renderer' => Expect::structure([
  357.                 'block_separator' => Expect::string("\n"),
  358.                 'inner_separator' => Expect::string("\n"),
  359.                 'soft_break' => Expect::string("\n"),
  360.             ]),
  361.             'slug_normalizer' => Expect::structure([
  362.                 'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()),
  363.                 'max_length' => Expect::int()->min(0)->default(255),
  364.                 'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLEDUniqueSlugNormalizerInterface::PER_ENVIRONMENTUniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT),
  365.             ]),
  366.         ]);
  367.     }
  368. }