thephpleague /
commonmark
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | /* |
||
| 6 | * This file is part of the league/commonmark package. |
||
| 7 | * |
||
| 8 | * (c) Colin O'Dell <[email protected]> |
||
| 9 | * |
||
| 10 | * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) |
||
| 11 | * - (c) John MacFarlane |
||
| 12 | * |
||
| 13 | * For the full copyright and license information, please view the LICENSE |
||
| 14 | * file that was distributed with this source code. |
||
| 15 | */ |
||
| 16 | |||
| 17 | namespace League\CommonMark\Environment; |
||
| 18 | |||
| 19 | use League\CommonMark\Delimiter\DelimiterParser; |
||
| 20 | use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; |
||
| 21 | use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; |
||
| 22 | use League\CommonMark\Event\DocumentParsedEvent; |
||
| 23 | use League\CommonMark\Event\ListenerData; |
||
| 24 | use League\CommonMark\Exception\AlreadyInitializedException; |
||
| 25 | use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; |
||
| 26 | use League\CommonMark\Extension\ConfigurableExtensionInterface; |
||
| 27 | use League\CommonMark\Extension\ExtensionInterface; |
||
| 28 | use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; |
||
| 29 | use League\CommonMark\Normalizer\SlugNormalizer; |
||
| 30 | use League\CommonMark\Normalizer\TextNormalizerInterface; |
||
| 31 | use League\CommonMark\Normalizer\UniqueSlugNormalizer; |
||
| 32 | use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface; |
||
| 33 | use League\CommonMark\Parser\Block\BlockStartParserInterface; |
||
| 34 | use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser; |
||
| 35 | use League\CommonMark\Parser\Inline\InlineParserInterface; |
||
| 36 | use League\CommonMark\Renderer\NodeRendererInterface; |
||
| 37 | use League\CommonMark\Util\HtmlFilter; |
||
| 38 | use League\CommonMark\Util\PrioritizedList; |
||
| 39 | use League\Config\Configuration; |
||
| 40 | use League\Config\ConfigurationAwareInterface; |
||
| 41 | use League\Config\ConfigurationInterface; |
||
| 42 | use Nette\Schema\Expect; |
||
| 43 | use Psr\EventDispatcher\EventDispatcherInterface; |
||
| 44 | use Psr\EventDispatcher\ListenerProviderInterface; |
||
| 45 | use Psr\EventDispatcher\StoppableEventInterface; |
||
| 46 | |||
| 47 | final class Environment implements EnvironmentInterface, EnvironmentBuilderInterface, ListenerProviderInterface |
||
| 48 | { |
||
| 49 | /** |
||
| 50 | * @var ExtensionInterface[] |
||
| 51 | * |
||
| 52 | * @psalm-readonly-allow-private-mutation |
||
| 53 | */ |
||
| 54 | private array $extensions = []; |
||
| 55 | |||
| 56 | /** |
||
| 57 | * @var ExtensionInterface[] |
||
| 58 | * |
||
| 59 | * @psalm-readonly-allow-private-mutation |
||
| 60 | */ |
||
| 61 | private array $uninitializedExtensions = []; |
||
| 62 | |||
| 63 | /** @psalm-readonly-allow-private-mutation */ |
||
| 64 | private bool $extensionsInitialized = false; |
||
| 65 | |||
| 66 | /** |
||
| 67 | * @var PrioritizedList<BlockStartParserInterface> |
||
| 68 | * |
||
| 69 | * @psalm-readonly |
||
| 70 | */ |
||
| 71 | private PrioritizedList $blockStartParsers; |
||
| 72 | |||
| 73 | /** |
||
| 74 | * @var PrioritizedList<InlineParserInterface> |
||
| 75 | * |
||
| 76 | * @psalm-readonly |
||
| 77 | */ |
||
| 78 | private PrioritizedList $inlineParsers; |
||
| 79 | |||
| 80 | /** @psalm-readonly */ |
||
| 81 | private DelimiterProcessorCollection $delimiterProcessors; |
||
| 82 | |||
| 83 | /** |
||
| 84 | * @var array<string, PrioritizedList<NodeRendererInterface>> |
||
|
0 ignored issues
–
show
Documentation
Bug
introduced
by
Loading history...
|
|||
| 85 | * |
||
| 86 | * @psalm-readonly-allow-private-mutation |
||
| 87 | */ |
||
| 88 | private array $renderersByClass = []; |
||
| 89 | |||
| 90 | /** |
||
| 91 | * @var PrioritizedList<ListenerData> |
||
| 92 | * |
||
| 93 | * @psalm-readonly-allow-private-mutation |
||
| 94 | */ |
||
| 95 | private PrioritizedList $listenerData; |
||
| 96 | |||
| 97 | private ?EventDispatcherInterface $eventDispatcher = null; |
||
| 98 | |||
| 99 | /** @psalm-readonly */ |
||
| 100 | private Configuration $config; |
||
| 101 | |||
| 102 | private ?TextNormalizerInterface $slugNormalizer = null; |
||
| 103 | |||
| 104 | /** |
||
| 105 | * @param array<string, mixed> $config |
||
| 106 | */ |
||
| 107 | 2494 | public function __construct(array $config = []) |
|
| 108 | { |
||
| 109 | 2494 | $this->config = self::createDefaultConfiguration(); |
|
| 110 | 2494 | $this->config->merge($config); |
|
| 111 | |||
| 112 | 2494 | $this->blockStartParsers = new PrioritizedList(); |
|
| 113 | 2494 | $this->inlineParsers = new PrioritizedList(); |
|
| 114 | 2494 | $this->listenerData = new PrioritizedList(); |
|
| 115 | 2494 | $this->delimiterProcessors = new DelimiterProcessorCollection(); |
|
| 116 | |||
| 117 | // Performance optimization: always include a block "parser" that aborts parsing if a line starts with a letter |
||
| 118 | // and is therefore unlikely to match any lines as a block start. |
||
| 119 | 2494 | $this->addBlockStartParser(new SkipLinesStartingWithLettersParser(), 249); |
|
| 120 | } |
||
| 121 | |||
| 122 | 2422 | public function getConfiguration(): ConfigurationInterface |
|
| 123 | { |
||
| 124 | 2422 | return $this->config->reader(); |
|
| 125 | } |
||
| 126 | |||
| 127 | /** |
||
| 128 | * @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. |
||
| 129 | * |
||
| 130 | * @param array<string, mixed> $config |
||
| 131 | */ |
||
| 132 | 4 | public function mergeConfig(array $config): void |
|
| 133 | { |
||
| 134 | 4 | @\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); |
|
| 135 | |||
| 136 | 4 | $this->assertUninitialized('Failed to modify configuration.'); |
|
| 137 | |||
| 138 | 2 | $this->config->merge($config); |
|
| 139 | } |
||
| 140 | |||
| 141 | 2498 | public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface |
|
| 142 | { |
||
| 143 | 2498 | $this->assertUninitialized('Failed to add block start parser.'); |
|
| 144 | |||
| 145 | 2498 | $this->blockStartParsers->add($parser, $priority); |
|
| 146 | 2498 | $this->injectEnvironmentAndConfigurationIfNeeded($parser); |
|
| 147 | |||
| 148 | 2498 | return $this; |
|
| 149 | } |
||
| 150 | |||
| 151 | 2396 | public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface |
|
| 152 | { |
||
| 153 | 2396 | $this->assertUninitialized('Failed to add inline parser.'); |
|
| 154 | |||
| 155 | 2394 | $this->inlineParsers->add($parser, $priority); |
|
| 156 | 2394 | $this->injectEnvironmentAndConfigurationIfNeeded($parser); |
|
| 157 | |||
| 158 | 2394 | return $this; |
|
| 159 | } |
||
| 160 | |||
| 161 | 2392 | public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface |
|
| 162 | { |
||
| 163 | 2392 | $this->assertUninitialized('Failed to add delimiter processor.'); |
|
| 164 | 2390 | $this->delimiterProcessors->add($processor); |
|
| 165 | 2390 | $this->injectEnvironmentAndConfigurationIfNeeded($processor); |
|
| 166 | |||
| 167 | 2390 | return $this; |
|
| 168 | } |
||
| 169 | |||
| 170 | 2404 | public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface |
|
| 171 | { |
||
| 172 | 2404 | $this->assertUninitialized('Failed to add renderer.'); |
|
| 173 | |||
| 174 | 2402 | if (! isset($this->renderersByClass[$nodeClass])) { |
|
| 175 | 2402 | $this->renderersByClass[$nodeClass] = new PrioritizedList(); |
|
| 176 | } |
||
| 177 | |||
| 178 | 2402 | $this->renderersByClass[$nodeClass]->add($renderer, $priority); |
|
| 179 | 2402 | $this->injectEnvironmentAndConfigurationIfNeeded($renderer); |
|
| 180 | |||
| 181 | 2402 | return $this; |
|
| 182 | } |
||
| 183 | |||
| 184 | /** |
||
| 185 | * {@inheritDoc} |
||
| 186 | */ |
||
| 187 | 2392 | public function getBlockStartParsers(): iterable |
|
| 188 | { |
||
| 189 | 2392 | if (! $this->extensionsInitialized) { |
|
| 190 | 22 | $this->initializeExtensions(); |
|
| 191 | } |
||
| 192 | |||
| 193 | 2392 | return $this->blockStartParsers->getIterator(); |
|
| 194 | } |
||
| 195 | |||
| 196 | 2166 | public function getDelimiterProcessors(): DelimiterProcessorCollection |
|
| 197 | { |
||
| 198 | 2166 | if (! $this->extensionsInitialized) { |
|
| 199 | 4 | $this->initializeExtensions(); |
|
| 200 | } |
||
| 201 | |||
| 202 | 2166 | return $this->delimiterProcessors; |
|
| 203 | } |
||
| 204 | |||
| 205 | /** |
||
| 206 | * {@inheritDoc} |
||
| 207 | */ |
||
| 208 | 2386 | public function getRenderersForClass(string $nodeClass): iterable |
|
| 209 | { |
||
| 210 | 2386 | if (! $this->extensionsInitialized) { |
|
| 211 | 18 | $this->initializeExtensions(); |
|
| 212 | } |
||
| 213 | |||
| 214 | // If renderers are defined for this specific class, return them immediately |
||
| 215 | 2386 | if (isset($this->renderersByClass[$nodeClass])) { |
|
| 216 | 2380 | return $this->renderersByClass[$nodeClass]; |
|
| 217 | } |
||
| 218 | |||
| 219 | /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */ |
||
| 220 | 26 | while (\class_exists($parent ??= $nodeClass) && $parent = \get_parent_class($parent)) { |
|
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 221 | 22 | if (! isset($this->renderersByClass[$parent])) { |
|
| 222 | 4 | continue; |
|
| 223 | } |
||
| 224 | |||
| 225 | // "Cache" this result to avoid future loops |
||
| 226 | 20 | return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent]; |
|
| 227 | } |
||
| 228 | |||
| 229 | 6 | return []; |
|
| 230 | } |
||
| 231 | |||
| 232 | /** |
||
| 233 | * {@inheritDoc} |
||
| 234 | */ |
||
| 235 | 14 | public function getExtensions(): iterable |
|
| 236 | { |
||
| 237 | 14 | return $this->extensions; |
|
| 238 | } |
||
| 239 | |||
| 240 | /** |
||
| 241 | * Add a single extension |
||
| 242 | * |
||
| 243 | * @return $this |
||
| 244 | */ |
||
| 245 | 2420 | public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface |
|
| 246 | { |
||
| 247 | 2420 | $this->assertUninitialized('Failed to add extension.'); |
|
| 248 | |||
| 249 | 2418 | $this->extensions[] = $extension; |
|
| 250 | 2418 | $this->uninitializedExtensions[] = $extension; |
|
| 251 | |||
| 252 | 2418 | if ($extension instanceof ConfigurableExtensionInterface) { |
|
| 253 | 2414 | $extension->configureSchema($this->config); |
|
| 254 | } |
||
| 255 | |||
| 256 | 2418 | return $this; |
|
| 257 | } |
||
| 258 | |||
| 259 | 2452 | private function initializeExtensions(): void |
|
| 260 | { |
||
| 261 | // Initialize the slug normalizer |
||
| 262 | 2452 | $this->getSlugNormalizer(); |
|
| 263 | |||
| 264 | // Ask all extensions to register their components |
||
| 265 | 2452 | while (\count($this->uninitializedExtensions) > 0) { |
|
| 266 | 2386 | foreach ($this->uninitializedExtensions as $i => $extension) { |
|
| 267 | 2386 | $extension->register($this); |
|
| 268 | 2386 | unset($this->uninitializedExtensions[$i]); |
|
| 269 | } |
||
| 270 | } |
||
| 271 | |||
| 272 | 2446 | $this->extensionsInitialized = true; |
|
| 273 | |||
| 274 | // Create the special delimiter parser if any processors were registered |
||
| 275 | 2446 | if ($this->delimiterProcessors->count() > 0) { |
|
| 276 | 2382 | $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN); |
|
| 277 | } |
||
| 278 | } |
||
| 279 | |||
| 280 | 2498 | private function injectEnvironmentAndConfigurationIfNeeded(object $object): void |
|
| 281 | { |
||
| 282 | 2498 | if ($object instanceof EnvironmentAwareInterface) { |
|
| 283 | 2396 | $object->setEnvironment($this); |
|
| 284 | } |
||
| 285 | |||
| 286 | 2498 | if ($object instanceof ConfigurationAwareInterface) { |
|
| 287 | 2460 | $object->setConfiguration($this->config->reader()); |
|
| 288 | } |
||
| 289 | } |
||
| 290 | |||
| 291 | /** |
||
| 292 | * @deprecated Instantiate the environment and add the extension yourself |
||
| 293 | * |
||
| 294 | * @param array<string, mixed> $config |
||
| 295 | */ |
||
| 296 | 2 | public static function createCommonMarkEnvironment(array $config = []): Environment |
|
| 297 | { |
||
| 298 | 2 | $environment = new self($config); |
|
| 299 | 2 | $environment->addExtension(new CommonMarkCoreExtension()); |
|
| 300 | |||
| 301 | 2 | return $environment; |
|
| 302 | } |
||
| 303 | |||
| 304 | /** |
||
| 305 | * @deprecated Instantiate the environment and add the extension yourself |
||
| 306 | * |
||
| 307 | * @param array<string, mixed> $config |
||
| 308 | */ |
||
| 309 | 2 | public static function createGFMEnvironment(array $config = []): Environment |
|
| 310 | { |
||
| 311 | 2 | $environment = new self($config); |
|
| 312 | 2 | $environment->addExtension(new CommonMarkCoreExtension()); |
|
| 313 | 2 | $environment->addExtension(new GithubFlavoredMarkdownExtension()); |
|
| 314 | |||
| 315 | 2 | return $environment; |
|
| 316 | } |
||
| 317 | |||
| 318 | 2460 | public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface |
|
| 319 | { |
||
| 320 | 2460 | $this->assertUninitialized('Failed to add event listener.'); |
|
| 321 | |||
| 322 | 2460 | $this->listenerData->add(new ListenerData($eventClass, $listener), $priority); |
|
| 323 | |||
| 324 | 2460 | if (\is_object($listener)) { |
|
| 325 | 360 | $this->injectEnvironmentAndConfigurationIfNeeded($listener); |
|
| 326 | 2460 | } elseif (\is_array($listener) && \is_object($listener[0])) { |
|
| 327 | 2460 | $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]); |
|
| 328 | } |
||
| 329 | |||
| 330 | 2460 | return $this; |
|
| 331 | } |
||
| 332 | |||
| 333 | 2396 | public function dispatch(object $event): object |
|
| 334 | { |
||
| 335 | 2396 | if (! $this->extensionsInitialized) { |
|
| 336 | 2396 | $this->initializeExtensions(); |
|
| 337 | } |
||
| 338 | |||
| 339 | 2390 | if ($this->eventDispatcher !== null) { |
|
| 340 | 2 | return $this->eventDispatcher->dispatch($event); |
|
| 341 | } |
||
| 342 | |||
| 343 | 2388 | foreach ($this->getListenersForEvent($event) as $listener) { |
|
| 344 | 2382 | if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { |
|
| 345 | 2 | return $event; |
|
| 346 | } |
||
| 347 | |||
| 348 | 2382 | $listener($event); |
|
| 349 | } |
||
| 350 | |||
| 351 | 2382 | return $event; |
|
| 352 | } |
||
| 353 | |||
| 354 | 2 | public function setEventDispatcher(EventDispatcherInterface $dispatcher): void |
|
| 355 | { |
||
| 356 | 2 | $this->eventDispatcher = $dispatcher; |
|
| 357 | } |
||
| 358 | |||
| 359 | /** |
||
| 360 | * {@inheritDoc} |
||
| 361 | * |
||
| 362 | * @return iterable<callable> |
||
| 363 | */ |
||
| 364 | 2388 | public function getListenersForEvent(object $event): iterable |
|
| 365 | { |
||
| 366 | 2388 | foreach ($this->listenerData as $listenerData) { |
|
| 367 | \assert($listenerData instanceof ListenerData); |
||
| 368 | |||
| 369 | /** @psalm-suppress ArgumentTypeCoercion */ |
||
| 370 | 2386 | if (! \is_a($event, $listenerData->getEvent())) { |
|
| 371 | 2378 | continue; |
|
| 372 | } |
||
| 373 | |||
| 374 | 2382 | yield function (object $event) use ($listenerData) { |
|
| 375 | 2382 | if (! $this->extensionsInitialized) { |
|
| 376 | $this->initializeExtensions(); |
||
| 377 | } |
||
| 378 | |||
| 379 | 2382 | return \call_user_func($listenerData->getListener(), $event); |
|
| 380 | 2382 | }; |
|
| 381 | } |
||
| 382 | } |
||
| 383 | |||
| 384 | /** |
||
| 385 | * @return iterable<InlineParserInterface> |
||
| 386 | */ |
||
| 387 | 2382 | public function getInlineParsers(): iterable |
|
| 388 | { |
||
| 389 | 2382 | if (! $this->extensionsInitialized) { |
|
| 390 | 12 | $this->initializeExtensions(); |
|
| 391 | } |
||
| 392 | |||
| 393 | 2382 | return $this->inlineParsers->getIterator(); |
|
| 394 | } |
||
| 395 | |||
| 396 | 2466 | public function getSlugNormalizer(): TextNormalizerInterface |
|
| 397 | { |
||
| 398 | 2466 | if ($this->slugNormalizer === null) { |
|
| 399 | 2466 | $normalizer = $this->config->get('slug_normalizer/instance'); |
|
| 400 | \assert($normalizer instanceof TextNormalizerInterface); |
||
| 401 | 2464 | $this->injectEnvironmentAndConfigurationIfNeeded($normalizer); |
|
| 402 | |||
| 403 | 2464 | if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) { |
|
| 404 | 2462 | $normalizer = new UniqueSlugNormalizer($normalizer); |
|
| 405 | } |
||
| 406 | |||
| 407 | 2464 | if ($normalizer instanceof UniqueSlugNormalizer) { |
|
| 408 | 2462 | if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) { |
|
| 409 | 2460 | $this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000); |
|
| 410 | } |
||
| 411 | } |
||
| 412 | |||
| 413 | 2464 | $this->slugNormalizer = $normalizer; |
|
| 414 | } |
||
| 415 | |||
| 416 | 2464 | return $this->slugNormalizer; |
|
|
0 ignored issues
–
show
|
|||
| 417 | } |
||
| 418 | |||
| 419 | /** |
||
| 420 | * @throws AlreadyInitializedException |
||
| 421 | */ |
||
| 422 | 2498 | private function assertUninitialized(string $message): void |
|
| 423 | { |
||
| 424 | 2498 | if ($this->extensionsInitialized) { |
|
| 425 | 14 | throw new AlreadyInitializedException($message . ' Extensions have already been initialized.'); |
|
| 426 | } |
||
| 427 | } |
||
| 428 | |||
| 429 | 2644 | public static function createDefaultConfiguration(): Configuration |
|
| 430 | { |
||
| 431 | 2644 | return new Configuration([ |
|
| 432 | 2644 | 'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW), |
|
| 433 | 2644 | 'allow_unsafe_links' => Expect::bool(true), |
|
| 434 | 2644 | 'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX), |
|
| 435 | 2644 | 'max_delimiters_per_line' => Expect::type('int')->default(PHP_INT_MAX), |
|
| 436 | 2644 | 'renderer' => Expect::structure([ |
|
| 437 | 2644 | 'block_separator' => Expect::string("\n"), |
|
| 438 | 2644 | 'inner_separator' => Expect::string("\n"), |
|
| 439 | 2644 | 'soft_break' => Expect::string("\n"), |
|
| 440 | 2644 | ]), |
|
| 441 | 2644 | 'slug_normalizer' => Expect::structure([ |
|
| 442 | 2644 | 'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()), |
|
| 443 | 2644 | 'max_length' => Expect::int()->min(0)->default(255), |
|
| 444 | 2644 | 'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT), |
|
| 445 | 2644 | ]), |
|
| 446 | 2644 | ]); |
|
| 447 | } |
||
| 448 | } |
||
| 449 |