Issues (33)

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\Delimiter\DelimiterParser;
20
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
21
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
22
use League\CommonMark\Event\DocumentParsedEvent;
23
use League\CommonMark\Event\ListenerData;
24
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
25
use League\CommonMark\Extension\ConfigurableExtensionInterface;
26
use League\CommonMark\Extension\ExtensionInterface;
27
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
28
use League\CommonMark\Normalizer\SlugNormalizer;
29
use League\CommonMark\Normalizer\TextNormalizerInterface;
30
use League\CommonMark\Normalizer\UniqueSlugNormalizer;
31
use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface;
32
use League\CommonMark\Parser\Block\BlockStartParserInterface;
33
use League\CommonMark\Parser\Inline\InlineParserInterface;
34
use League\CommonMark\Renderer\NodeRendererInterface;
35
use League\CommonMark\Util\HtmlFilter;
36
use League\CommonMark\Util\PrioritizedList;
37
use League\Config\Configuration;
38
use League\Config\ConfigurationAwareInterface;
39
use League\Config\ConfigurationInterface;
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 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...
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 3150
    public function __construct(array $config = [])
120
    {
121 3150
        $this->config = self::createDefaultConfiguration();
122 3150
        $this->config->merge($config);
123
124 3150
        $this->blockStartParsers   = new PrioritizedList();
125 3150
        $this->inlineParsers       = new PrioritizedList();
126 3150
        $this->listenerData        = new PrioritizedList();
127 3150
        $this->delimiterProcessors = new DelimiterProcessorCollection();
128 3150
    }
129
130 3039
    public function getConfiguration(): ConfigurationInterface
