Completed
Push — latest ( da8ccb...1f5488 )
by Colin
15s queued 12s
created

Environment::dispatch()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 8
nop 1
crap 6
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\Configuration\ConfigurationInterface;
22
use League\CommonMark\Delimiter\DelimiterParser;
23
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
24
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
25
use League\CommonMark\Event\DocumentParsedEvent;
26
use League\CommonMark\Event\ListenerData;
27
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
28
use League\CommonMark\Extension\ConfigurableExtensionInterface;
29
use League\CommonMark\Extension\ExtensionInterface;
30
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
31
use League\CommonMark\Normalizer\SlugNormalizer;
32
use League\CommonMark\Normalizer\TextNormalizerInterface;
33
use League\CommonMark\Normalizer\UniqueSlugNormalizer;
34
use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface;
35
use League\CommonMark\Parser\Block\BlockStartParserInterface;
36
use League\CommonMark\Parser\Inline\InlineParserInterface;
37
use League\CommonMark\Renderer\NodeRendererInterface;
38
use League\CommonMark\Util\HtmlFilter;
39
use League\CommonMark\Util\PrioritizedList;
40
use Nette\Schema\Expect;
41
use Psr\EventDispatcher\EventDispatcherInterface;
42
use Psr\EventDispatcher\ListenerProviderInterface;
43
use Psr\EventDispatcher\StoppableEventInterface;
44
45
final class Environment implements EnvironmentInterface, EnvironmentBuilderInterface, ListenerProviderInterface
46
{
47
    /**
48
     * @var ExtensionInterface[]
49
     *
50
     * @psalm-readonly-allow-private-mutation
51
     */
52
    private $extensions = [];
53
54
    /**
55
     * @var ExtensionInterface[]
56
     *
57
     * @psalm-readonly-allow-private-mutation
58
     */
59
    private $uninitializedExtensions = [];
60
61
    /**
62
     * @var bool
63
     *
64
     * @psalm-readonly-allow-private-mutation
65
     */
66
    private $extensionsInitialized = false;
67
68
    /**
69
     * @var PrioritizedList<BlockStartParserInterface>
70
     *
71
     * @psalm-readonly
72
     */
73
    private $blockStartParsers;
74
75
    /**
76
     * @var PrioritizedList<InlineParserInterface>
77
     *
78
     * @psalm-readonly
79
     */
80
    private $inlineParsers;
81
82
    /**
83
     * @var DelimiterProcessorCollection
84
     *
85
     * @psalm-readonly
86
     */
87
    private $delimiterProcessors;
88
89
    /**
90
     * @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...
91
     *
92
     * @psalm-readonly-allow-private-mutation
93
     */
94
    private $renderersByClass = [];
95
96
    /**
97
     * @var PrioritizedList<ListenerData>
98
     *
99
     * @psalm-readonly-allow-private-mutation
100
     */
101
    private $listenerData;
102
103
    /** @var EventDispatcherInterface|null */
104
    private $eventDispatcher;
105
106
    /**
107
     * @var Configuration
108
     *
109
     * @psalm-readonly
110
     */
111
    private $config;
112
113
    /** @var TextNormalizerInterface|null */
114
    private $slugNormalizer = null;
115
116
    /**
117
     * @param array<string, mixed> $config
118
     */
119 3108
    public function __construct(array $config = [])
120
    {
121 3108
        $this->config = self::createDefaultConfiguration();
122 3108
        $this->config->merge($config);
123
124 3108
        $this->blockStartParsers   = new PrioritizedList();
125 3108
        $this->inlineParsers       = new PrioritizedList();
126 3108
        $this->listenerData        = new PrioritizedList();
127 3108
        $this->delimiterProcessors = new DelimiterProcessorCollection();
128 3108
    }
129
130 2997
    public function getConfiguration(): ConfigurationInterface
131
    {
132 2997
        return $this->config->reader();
133
    }
134
135
    /**
136
     * @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.
137
     *
138
     * @param array<string, mixed> $config
139
     */
140 306
    public function mergeConfig(array $config): void
141
    {
142 306
        @\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);
143
144 306
        $this->assertUninitialized('Failed to modify configuration.');
145
146 303
        $this->config->merge($config);
147 303
    }
148
149 2955
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
150
    {
151 2955
        $this->assertUninitialized('Failed to add block start parser.');
152
153 2952
        $this->blockStartParsers->add($parser, $priority);
154 2952
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
155
156 2952
        return $this;
157
    }
