Passed
Push — latest ( dcbb7e...fec122 )
by Colin
01:59
created

src/Environment/Environment.php (1 issue)

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\Configuration\Configuration;
20
use League\CommonMark\Configuration\ConfigurationAwareInterface;
21
use League\CommonMark\Delimiter\DelimiterParser;
22
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
23
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
24
use League\CommonMark\Event\ListenerData;
25
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
26
use League\CommonMark\Extension\ExtensionInterface;
27
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
28
use League\CommonMark\Parser\Block\BlockStartParserInterface;
29
use League\CommonMark\Parser\Inline\InlineParserInterface;
30
use League\CommonMark\Renderer\NodeRendererInterface;
31
use League\CommonMark\Util\HtmlFilter;
32
use League\CommonMark\Util\PrioritizedList;
33
use Psr\EventDispatcher\EventDispatcherInterface;
34
use Psr\EventDispatcher\ListenerProviderInterface;
35
use Psr\EventDispatcher\StoppableEventInterface;
36
37
final class Environment implements ConfigurableEnvironmentInterface, ListenerProviderInterface
38
{
39
    /**
40
     * @var ExtensionInterface[]
41
     *
42
     * @psalm-readonly-allow-private-mutation
43
     */
44
    private $extensions = [];
45
46
    /**
47
     * @var ExtensionInterface[]
48
     *
49
     * @psalm-readonly-allow-private-mutation
50
     */
51
    private $uninitializedExtensions = [];
52
53
    /**
54
     * @var bool
55
     *
56
     * @psalm-readonly-allow-private-mutation
57
     */
58
    private $extensionsInitialized = false;
59
60
    /**
61
     * @var PrioritizedList<BlockStartParserInterface>
62
     *
63
     * @psalm-readonly
64
     */
65
    private $blockStartParsers;
66
67
    /**
68
     * @var PrioritizedList<InlineParserInterface>
69
     *
70
     * @psalm-readonly
71
     */
72
    private $inlineParsers;
73
74
    /**
75
     * @var DelimiterProcessorCollection
76
     *
77
     * @psalm-readonly
78
     */
79
    private $delimiterProcessors;
80
81
    /**
82
     * @var array<string, PrioritizedList<NodeRendererInterface>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, Prioritize...NodeRendererInterface>> at position 4 could not be parsed: Expected '>' at position 4, but found 'PrioritizedList'.
Loading history...
83
     *
84
     * @psalm-readonly-allow-private-mutation
85
     */
86
    private $renderersByClass = [];
87
88
    /**
89
     * @var PrioritizedList<ListenerData>
90
     *
91
     * @psalm-readonly-allow-private-mutation
92
     */
93
    private $listenerData;
94
95
    /** @var EventDispatcherInterface|null */
96
    private $eventDispatcher;
97
98
    /**
99
     * @var Configuration
100
     *
101
     * @psalm-readonly
102
     */
103
    private $config;
104
105
    /**
106
     * @param array<string, mixed> $config
107
     */
108 3015
    public function __construct(array $config = [])
109
    {
110 3015
        $this->config = new Configuration($config);
111
112 3015
        $this->blockStartParsers   = new PrioritizedList();
113 3015
        $this->inlineParsers       = new PrioritizedList();
114 3015
        $this->listenerData        = new PrioritizedList();
115 3015
        $this->delimiterProcessors = new DelimiterProcessorCollection();
116 3015
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 2916
    public function mergeConfig(array $config = []): void
122
    {
123 2916
        $this->assertUninitialized('Failed to modify configuration.');
124
125 2913
        $this->config->merge($config);
126 2913
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 24
    public function setConfig(array $config = []): void
132
    {
133 24
        $this->assertUninitialized('Failed to modify configuration.');
134
135 21
        $this->config->replace($config);
136 21
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 2922
    public function getConfig(?string $key = null, $default = null)
142
    {
143 2922
        return $this->config->get($key, $default);
144
    }
145
146 2904
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
147
    {
148 2904
        $this->assertUninitialized('Failed to add block start parser.');
149
150 2901
        $this->blockStartParsers->add($parser, $priority);
151 2901
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
152
153 2901
        return $this;
154
    }
155
156 2910
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
157
    {
158 2910
        $this->assertUninitialized('Failed to add inline parser.');
159
160 2907
        $this->inlineParsers->add($parser, $priority);
161 2907
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
162
163 2907
        return $this;
164
    }
165
166 2907
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
167
    {
168 2907
        $this->assertUninitialized('Failed to add delimiter processor.');
169 2904
        $this->delimiterProcessors->add($processor);
170 2904
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
171
172 2904
        return $this;
173
    }
174
175 2925
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
176
    {
177 2925
        $this->assertUninitialized('Failed to add renderer.');
178
179 2922
        if (! isset($this->renderersByClass[$nodeClass])) {
180 2922
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
181
        }
182
183 2922
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
184 2922
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
185
186 2922
        return $this;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 2271
    public function getBlockStartParsers(): iterable
193
    {
194 2271
        if (! $this->extensionsInitialized) {
195 33
            $this->initializeExtensions();
196
        }
197
198 2271
        return $this->blockStartParsers->getIterator();
199
    }
200
201 2598
    public function getDelimiterProcessors(): DelimiterProcessorCollection
202
    {
203 2598
        if (! $this->extensionsInitialized) {
204 6
            $this->initializeExtensions();
205
        }
206
207 2598
        return $this->delimiterProcessors;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 2904
    public function getRenderersForClass(string $nodeClass): iterable
214
    {
215 2904
        if (! $this->extensionsInitialized) {
216 33
            $this->initializeExtensions();
217
        }
218
219
        // If renderers are defined for this specific class, return them immediately
220 2904
        if (isset($this->renderersByClass[$nodeClass])) {
221 2895
            return $this->renderersByClass[$nodeClass];
222
        }
223
224
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
225 66
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
226 60
            if (! isset($this->renderersByClass[$parent])) {
227 6
                continue;
228
            }
229
230
            // "Cache" this result to avoid future loops
231 57
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
232
        }
233
234 9
        return [];
235
    }
236
237
    /**
238
     * Get all registered extensions
239
     *
240
     * @return ExtensionInterface[]
241
     */
242 12
    public function getExtensions(): iterable
243
    {
244 12
        return $this->extensions;
245
    }
246
247
    /**
248
     * Add a single extension
249
     *
250
     * @return $this
251
     */
252 2919
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
253
    {
254 2919
        $this->assertUninitialized('Failed to add extension.');
255
256 2916
        $this->extensions[]              = $extension;
257 2916
        $this->uninitializedExtensions[] = $extension;
258
259 2916
        return $this;
260
    }
261
262 2985
    private function initializeExtensions(): void
263
    {
264
        // Ask all extensions to register their components
265 2985
        while (\count($this->uninitializedExtensions) > 0) {
266 2898
            foreach ($this->uninitializedExtensions as $i => $extension) {
267 2898
                $extension->register($this);
268 2898
                unset($this->uninitializedExtensions[$i]);
269
            }
270
        }
271
272 2976
        $this->extensionsInitialized = true;
273
274
        // Create the special delimiter parser if any processors were registered
275 2976
        if ($this->delimiterProcessors->count() > 0) {
276 2892
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
277
        }
278 2976
    }
279
280 2961
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
281
    {
282 2961
        if ($object instanceof EnvironmentAwareInterface) {
283 2910
            $object->setEnvironment($this);
284
        }
285
286 2961
        if ($object instanceof ConfigurationAwareInterface) {
287 2910
            $object->setConfiguration($this->config);
288
        }
289 2961
    }
290
291 2907
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
292
    {
293 2907
        $environment = new static();
294 2907
        $environment->addExtension(new CommonMarkCoreExtension());
295 2907
        $environment->mergeConfig([
296 969
            'renderer' => [
297 1938
                'block_separator' => "\n",
298
                'inner_separator' => "\n",
299
                'soft_break'      => "\n",
300
            ],
301 2907
            'html_input'         => HtmlFilter::ALLOW,
302
            'allow_unsafe_links' => true,
303
            'max_nesting_level'  => \INF,
304
        ]);
305
306 2907
        return $environment;
307
    }
308
309 150
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
310
    {
311 150
        $environment = self::createCommonMarkEnvironment();
312 150
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
313
314 150
        return $environment;
315
    }
316
317 426
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
318
    {
319 426
        $this->assertUninitialized('Failed to add event listener.');
320
321 423
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
322
323 423
        if (\is_object($listener)) {
324 300
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
325 162
        } elseif (\is_array($listener) && \is_object($listener[0])) {
326 162
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
327
        }
328
329 423
        return $this;
330
    }
331
332
    /**
333
     * {@inheritDoc}
334
     */
335 2907
    public function dispatch(object $event)
336
    {
337 2907
        if (! $this->extensionsInitialized) {
338 2901
            $this->initializeExtensions();
339
        }
340
341 2898
        if ($this->eventDispatcher !== null) {
342 3
            return $this->eventDispatcher->dispatch($event);
343
        }
344
345 2895
        foreach ($this->getListenersForEvent($event) as $listener) {
346 417
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
347 3
                return $event;
348
            }
349
350 417
            $listener($event);
351
        }
352
353 2889
        return $event;
354
    }
355
356 3
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
357
    {
358 3
        $this->eventDispatcher = $dispatcher;
359 3
    }
360
361
    /**
362
     * {@inheritDoc}
363
     *
364
     * @return iterable<callable>
365
     */
366 2895
    public function getListenersForEvent(object $event): iterable
367
    {
368 2895
        foreach ($this->listenerData as $listenerData) {
369
            \assert($listenerData instanceof ListenerData);
370
371 417
            if (! \is_a($event, $listenerData->getEvent())) {
372 408
                continue;
373
            }
374
375 278
            yield function (object $event) use ($listenerData) {
376 417
                if (! $this->extensionsInitialized) {
377
                    $this->initializeExtensions();
378
                }
379
380 417
                return \call_user_func($listenerData->getListener(), $event);
381 417
            };
382
        }
383 2889
    }
384
385
    /**
386
     * @return iterable<InlineParserInterface>
387
     */
388 2892
    public function getInlineParsers(): iterable
389
    {
390 2892
        if (! $this->extensionsInitialized) {
391 12
            $this->initializeExtensions();
392
        }
393
394 2892
        return $this->inlineParsers->getIterator();
395
    }
396
397
    /**
398
     * @throws \RuntimeException
399
     */
400 3009
    private function assertUninitialized(string $message): void
401
    {
402 3009
        if ($this->extensionsInitialized) {
403 24
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
404
        }
405 2985
    }
406
}
407