Completed
Push — latest ( 76d169...995567 )
by Colin
22s queued 10s
created

Environment::mergeConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 Colin O'Dell
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 3003
    public function __construct(array $config = [])
109
    {
110 3003
        $this->config = new Configuration($config);
111
112 3003
        $this->blockStartParsers   = new PrioritizedList();
113 3003
        $this->inlineParsers       = new PrioritizedList();
114 3003
        $this->listenerData        = new PrioritizedList();
115 3003
        $this->delimiterProcessors = new DelimiterProcessorCollection();
116 3003
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 2904
    public function mergeConfig(array $config = []): void
122
    {
123 2904
        $this->assertUninitialized('Failed to modify configuration.');
124
125 2901
        $this->config->merge($config);
126 2901
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 18
    public function setConfig(array $config = []): void
132
    {
133 18
        $this->assertUninitialized('Failed to modify configuration.');
134
135 15
        $this->config->replace($config);
136 15
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 2910
    public function getConfig(?string $key = null, $default = null)
142
    {
143 2910
        return $this->config->get($key, $default);
144
    }
145
146 2892
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
147
    {
148 2892
        $this->assertUninitialized('Failed to add block start parser.');
149
150 2889
        $this->blockStartParsers->add($parser, $priority);
151 2889
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
152
153 2889
        return $this;
154
    }
155
156 2898
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
157
    {
158 2898
        $this->assertUninitialized('Failed to add inline parser.');
159
160 2895
        $this->inlineParsers->add($parser, $priority);
161 2895
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
162
163 2895
        return $this;
164
    }
165
166 2895
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
167
    {
168 2895
        $this->assertUninitialized('Failed to add delimiter processor.');
169 2892
        $this->delimiterProcessors->add($processor);
170 2892
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
171
172 2892
        return $this;
173
    }
174
175 2913
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
176
    {
177 2913
        $this->assertUninitialized('Failed to add renderer.');
178
179 2910
        if (! isset($this->renderersByClass[$nodeClass])) {
180 2910
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
181
        }
182
183 2910
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
184 2910
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
185
186 2910
        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 2592
    public function getDelimiterProcessors(): DelimiterProcessorCollection
202
    {
203 2592
        if (! $this->extensionsInitialized) {
204 6
            $this->initializeExtensions();
205
        }
206
207 2592
        return $this->delimiterProcessors;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 2898
    public function getRenderersForClass(string $nodeClass): iterable
214
    {
215 2898
        if (! $this->extensionsInitialized) {
216 33
            $this->initializeExtensions();
217
        }
218
219
        // If renderers are defined for this specific class, return them immediately
220 2898
        if (isset($this->renderersByClass[$nodeClass])) {
221 2889
            return $this->renderersByClass[$nodeClass];
222
        }
223
224
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
225 60
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
226 54
            if (! isset($this->renderersByClass[$parent])) {
227 6
                continue;
228
            }
229
230
            // "Cache" this result to avoid future loops
231 51
            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 2907
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
253
    {
254 2907
        $this->assertUninitialized('Failed to add extension.');
255
256 2904
        $this->extensions[]              = $extension;
257 2904
        $this->uninitializedExtensions[] = $extension;
258
259 2904
        return $this;
260
    }
261
262 2973
    private function initializeExtensions(): void
263
    {
264
        // Ask all extensions to register their components
265 2973
        while (\count($this->uninitializedExtensions) > 0) {
266 2886
            foreach ($this->uninitializedExtensions as $i => $extension) {
267 2886
                $extension->register($this);
268 2886
                unset($this->uninitializedExtensions[$i]);
269
            }
270
        }
271
272 2970
        $this->extensionsInitialized = true;
273
274
        // Create the special delimiter parser if any processors were registered
275 2970
        if ($this->delimiterProcessors->count() > 0) {
276 2886
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
277
        }
278 2970
    }
279
280 2949
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
281
    {
282 2949
        if ($object instanceof EnvironmentAwareInterface) {
283 2898
            $object->setEnvironment($this);
284
        }
285
286 2949
        if ($object instanceof ConfigurationAwareInterface) {
287 2898
            $object->setConfiguration($this->config);
288
        }
289 2949
    }
290
291 2895
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
292
    {
293 2895
        $environment = new static();
294 2895
        $environment->addExtension(new CommonMarkCoreExtension());
295 2895
        $environment->mergeConfig([
296 965
            'renderer' => [
297 1930
                'block_separator' => "\n",
298
                'inner_separator' => "\n",
299
                'soft_break'      => "\n",
300
            ],
301 2895
            'html_input'         => HtmlFilter::ALLOW,
302
            'allow_unsafe_links' => true,
303
            'max_nesting_level'  => \INF,
304
        ]);
305
306 2895
        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 2895
    public function dispatch(object $event)
336
    {
337 2895
        if (! $this->extensionsInitialized) {
338 2889
            $this->initializeExtensions();
339
        }
340
341 2892
        if ($this->eventDispatcher !== null) {
342 3
            return $this->eventDispatcher->dispatch($event);
343
        }
344
345 2889
        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 2883
        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 2889
    public function getListenersForEvent(object $event): iterable
367
    {
368 2889
        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 2883
    }
384
385
    /**
386
     * @return iterable<InlineParserInterface>
387
     */
388 2886
    public function getInlineParsers(): iterable
389
    {
390 2886
        if (! $this->extensionsInitialized) {
391 12
            $this->initializeExtensions();
392
        }
393
394 2886
        return $this->inlineParsers->getIterator();
395
    }
396
397
    /**
398
     * @throws \RuntimeException
399
     */
400 2997
    private function assertUninitialized(string $message): void
401
    {
402 2997
        if ($this->extensionsInitialized) {
403 24
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
404
        }
405 2973
    }
406
}
407