131
    {
132 3039
        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 2997
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
150
    {
151 2997
        $this->assertUninitialized('Failed to add block start parser.');
152
153 2994
        $this->blockStartParsers->add($parser, $priority);
154 2994
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
155
156 2994
        return $this;
157
    }
158
159 3003
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
160
    {
161 3003
        $this->assertUninitialized('Failed to add inline parser.');
162
163 3000
        $this->inlineParsers->add($parser, $priority);
164 3000
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
165
166 3000
        return $this;
167
    }
168
169 3000
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
170
    {
171 3000
        $this->assertUninitialized('Failed to add delimiter processor.');
172 2997
        $this->delimiterProcessors->add($processor);
173 2997
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
174
175 2997
        return $this;
176
    }
177
178 3018
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface
179
    {
180 3018
        $this->assertUninitialized('Failed to add renderer.');
181
182 3015
        if (! isset($this->renderersByClass[$nodeClass])) {
183 3015
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
184
        }
185
186 3015
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
187 3015
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
188
189 3015
        return $this;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 2370
    public function getBlockStartParsers(): iterable
196
    {
197 2370
        if (! $this->extensionsInitialized) {
198 30
            $this->initializeExtensions();
199
        }
200
201 2370
        return $this->blockStartParsers->getIterator();
202
    }
203
204 2700
    public function getDelimiterProcessors(): DelimiterProcessorCollection
205
    {
206 2700
        if (! $this->extensionsInitialized) {
207 6
            $this->initializeExtensions();
208
        }
209
210 2700
        return $this->delimiterProcessors;
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216 3009
    public function getRenderersForClass(string $nodeClass): iterable
217
    {
218 3009
        if (! $this->extensionsInitialized) {
219 33
            $this->initializeExtensions();
220
        }
221
222
        // If renderers are defined for this specific class, return them immediately
223 3009
        if (isset($this->renderersByClass[$nodeClass])) {
224 3000
            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 3048
    public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
254
    {
255 3048
        $this->assertUninitialized('Failed to add extension.');
256
257 3045
        $this->extensions[]              = $extension;
258 3045
        $this->uninitializedExtensions[] = $extension;
259
260 3045
        if ($extension instanceof ConfigurableExtensionInterface) {
261 3039
            $extension->configureSchema($this->config);
262
        }
263
264 3045
        return $this;
265
    }
266
267 3081
    private function initializeExtensions(): void
268
    {
269
        // Initialize the slug normalizer
270 3081
        $this->getSlugNormalizer();
271
272
        // Ask all extensions to register their components
273 3081
        while (\count($this->uninitializedExtensions) > 0) {
274 2991
            foreach ($this->uninitializedExtensions as $i => $extension) {
275 2991
                $extension->register($this);
276 2991
                unset($this->uninitializedExtensions[$i]);
277
            }
278
        }
279
280 3081
        $this->extensionsInitialized = true;
281
282
        // Create the special delimiter parser if any processors were registered
283 3081
        if ($this->delimiterProcessors->count() > 0) {
284 2994
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
285
        }
286 3081
    }
287
288 3102
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
289
    {
290 3102
        if ($object instanceof EnvironmentAwareInterface) {
291 3003
            $object->setEnvironment($this);
292
        }
293
294 3102
        if ($object instanceof ConfigurationAwareInterface) {
295 3093
            $object->setConfiguration($this->config->reader());
296
        }
297 3102
    }
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 Colin O'Dell
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 3093
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
326
    {
327 3093
        $this->assertUninitialized('Failed to add event listener.');
328
329 3093
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
330
331 3093
        if (\is_object($listener)) {
332 345
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
333 3093
        } elseif (\is_array($listener) && \is_object($listener[0])) {
334 3093
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
335
        }
336
337 3093
        return $this;
338
    }
339
340
    /**
341
     * {@inheritDoc}
342
     */
343 3006
    public function dispatch(object $event)
344
    {
345 3006
        if (! $this->extensionsInitialized) {
346 3000
            $this->initializeExtensions();
347
        }
348
349 3006
        if ($this->eventDispatcher !== null) {
350 3
            return $this->eventDispatcher->dispatch($event);
351
        }
352
353 3003
        foreach ($this->getListenersForEvent($event) as $listener) {
354 2994
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
355 3
                return $event;
356
            }
357
358 2994
            $listener($event);
359
        }
360
361 2997
        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 3003
    public function getListenersForEvent(object $event): iterable
375
    {
376 3003
        foreach ($this->listenerData as $listenerData) {
377
            \assert($listenerData instanceof ListenerData);
378
379 3000
            if (! \is_a($event, $listenerData->getEvent())) {
380 2991
                continue;
381
            }
382
383 1996
            yield function (object $event) use ($listenerData) {
384 2994
                if (! $this->extensionsInitialized) {
385
                    $this->initializeExtensions();
386
                }
387
388 2994
                return \call_user_func($listenerData->getListener(), $event);
389 2994
            };
390
        }
391 2997
    }
392
393
    /**
394
     * @return iterable<InlineParserInterface>
395
     */
396 2994
    public function getInlineParsers(): iterable
397
    {
398 2994
        if (! $this->extensionsInitialized) {
399 12
            $this->initializeExtensions();
400
        }
401
402 2994
        return $this->inlineParsers->getIterator();
403
    }
404
405 3102
    public function getSlugNormalizer(): TextNormalizerInterface
406
    {
407 3102
        if ($this->slugNormalizer === null) {
408 3102
            $normalizer = $this->config->get('slug_normalizer/instance');
409
            \assert($normalizer instanceof TextNormalizerInterface);
410 3099
            $this->injectEnvironmentAndConfigurationIfNeeded($normalizer);
411
412 3099
            if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) {
413 3096
                $normalizer = new UniqueSlugNormalizer($normalizer);
414
            }
415
416 3099
            if ($normalizer instanceof UniqueSlugNormalizer) {
417 3096
                if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) {
418 3093
                    $this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000);
419
                }
420
            }
421
422 3099
            $this->slugNormalizer = $normalizer;
423
        }
424
425 3099
        return $this->slugNormalizer;
426
    }
427
428
    /**
429
     * @throws \RuntimeException
430
     */
431 3138
    private function assertUninitialized(string $message): void
432
    {
433 3138
        if ($this->extensionsInitialized) {
434 21
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
435
        }
436 3138
    }
437
438 3372
    public static function createDefaultConfiguration(): Configuration
439
    {
440 3372
        return new Configuration([
441 3372
            'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
442 3372
            'allow_unsafe_links' => Expect::bool(true),
443 3372
            'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
444 3372
            'renderer' => Expect::structure([
445 3372
                'block_separator' => Expect::string("\n"),
446 3372
                'inner_separator' => Expect::string("\n"),
447 3372
                'soft_break' => Expect::string("\n"),
448
            ]),
449 3372
            'slug_normalizer' => Expect::structure([
450 3372
                'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()),
451 3372
                'max_length' => Expect::int()->min(0)->default(255),
452 3372
                'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT),
453
            ]),
454
        ]);
455
    }
456
}
457