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