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\Parser\BlockParserInterface; 18use League\CommonMark\Block\Renderer\BlockRendererInterface; 19use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; 20use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; 21use League\CommonMark\Event\AbstractEvent; 22use League\CommonMark\Extension\CommonMarkCoreExtension; 23use League\CommonMark\Extension\ExtensionInterface; 24use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; 25use League\CommonMark\Inline\Parser\InlineParserInterface; 26use League\CommonMark\Inline\Renderer\InlineRendererInterface; 27use League\CommonMark\Util\Configuration; 28use League\CommonMark\Util\ConfigurationAwareInterface; 29use League\CommonMark\Util\PrioritizedList; 30 31final class Environment implements ConfigurableEnvironmentInterface 32{ 33 /** 34 * @var ExtensionInterface[] 35 */ 36 private $extensions = []; 37 38 /** 39 * @var ExtensionInterface[] 40 */ 41 private $uninitializedExtensions = []; 42 43 /** 44 * @var bool 45 */ 46 private $extensionsInitialized = false; 47 48 /** 49 * @var PrioritizedList<BlockParserInterface> 50 */ 51 private $blockParsers; 52 53 /** 54 * @var PrioritizedList<InlineParserInterface> 55 */ 56 private $inlineParsers; 57 58 /** 59 * @var array<string, PrioritizedList<InlineParserInterface>> 60 */ 61 private $inlineParsersByCharacter = []; 62 63 /** 64 * @var DelimiterProcessorCollection 65 */ 66 private $delimiterProcessors; 67 68 /** 69 * @var array<string, PrioritizedList<BlockRendererInterface>> 70 */ 71 private $blockRenderersByClass = []; 72 73 /** 74 * @var array<string, PrioritizedList<InlineRendererInterface>> 75 */ 76 private $inlineRenderersByClass = []; 77 78 /** 79 * @var array<string, PrioritizedList<callable>> 80 */ 81 private $listeners = []; 82 83 /** 84 * @var Configuration 85 */ 86 private $config; 87 88 /** 89 * @var string 90 */ 91 private $inlineParserCharacterRegex; 92 93 /** 94 * @param array<string, mixed> $config 95 */ 96 public function __construct(array $config = []) 97 { 98 $this->config = new Configuration($config); 99 100 $this->blockParsers = new PrioritizedList(); 101 $this->inlineParsers = new PrioritizedList(); 102 $this->delimiterProcessors = new DelimiterProcessorCollection(); 103 } 104 105 public function mergeConfig(array $config = []) 106 { 107 if (\func_num_args() === 0) { 108 @\trigger_error('Calling Environment::mergeConfig() without any parameters is deprecated in league/commonmark 1.6 and will not be allowed in 2.0', \E_USER_DEPRECATED); 109 } 110 111 $this->assertUninitialized('Failed to modify configuration.'); 112 113 $this->config->merge($config); 114 } 115 116 public function setConfig(array $config = []) 117 { 118 @\trigger_error('The Environment::setConfig() method is deprecated in league/commonmark 1.6 and will be removed in 2.0. Use mergeConfig() instead.', \E_USER_DEPRECATED); 119 120 $this->assertUninitialized('Failed to modify configuration.'); 121 122 $this->config->replace($config); 123 } 124 125 public function getConfig($key = null, $default = null) 126 { 127 return $this->config->get($key, $default); 128 } 129 130 public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface 131 { 132 $this->assertUninitialized('Failed to add block parser.'); 133 134 $this->blockParsers->add($parser, $priority); 135 $this->injectEnvironmentAndConfigurationIfNeeded($parser); 136 137 return $this; 138 } 139 140 public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface 141 { 142 $this->assertUninitialized('Failed to add inline parser.'); 143 144 $this->inlineParsers->add($parser, $priority); 145 $this->injectEnvironmentAndConfigurationIfNeeded($parser); 146 147 foreach ($parser->getCharacters() as $character) { 148 if (!isset($this->inlineParsersByCharacter[$character])) { 149 $this->inlineParsersByCharacter[$character] = new PrioritizedList(); 150 } 151 152 $this->inlineParsersByCharacter[$character]->add($parser, $priority); 153 } 154 155 return $this; 156 } 157 158 public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface 159 { 160 $this->assertUninitialized('Failed to add delimiter processor.'); 161 $this->delimiterProcessors->add($processor); 162 $this->injectEnvironmentAndConfigurationIfNeeded($processor); 163 164 return $this; 165 } 166 167 public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface 168 { 169 $this->assertUninitialized('Failed to add block renderer.'); 170 171 if (!isset($this->blockRenderersByClass[$blockClass])) { 172 $this->blockRenderersByClass[$blockClass] = new PrioritizedList(); 173 } 174 175 $this->blockRenderersByClass[$blockClass]->add($blockRenderer, $priority); 176 $this->injectEnvironmentAndConfigurationIfNeeded($blockRenderer); 177 178 return $this; 179 } 180 181 public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface 182 { 183 $this->assertUninitialized('Failed to add inline renderer.'); 184 185 if (!isset($this->inlineRenderersByClass[$inlineClass])) { 186 $this->inlineRenderersByClass[$inlineClass] = new PrioritizedList(); 187 } 188 189 $this->inlineRenderersByClass[$inlineClass]->add($renderer, $priority); 190 $this->injectEnvironmentAndConfigurationIfNeeded($renderer); 191 192 return $this; 193 } 194 195 public function getBlockParsers(): iterable 196 { 197 if (!$this->extensionsInitialized) { 198 $this->initializeExtensions(); 199 } 200 201 return $this->blockParsers->getIterator(); 202 } 203 204 public function getInlineParsersForCharacter(string $character): iterable 205 { 206 if (!$this->extensionsInitialized) { 207 $this->initializeExtensions(); 208 } 209 210 if (!isset($this->inlineParsersByCharacter[$character])) { 211 return []; 212 } 213 214 return $this->inlineParsersByCharacter[$character]->getIterator(); 215 } 216 217 public function getDelimiterProcessors(): DelimiterProcessorCollection 218 { 219 if (!$this->extensionsInitialized) { 220 $this->initializeExtensions(); 221 } 222 223 return $this->delimiterProcessors; 224 } 225 226 public function getBlockRenderersForClass(string $blockClass): iterable 227 { 228 if (!$this->extensionsInitialized) { 229 $this->initializeExtensions(); 230 } 231 232 return $this->getRenderersByClass($this->blockRenderersByClass, $blockClass, BlockRendererInterface::class); 233 } 234 235 public function getInlineRenderersForClass(string $inlineClass): iterable 236 { 237 if (!$this->extensionsInitialized) { 238 $this->initializeExtensions(); 239 } 240 241 return $this->getRenderersByClass($this->inlineRenderersByClass, $inlineClass, InlineRendererInterface::class); 242 } 243 244 /** 245 * Get all registered extensions 246 * 247 * @return ExtensionInterface[] 248 */ 249 public function getExtensions(): iterable 250 { 251 return $this->extensions; 252 } 253 254 /** 255 * Add a single extension 256 * 257 * @param ExtensionInterface $extension 258 * 259 * @return $this 260 */ 261 public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface 262 { 263 $this->assertUninitialized('Failed to add extension.'); 264 265 $this->extensions[] = $extension; 266 $this->uninitializedExtensions[] = $extension; 267 268 return $this; 269 } 270 271 private function initializeExtensions(): void 272 { 273 // Ask all extensions to register their components 274 while (!empty($this->uninitializedExtensions)) { 275 foreach ($this->uninitializedExtensions as $i => $extension) { 276 $extension->register($this); 277 unset($this->uninitializedExtensions[$i]); 278 } 279 } 280 281 $this->extensionsInitialized = true; 282 283 // Lastly, let's build a regex which matches non-inline characters 284 // This will enable a huge performance boost with inline parsing 285 $this->buildInlineParserCharacterRegex(); 286 } 287 288 /** 289 * @param object $object 290 */ 291 private function injectEnvironmentAndConfigurationIfNeeded($object): void 292 { 293 if ($object instanceof EnvironmentAwareInterface) { 294 $object->setEnvironment($this); 295 } 296 297 if ($object instanceof ConfigurationAwareInterface) { 298 $object->setConfiguration($this->config); 299 } 300 } 301 302 public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface 303 { 304 $environment = new static(); 305 $environment->addExtension(new CommonMarkCoreExtension()); 306 $environment->mergeConfig([ 307 'renderer' => [ 308 'block_separator' => "\n", 309 'inner_separator' => "\n", 310 'soft_break' => "\n", 311 ], 312 'html_input' => self::HTML_INPUT_ALLOW, 313 'allow_unsafe_links' => true, 314 'max_nesting_level' => \PHP_INT_MAX, 315 ]); 316 317 return $environment; 318 } 319 320 public static function createGFMEnvironment(): ConfigurableEnvironmentInterface 321 { 322 $environment = self::createCommonMarkEnvironment(); 323 $environment->addExtension(new GithubFlavoredMarkdownExtension()); 324 325 return $environment; 326 } 327 328 public function getInlineParserCharacterRegex(): string 329 { 330 return $this->inlineParserCharacterRegex; 331 } 332 333 public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface 334 { 335 $this->assertUninitialized('Failed to add event listener.'); 336 337 if (!isset($this->listeners[$eventClass])) { 338 $this->listeners[$eventClass] = new PrioritizedList(); 339 } 340 341 $this->listeners[$eventClass]->add($listener, $priority); 342 343 if (\is_object($listener)) { 344 $this->injectEnvironmentAndConfigurationIfNeeded($listener); 345 } elseif (\is_array($listener) && \is_object($listener[0])) { 346 $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]); 347 } 348 349 return $this; 350 } 351 352 public function dispatch(AbstractEvent $event): void 353 { 354 if (!$this->extensionsInitialized) { 355 $this->initializeExtensions(); 356 } 357 358 $type = \get_class($event); 359 360 foreach ($this->listeners[$type] ?? [] as $listener) { 361 if ($event->isPropagationStopped()) { 362 return; 363 } 364 365 $listener($event); 366 } 367 } 368 369 private function buildInlineParserCharacterRegex(): void 370 { 371 $chars = \array_unique(\array_merge( 372 \array_keys($this->inlineParsersByCharacter), 373 $this->delimiterProcessors->getDelimiterCharacters() 374 )); 375 376 if (empty($chars)) { 377 // If no special inline characters exist then parse the whole line 378 $this->inlineParserCharacterRegex = '/^.+$/'; 379 } else { 380 // Match any character which inline parsers are not interested in 381 $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/'; 382 383 // Only add the u modifier (which slows down performance) if we have a multi-byte UTF-8 character in our regex 384 if (\strlen($this->inlineParserCharacterRegex) > \mb_strlen($this->inlineParserCharacterRegex)) { 385 $this->inlineParserCharacterRegex .= 'u'; 386 } 387 } 388 } 389 390 /** 391 * @param string $message 392 * 393 * @throws \RuntimeException 394 */ 395 private function assertUninitialized(string $message): void 396 { 397 if ($this->extensionsInitialized) { 398 throw new \RuntimeException($message . ' Extensions have already been initialized.'); 399 } 400 } 401 402 /** 403 * @param array<string, PrioritizedList> $list 404 * @param string $class 405 * @param string $type 406 * 407 * @return iterable 408 * 409 * @phpstan-template T 410 * 411 * @phpstan-param array<string, PrioritizedList<T>> $list 412 * @phpstan-param string $class 413 * @phpstan-param class-string<T> $type 414 * 415 * @phpstan-return iterable<T> 416 */ 417 private function getRenderersByClass(array &$list, string $class, string $type): iterable 418 { 419 // If renderers are defined for this specific class, return them immediately 420 if (isset($list[$class])) { 421 return $list[$class]; 422 } 423 424 while (\class_exists($parent = $parent ?? $class) && $parent = \get_parent_class($parent)) { 425 if (!isset($list[$parent])) { 426 continue; 427 } 428 429 // "Cache" this result to avoid future loops 430 return $list[$class] = $list[$parent]; 431 } 432 433 return []; 434 } 435} 436