Passed
Push — latest ( 7d037c...aae611 )
by Colin
02:38 queued 12s
created

Environment::setConfig()   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
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 3063
    public function __construct(array $config = [])
109
    {
110 3063
        $this->config = new Configuration($config);
111
112 3063
        $this->blockStartParsers   = new PrioritizedList();
113 3063
        $this->inlineParsers       = new PrioritizedList();
114 3063
        $this->listenerData        = new PrioritizedList();
115 3063
        $this->delimiterProcessors = new DelimiterProcessorCollection();
116 3063
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 2967
    public function mergeConfig(array $config): void
122
    {
123 2967
        $this->assertUninitialized('Failed to modify configuration.');
124
125 2964
        $this->config->merge($config);
126 2964
    }
127
128
    /**
129
     * {@inheritDoc}
130
     */
131 2973
    public function getConfig(string $key, $default = null)
132
    {
133 2973
        return $this->config->get($key, $default);
134
    }
135
136 2949
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
137
    {
138 2949
        $this->assertUninitialized('Failed to add block start parser.');
139
140 2946
        $this->blockStartParsers->add($parser, $priority);
141 2946
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
142
143 2946
        return $this;
144
    }
145
146 2955
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
147
    {
148 2955
        $this->assertUninitialized('Failed to add inline parser.');
149
150 2952
        $this->inlineParsers->add($parser, $priority);
151 2952
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
152
153 2952
        return $this;
154
    }
155
156 2952
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
157
    {
158 2952
        $this->assertUninitialized('Failed to add delimiter processor.');
159 2949
        $this->delimiterProcessors->add($processor);
160 2949
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
161
162 2949
        return $this;
163
    }
164
165 2970
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
166
    {
167 2970
        $this->assertUninitialized('Failed to add renderer.');
168
169 2967
        if (! isset($this->renderersByClass[$nodeClass])) {
170 2967
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
171
        }
172
173 2967
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
174 2967
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
175
176 2967
        return $this;
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 2313
    public function getBlockStartParsers(): iterable
183
    {
184 2313
        if (! $this->extensionsInitialized) {
185 30
            $this->initializeExtensions();
186
        }
187
188 2313
        return $this->blockStartParsers->getIterator();
189
    }
190
191 2643
    public function getDelimiterProcessors(): DelimiterProcessorCollection
192
    {
193 2643
        if (! $this->extensionsInitialized) {
194 6
            $this->initializeExtensions();
195
        }
196
197 2643
        return $this->delimiterProcessors;
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 2949
    public function getRenderersForClass(string $nodeClass): iterable
204
    {
205 2949
        if (! $this->extensionsInitialized) {
206 33
            $this->initializeExtensions();
207
        }
208
209
        // If renderers are defined for this specific class, return them immediately
210 2949
        if (isset($this->renderersByClass[$nodeClass])) {
211 2940
            return $this->renderersByClass[$nodeClass];
212
        }
213
214
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
215 72
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
216 66
            if (! isset($this->renderersByClass[$parent])) {
217 6
                continue;
218
            }
219
220
            // "Cache" this result to avoid future loops
221 63
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
222
        }
223
224 9
        return [];
225
    }
226
227
    /**
228
     * Get all registered extensions
229
     *
230
     * @return ExtensionInterface[]
231
     */
232 18
    public function getExtensions(): iterable
233
    {
234 18
        return $this->extensions;
235
    }
236
237
    /**
238
     * Add a single extension
239
     *
240
     * @return $this
241
     */
242 2973
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
243
    {
244 2973
        $this->assertUninitialized('Failed to add extension.');
245
246 2970
        $this->extensions[]              = $extension;
247 2970
        $this->uninitializedExtensions[] = $extension;
248
249 2970
        return $this;
250
    }
251
252 3027
    private function initializeExtensions(): void
253
    {
254
        // Ask all extensions to register their components
255 3027
        while (\count($this->uninitializedExtensions) > 0) {
256 2943
            foreach ($this->uninitializedExtensions as $i => $extension) {
257 2943
                $extension->register($this);
258 2943
                unset($this->uninitializedExtensions[$i]);
259
            }
260
        }
261
262 3018
        $this->extensionsInitialized = true;
263
264
        // Create the special delimiter parser if any processors were registered
265 3018
        if ($this->delimiterProcessors->count() > 0) {
266 2937
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
267
        }
268 3018
    }
269
270 3006
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
271
    {
272 3006
        if ($object instanceof EnvironmentAwareInterface) {
273 2955
            $object->setEnvironment($this);
274
        }
275
276 3006
        if ($object instanceof ConfigurationAwareInterface) {
277 2955
            $object->setConfiguration($this->config);
278
        }
279 3006
    }
280
281 2961
    public static function createCommonMarkEnvironment(): Environment
282
    {
283 2961
        $environment = new static();
284 2961
        $environment->addExtension(new CommonMarkCoreExtension());
285 2961
        $environment->mergeConfig([
286 987
            'renderer' => [
287 1974
                'block_separator' => "\n",
288
                'inner_separator' => "\n",
289
                'soft_break'      => "\n",
290
            ],
291 2961
            'html_input'         => HtmlFilter::ALLOW,
292
            'allow_unsafe_links' => true,
293
            'max_nesting_level'  => \PHP_INT_MAX,
294
        ]);
295
296 2961
        return $environment;
297
    }
298
299 159
    public static function createGFMEnvironment(): Environment
300
    {
301 159
        $environment = self::createCommonMarkEnvironment();
302 159
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
303
304 159
        return $environment;
305
    }
306
307 429
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
308
    {
309 429
        $this->assertUninitialized('Failed to add event listener.');
310
311 426
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
312
313 426
        if (\is_object($listener)) {
314 303
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
315 165
        } elseif (\is_array($listener) && \is_object($listener[0])) {
316 165
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
317
        }
318
319 426
        return $this;
320
    }
321
322
    /**
323
     * {@inheritDoc}
324
     */
325 2952
    public function dispatch(object $event)
326
    {
327 2952
        if (! $this->extensionsInitialized) {
328 2946
            $this->initializeExtensions();
329
        }
330
331 2943
        if ($this->eventDispatcher !== null) {
332 3
            return $this->eventDispatcher->dispatch($event);
333
        }
334
335 2940
        foreach ($this->getListenersForEvent($event) as $listener) {
336 420
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
337 3
                return $event;
338
            }
339
340 420
            $listener($event);
341
        }
342
343 2934
        return $event;
344
    }
345
346 3
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
347
    {
348 3
        $this->eventDispatcher = $dispatcher;
349 3
    }
350
351
    /**
352
     * {@inheritDoc}
353
     *
354
     * @return iterable<callable>
355
     */
356 2940
    public function getListenersForEvent(object $event): iterable
357
    {
358 2940
        foreach ($this->listenerData as $listenerData) {
359
            \assert($listenerData instanceof ListenerData);
360
361 420
            if (! \is_a($event, $listenerData->getEvent())) {
362 411
                continue;
363
            }
364
365 280
            yield function (object $event) use ($listenerData) {
366 420
                if (! $this->extensionsInitialized) {
367
                    $this->initializeExtensions();
368
                }
369
370 420
                return \call_user_func($listenerData->getListener(), $event);
371 420
            };
372
        }
373 2934
    }
374
375
    /**
376
     * @return iterable<InlineParserInterface>
377
     */
378 2937
    public function getInlineParsers(): iterable
379
    {
380 2937
        if (! $this->extensionsInitialized) {
381 12
            $this->initializeExtensions();
382
        }
383
384 2937
        return $this->inlineParsers->getIterator();
385
    }
386
387
    /**
388
     * @throws \RuntimeException
389
     */
390 3057
    private function assertUninitialized(string $message): void
391
    {
392 3057
        if ($this->extensionsInitialized) {
393 21
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
394
        }
395 3036
    }
396
}
397