158
159 2961
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
160
    {
161 2961
        $this->assertUninitialized('Failed to add inline parser.');
162
163 2958
        $this->inlineParsers->add($parser, $priority);
164 2958
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
165
166 2958
        return $this;
167
    }
168
169 2958
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
170
    {
171 2958
        $this->assertUninitialized('Failed to add delimiter processor.');
172 2955
        $this->delimiterProcessors->add($processor);
173 2955
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
174
175 2955
        return $this;
176
    }
177
178 2976
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface
179
    {
180 2976
        $this->assertUninitialized('Failed to add renderer.');
181
182 2973
        if (! isset($this->renderersByClass[$nodeClass])) {
183 2973
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
184
        }
185
186 2973
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
187 2973
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
188
189 2973
        return $this;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 2328
    public function getBlockStartParsers(): iterable
196
    {
197 2328
        if (! $this->extensionsInitialized) {
198 30
            $this->initializeExtensions();
199
        }
200
201 2328
        return $this->blockStartParsers->getIterator();
202
    }
203
204 2658
    public function getDelimiterProcessors(): DelimiterProcessorCollection
205
    {
206 2658
        if (! $this->extensionsInitialized) {
207 6
            $this->initializeExtensions();
208
        }
209
210 2658
        return $this->delimiterProcessors;
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216 2967
    public function getRenderersForClass(string $nodeClass): iterable
217
    {
218 2967
        if (! $this->extensionsInitialized) {
219 33
            $this->initializeExtensions();
220
        }
221
222
        // If renderers are defined for this specific class, return them immediately
223 2967
        if (isset($this->renderersByClass[$nodeClass])) {
224 2958
            return $this->renderersByClass[$nodeClass];
225
        }
226
227
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
228 75
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
229 69
            if (! isset($this->renderersByClass[$parent])) {
230 6
                continue;
231
            }
232
233
            // "Cache" this result to avoid future loops
234 66
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
235
        }
236
237 9
        return [];
238
    }
239
240
    /**
241
     * {@inheritDoc}
242
     */
243 18
    public function getExtensions(): iterable
244
    {
245 18
        return $this->extensions;
246
    }
247
248
    /**
249
     * Add a single extension
250
     *
251
     * @return $this
252
     */
253 3006
    public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
254
    {
255 3006
        $this->assertUninitialized('Failed to add extension.');
256
257 3003
        $this->extensions[]              = $extension;
258 3003
        $this->uninitializedExtensions[] = $extension;
259
260 3003
        if ($extension instanceof ConfigurableExtensionInterface) {
261 2997
            $extension->configureSchema($this->config);
262
        }
263
264 3003
        return $this;
265
    }
266
267 3039
    private function initializeExtensions(): void
268
    {
269
        // Initialize the slug normalizer
270 3039
        $this->getSlugNormalizer();
271
272
        // Ask all extensions to register their components
273 3039
        while (\count($this->uninitializedExtensions) > 0) {
274 2949
            foreach ($this->uninitializedExtensions as $i => $extension) {
275 2949
                $extension->register($this);
276 2949
                unset($this->uninitializedExtensions[$i]);
277
            }
278
        }
279
280 3039
        $this->extensionsInitialized = true;
281
282
        // Create the special delimiter parser if any processors were registered
283 3039
        if ($this->delimiterProcessors->count() > 0) {
284 2952
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
285
        }
286 3039
    }
287
288 3060
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
289
    {
290 3060
        if ($object instanceof EnvironmentAwareInterface) {
291 2961
            $object->setEnvironment($this);
292
        }
293
294 3060
        if ($object instanceof ConfigurationAwareInterface) {
295 3051
            $object->setConfiguration($this->config->reader());
296
        }
297 3060
    }
298
299
    /**
300
     * @deprecated Instantiate the environment and add the extension yourself
301
     *
302
     * @param array<string, mixed> $config
303
     */
304 756
    public static function createCommonMarkEnvironment(array $config = []): Environment
305
    {
306 756
        $environment = new self($config);
307 756
        $environment->addExtension(new CommonMarkCoreExtension());
308
309 756
        return $environment;
310
    }
311
312
    /**
313
     * @deprecated Instantiate the environment and add the extension yourself
314
     *
315
     * @param array<string, mixed> $config
316
     */
317 81
    public static function createGFMEnvironment(array $config = []): Environment
318
    {
319 81
        $environment = self::createCommonMarkEnvironment($config);
0 ignored issues
show
Deprecated Code introduced by
The function League\CommonMark\Enviro...CommonMarkEnvironment() has been deprecated: Instantiate the environment and add the extension yourself ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

319
        $environment = /** @scrutinizer ignore-deprecated */ self::createCommonMarkEnvironment($config);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
320 81
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
321
322 81
        return $environment;
323
    }
324
325 3051
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
326
    {
327 3051
        $this->assertUninitialized('Failed to add event listener.');
328
329 3051
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
330
331 3051
        if (\is_object($listener)) {
332 303
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
333 3051
        } elseif (\is_array($listener) && \is_object($listener[0])) {
334 3051
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
335
        }
336
337 3051
        return $this;
338
    }
339
340
    /**
341
     * {@inheritDoc}
342
     */
343 2964
    public function dispatch(object $event)
344
    {
345 2964
        if (! $this->extensionsInitialized) {
346 2958
            $this->initializeExtensions();
347
        }
348
349 2964
        if ($this->eventDispatcher !== null) {
350 3
            return $this->eventDispatcher->dispatch($event);
351
        }
352
353 2961
        foreach ($this->getListenersForEvent($event) as $listener) {
354 2952
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
355 3
                return $event;
356
            }
357
358 2952
            $listener($event);
359
        }
360
361 2955
        return $event;
362
    }
