Passed
Pull Request — latest (#598)
by Colin
16:46 queued 13:48
created

Environment::createGFMEnvironment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

308
        $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...
309 81
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
310
311 81
        return $environment;
312
    }
313
314 426
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
315
    {
316 426
        $this->assertUninitialized('Failed to add event listener.');
317
318 423
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
319
320 423
        if (\is_object($listener)) {
321 300
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
322 165
        } elseif (\is_array($listener) && \is_object($listener[0])) {
323 165
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
324
        }
325
326 423
        return $this;
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332 2940
    public function dispatch(object $event)
333
    {
334 2940
        if (! $this->extensionsInitialized) {
335 2934
            $this->initializeExtensions();
336
        }
337
338 2940
        if ($this->eventDispatcher !== null) {
339 3
            return $this->eventDispatcher->dispatch($event);
340
        }
341
342 2937
        foreach ($this->getListenersForEvent($event) as $listener) {
343 417
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
344 3
                return $event;
345
            }
346
347 417
            $listener($event);
348
        }
349
350 2931
        return $event;
351
    }
352
353 3
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
354
    {
355 3
        $this->eventDispatcher = $dispatcher;
356 3
    }
357
358
    /**
359
     * {@inheritDoc}
360
     *
361
     * @return iterable<callable>
362
     */
363 2937
    public function getListenersForEvent(object $event): iterable
364
    {
365 2937
        foreach ($this->listenerData as $listenerData) {
366
            \assert($listenerData instanceof ListenerData);
367
368 417
            if (! \is_a($event, $listenerData->getEvent())) {
369 408
                continue;
370
            }
371
372 278
            yield function (object $event) use ($listenerData) {
373 417
                if (! $this->extensionsInitialized) {
374
                    $this->initializeExtensions();
375
                }
376
377 417
                return \call_user_func($listenerData->getListener(), $event);
378 417
            };
379
        }
380 2931
    }
381
382
    /**
383
     * @return iterable<InlineParserInterface>
384
     */
385 2934
    public function getInlineParsers(): iterable
386
    {
387 2934
        if (! $this->extensionsInitialized) {
388 12
            $this->initializeExtensions();
389
        }
390
391 2934
        return $this->inlineParsers->getIterator();
392
    }
393
394
    /**
395
     * @throws \RuntimeException
396
     */
397 3060
    private function assertUninitialized(string $message): void
398
    {
399 3060
        if ($this->extensionsInitialized) {
400 21
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
401
        }
402 3039
    }
403
404 3300
    public static function createDefaultConfiguration(): Configuration
405
    {
406 3300
        return new Configuration([
407 3300
            'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
408 3300
            'allow_unsafe_links' => Expect::bool(true),
409 3300
            'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
410 3300
            'renderer' => Expect::structure([
411 3300
                'block_separator' => Expect::string("\n"),
412 3300
                'inner_separator' => Expect::string("\n"),
413 3300
                'soft_break' => Expect::string("\n"),
414 3300
            ])->castTo('array'),
415
        ]);
416
    }
417
}
418