Issues (33)

src/Environment/Environment.php (1 issue)

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
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);
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