363
364 3
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
365
    {
366 3
        $this->eventDispatcher = $dispatcher;
367 3
    }
368
369
    /**
370
     * {@inheritDoc}
371
     *
372
     * @return iterable<callable>
373
     */
374 2961
    public function getListenersForEvent(object $event): iterable
375
    {
376 2961
        foreach ($this->listenerData as $listenerData) {
377
            \assert($listenerData instanceof ListenerData);
378
379 2958
            if (! \is_a($event, $listenerData->getEvent())) {
380 2949
                continue;
381
            }
382
383 1968
            yield function (object $event) use ($listenerData) {
384 2952
                if (! $this->extensionsInitialized) {
385
                    $this->initializeExtensions();
386
                }
387
388 2952
                return \call_user_func($listenerData->getListener(), $event);
389 2952
            };
390
        }
391 2955
    }
392
393
    /**
394
     * @return iterable<InlineParserInterface>
395
     */
396 2952
    public function getInlineParsers(): iterable
397
    {
398 2952
        if (! $this->extensionsInitialized) {
399 12
            $this->initializeExtensions();
400
        }
401
402 2952
        return $this->inlineParsers->getIterator();
403
    }
404
405 3060
    public function getSlugNormalizer(): TextNormalizerInterface
406
    {
407 3060
        if ($this->slugNormalizer === null) {
408 3060
            $normalizer = $this->config->get('slug_normalizer/instance');
409
            \assert($normalizer instanceof TextNormalizerInterface);
410 3057
            $this->injectEnvironmentAndConfigurationIfNeeded($normalizer);
411
412 3057
            if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) {
413 3054
                $normalizer = new UniqueSlugNormalizer($normalizer);
414
            }
415
416 3057
            if ($normalizer instanceof UniqueSlugNormalizer) {
417 3054
                if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) {
418 3051
                    $this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000);
419
                }
420
            }
421
422 3057
            $this->slugNormalizer = $normalizer;
423
        }
424
425 3057
        return $this->slugNormalizer;
426
    }
427
428
    /**
429
     * @throws \RuntimeException
430
     */
431 3096
    private function assertUninitialized(string $message): void
432
    {
433 3096
        if ($this->extensionsInitialized) {
434 21
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
435
        }
436 3096
    }
437
438 3330
    public static function createDefaultConfiguration(): Configuration
439
    {
440 3330
        return new Configuration([
441 3330
            'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
442 3330
            'allow_unsafe_links' => Expect::bool(true),
443 3330
            'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
444 3330
            'renderer' => Expect::structure([
445 3330
                'block_separator' => Expect::string("\n"),
446 3330
                'inner_separator' => Expect::string("\n"),
447 3330
                'soft_break' => Expect::string("\n"),
448 3330
            ])->castTo('array'),
449 3330
            'slug_normalizer' => Expect::structure([
450 3330
                'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()),
451 3330
                'max_length' => Expect::int()->min(0)->default(255),
452 3330
                'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT),
453 3330
            ])->castTo('array'),
454
        ]);
455
    }
456
}
457