Passed
Push — latest ( adbbf9...7e49ae )
by Colin
24:26 queued 22:27
created

src/Environment/Environment.php (2 issues)

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 3072
    public function __construct(array $config = [])
112
    {
113 3072
        $this->config = self::createDefaultConfiguration();
114 3072
        $this->config->merge($config);
115
116 3072
        $this->blockStartParsers   = new PrioritizedList();
117 3072
        $this->inlineParsers       = new PrioritizedList();
118 3072
        $this->listenerData        = new PrioritizedList();
119 3072
        $this->delimiterProcessors = new DelimiterProcessorCollection();
120 3072
    }
121
122 2976
    public function getConfiguration(): ConfigurationInterface
123
    {
124 2976
        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 309
    public function mergeConfig(array $config): void
133
    {
134 309
        @\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 309
        $this->assertUninitialized('Failed to modify configuration.');
137
138 306
        $this->config->merge($config);
139 306
    }
140
141 2943
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
142
    {
143 2943
        $this->assertUninitialized('Failed to add block start parser.');
144
145 2940
        $this->blockStartParsers->add($parser, $priority);
146 2940
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
147
148 2940
        return $this;
149
    }
150
151 2949
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
152
    {
153 2949
        $this->assertUninitialized('Failed to add inline parser.');
154
155 2946
        $this->inlineParsers->add($parser, $priority);
156 2946
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
157
158 2946
        return $this;
159
    }
160
161 2946
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
162
    {
163 2946
        $this->assertUninitialized('Failed to add delimiter processor.');
164 2943
        $this->delimiterProcessors->add($processor);
165 2943
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
166
167 2943
        return $this;
168
    }
169
170 2964
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface
171
    {
172 2964
        $this->assertUninitialized('Failed to add renderer.');
173
174 2961
        if (! isset($this->renderersByClass[$nodeClass])) {
175 2961
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
176
        }
177
178 2961
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
179 2961
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
180
181 2961
        return $this;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 2316
    public function getBlockStartParsers(): iterable
188
    {
189 2316
        if (! $this->extensionsInitialized) {
190 30
            $this->initializeExtensions();
191
        }
192
193 2316
        return $this->blockStartParsers->getIterator();
194
    }
195
196 2646
    public function getDelimiterProcessors(): DelimiterProcessorCollection
197
    {
198 2646
        if (! $this->extensionsInitialized) {
199 6
            $this->initializeExtensions();
200
        }
201
202 2646
        return $this->delimiterProcessors;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208 2955
    public function getRenderersForClass(string $nodeClass): iterable
209
    {
210 2955
        if (! $this->extensionsInitialized) {
211 33
            $this->initializeExtensions();
212
        }
213
214
        // If renderers are defined for this specific class, return them immediately
215 2955
        if (isset($this->renderersByClass[$nodeClass])) {
216 2946
            return $this->renderersByClass[$nodeClass];
217
        }
218
219
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
220 75
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
221 69
            if (! isset($this->renderersByClass[$parent])) {
222 6
                continue;
223
            }
224
225
            // "Cache" this result to avoid future loops
226 66
            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 2982
    public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
246
    {
247 2982
        $this->assertUninitialized('Failed to add extension.');
248
249 2979
        $this->extensions[]              = $extension;
250 2979
        $this->uninitializedExtensions[] = $extension;
251
252 2979
        if ($extension instanceof ConfigurableExtensionInterface) {
253 2973
            $extension->configureSchema($this->config);
254
        }
255
256 2979
        return $this;
257
    }
258
259 3021
    private function initializeExtensions(): void
260
    {
261
        // Ask all extensions to register their components
262 3021
        while (\count($this->uninitializedExtensions) > 0) {
263 2937
            foreach ($this->uninitializedExtensions as $i => $extension) {
264 2937
                $extension->register($this);
265 2937
                unset($this->uninitializedExtensions[$i]);
266
            }
267
        }
268
269 3021
        $this->extensionsInitialized = true;
270
271
        // Create the special delimiter parser if any processors were registered
272 3021
        if ($this->delimiterProcessors->count() > 0) {
273 2940
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
274
        }
275 3021
    }
276
277 3000
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
278
    {
279 3000
        if ($object instanceof EnvironmentAwareInterface) {
280 2949
            $object->setEnvironment($this);
281
        }
282
283 3000
        if ($object instanceof ConfigurationAwareInterface) {
284 2949
            $object->setConfiguration($this->config->reader());
285
        }
286 3000
    }
287
288
    /**
289
     * @deprecated Instantiate the environment and add the extension yourself
290
     *
291
     * @param array<string, mixed> $config
292
     */
293 756
    public static function createCommonMarkEnvironment(array $config = []): Environment
294
    {
295 756
        $environment = new self($config);
296 756
        $environment->addExtension(new CommonMarkCoreExtension());
297
298 756
        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 429
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
315
    {
316 429
        $this->assertUninitialized('Failed to add event listener.');
317
318 426
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
319
320 426
        if (\is_object($listener)) {
321 303
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
322 168
        } elseif (\is_array($listener) && \is_object($listener[0])) {
323 168
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
324
        }
325
326 426
        return $this;
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332 2946
    public function dispatch(object $event)
333
    {
334 2946
        if (! $this->extensionsInitialized) {
335 2940
            $this->initializeExtensions();
336
        }
337
338 2946
        if ($this->eventDispatcher !== null) {
339 3
            return $this->eventDispatcher->dispatch($event);
340
        }
341
342 2943
        foreach ($this->getListenersForEvent($event) as $listener) {
343 420
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
344 3
                return $event;
345
            }
346
347 420
            $listener($event);
348
        }
349
350 2937
        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 2943
    public function getListenersForEvent(object $event): iterable
364
    {
365 2943
        foreach ($this->listenerData as $listenerData) {
366
            \assert($listenerData instanceof ListenerData);
367
368 420
            if (! \is_a($event, $listenerData->getEvent())) {
369 411
                continue;
370
            }
371
372 280
            yield function (object $event) use ($listenerData) {
373 420
                if (! $this->extensionsInitialized) {
374
                    $this->initializeExtensions();
375
                }
376
377 420
                return \call_user_func($listenerData->getListener(), $event);
378 420
            };
379
        }
380 2937
    }
381
382
    /**
383
     * @return iterable<InlineParserInterface>
384
     */
385 2940
    public function getInlineParsers(): iterable
386
    {
387 2940
        if (! $this->extensionsInitialized) {
388 12
            $this->initializeExtensions();
389
        }
390
391 2940
        return $this->inlineParsers->getIterator();
392
    }
393
394
    /**
395
     * @throws \RuntimeException
396
     */
397 3066
    private function assertUninitialized(string $message): void
398
    {
399 3066
        if ($this->extensionsInitialized) {
400 21
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
401
        }
402 3045
    }
403
404 3306
    public static function createDefaultConfiguration(): Configuration
405
    {
406 3306
        return new Configuration([
407 3306
            'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
408 3306
            'allow_unsafe_links' => Expect::bool(true),
409 3306
            'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
410 3306
            'renderer' => Expect::structure([
411 3306
                'block_separator' => Expect::string("\n"),
412 3306
                'inner_separator' => Expect::string("\n"),
413 3306
                'soft_break' => Expect::string("\n"),
414 3306
            ])->castTo('array'),
415
        ]);
416
    }
417
}
418