Completed
Push — latest ( 7e49ae...5ac9c7 )
by Colin
20s queued 12s
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 3084
    public function __construct(array $config = [])
112
    {
113 3084
        $this->config = self::createDefaultConfiguration();
114 3084
        $this->config->merge($config);
115
116 3084
        $this->blockStartParsers   = new PrioritizedList();
117 3084
        $this->inlineParsers       = new PrioritizedList();
118 3084
        $this->listenerData        = new PrioritizedList();
119 3084
        $this->delimiterProcessors = new DelimiterProcessorCollection();
120 3084
    }
121
122 2988
    public function getConfiguration(): ConfigurationInterface
123
    {
124 2988
        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 2955
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
142
    {
143 2955
        $this->assertUninitialized('Failed to add block start parser.');
144
145 2952
        $this->blockStartParsers->add($parser, $priority);
146 2952
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
147
148 2952
        return $this;
149
    }
150
151 2961
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
152
    {
153 2961
        $this->assertUninitialized('Failed to add inline parser.');
154
155 2958
        $this->inlineParsers->add($parser, $priority);
156 2958
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
157
158 2958
        return $this;
159
    }
160
161 2958
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
162
    {
163 2958
        $this->assertUninitialized('Failed to add delimiter processor.');
164 2955
        $this->delimiterProcessors->add($processor);
165 2955
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
166
167 2955
        return $this;
168
    }
169
170 2976
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface
171
    {
172 2976
        $this->assertUninitialized('Failed to add renderer.');
173
174 2973
        if (! isset($this->renderersByClass[$nodeClass])) {
175 2973
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
176
        }
177
178 2973
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
179 2973
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
180
181 2973
        return $this;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 2328
    public function getBlockStartParsers(): iterable
188
    {
189 2328
        if (! $this->extensionsInitialized) {
190 30
            $this->initializeExtensions();
191
        }
192
193 2328
        return $this->blockStartParsers->getIterator();
194
    }
195
196 2658
    public function getDelimiterProcessors(): DelimiterProcessorCollection
197
    {
198 2658
        if (! $this->extensionsInitialized) {
199 6
            $this->initializeExtensions();
200
        }
201
202 2658
        return $this->delimiterProcessors;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208 2967
    public function getRenderersForClass(string $nodeClass): iterable
209
    {
210 2967
        if (! $this->extensionsInitialized) {
211 33
            $this->initializeExtensions();
212
        }
213
214
        // If renderers are defined for this specific class, return them immediately
215 2967
        if (isset($this->renderersByClass[$nodeClass])) {
216 2958
            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 2994
    public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
246
    {
247 2994
        $this->assertUninitialized('Failed to add extension.');
248
249 2991
        $this->extensions[]              = $extension;
250 2991
        $this->uninitializedExtensions[] = $extension;
251
252 2991
        if ($extension instanceof ConfigurableExtensionInterface) {
253 2985
            $extension->configureSchema($this->config);
254
        }
255
256 2991
        return $this;
257
    }
258
259 3033
    private function initializeExtensions(): void
260
    {
261
        // Ask all extensions to register their components
262 3033
        while (\count($this->uninitializedExtensions) > 0) {
263 2949
            foreach ($this->uninitializedExtensions as $i => $extension) {
264 2949
                $extension->register($this);
265 2949
                unset($this->uninitializedExtensions[$i]);
266
            }
267
        }
268
269 3033
        $this->extensionsInitialized = true;
270
271
        // Create the special delimiter parser if any processors were registered
272 3033
        if ($this->delimiterProcessors->count() > 0) {
273 2952
            $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
274
        }
275 3033
    }
276
277 3012
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
278
    {
279 3012
        if ($object instanceof EnvironmentAwareInterface) {
280 2961
            $object->setEnvironment($this);
281
        }
282
283 3012
        if ($object instanceof ConfigurationAwareInterface) {
284 2961
            $object->setConfiguration($this->config->reader());
285
        }
286 3012
    }
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 441
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
315
    {
316 441
        $this->assertUninitialized('Failed to add event listener.');
317
318 438
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
319
320 438
        if (\is_object($listener)) {
321 303
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
322 180
        } elseif (\is_array($listener) && \is_object($listener[0])) {
323 180
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
324
        }
325
326 438
        return $this;
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332 2958
    public function dispatch(object $event)
333
    {
334 2958
        if (! $this->extensionsInitialized) {
335 2952
            $this->initializeExtensions();
336
        }
337
338 2958
        if ($this->eventDispatcher !== null) {
339 3
            return $this->eventDispatcher->dispatch($event);
340
        }
341
342 2955
        foreach ($this->getListenersForEvent($event) as $listener) {
343 432
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
344 3
                return $event;
345
            }
346
347 432
            $listener($event);
348
        }
349
350 2949
        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 2955
    public function getListenersForEvent(object $event): iterable
364
    {
365 2955
        foreach ($this->listenerData as $listenerData) {
366
            \assert($listenerData instanceof ListenerData);
367
368 432
            if (! \is_a($event, $listenerData->getEvent())) {
369 423
                continue;
370
            }
371
372 432
            yield function (object $event) use ($listenerData) {
373 432
                if (! $this->extensionsInitialized) {
374
                    $this->initializeExtensions();
375
                }
376
377 432
                return \call_user_func($listenerData->getListener(), $event);
378 432
            };
379
        }
380 2949
    }
381
382
    /**
383
     * @return iterable<InlineParserInterface>
384
     */
385 2952
    public function getInlineParsers(): iterable
386
    {
387 2952
        if (! $this->extensionsInitialized) {
388 12
            $this->initializeExtensions();
389
        }
390
391 2952
        return $this->inlineParsers->getIterator();
392
    }
393
394
    /**
395
     * @throws \RuntimeException
396
     */
397 3078
    private function assertUninitialized(string $message): void
398
    {
399 3078
        if ($this->extensionsInitialized) {
400 21
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
401
        }
402 3057
    }
403
404 3318
    public static function createDefaultConfiguration(): Configuration
405
    {
406 3318
        return new Configuration([
407 3318
            'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
408 3318
            'allow_unsafe_links' => Expect::bool(true),
409 3318
            'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
410 3318
            'renderer' => Expect::structure([
411 3318
                'block_separator' => Expect::string("\n"),
412 3318
                'inner_separator' => Expect::string("\n"),
413 3318
                'soft_break' => Expect::string("\n"),
414 3318
            ])->castTo('array'),
415
        ]);
416
    }
417
}
418