Passed
Branch latest (76d169)
by Colin
01:59
created

Environment::getInlineParsersForCharacter()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
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\Delimiter\Processor\DelimiterProcessorCollection;
22
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
23
use League\CommonMark\Event\ListenerData;
24
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
25
use League\CommonMark\Extension\ExtensionInterface;
26
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
27
use League\CommonMark\Parser\Block\BlockStartParserInterface;
28
use League\CommonMark\Parser\Inline\InlineParserInterface;
29
use League\CommonMark\Renderer\NodeRendererInterface;
30
use League\CommonMark\Util\HtmlFilter;
31
use League\CommonMark\Util\PrioritizedList;
32
use Psr\EventDispatcher\EventDispatcherInterface;
33
use Psr\EventDispatcher\ListenerProviderInterface;
34
use Psr\EventDispatcher\StoppableEventInterface;
35
36
final class Environment implements ConfigurableEnvironmentInterface, ListenerProviderInterface
37
{
38
    /**
39
     * @var ExtensionInterface[]
40
     *
41
     * @psalm-readonly-allow-private-mutation
42
     */
43
    private $extensions = [];
44
45
    /**
46
     * @var ExtensionInterface[]
47
     *
48
     * @psalm-readonly-allow-private-mutation
49
     */
50
    private $uninitializedExtensions = [];
51
52
    /**
53
     * @var bool
54
     *
55
     * @psalm-readonly-allow-private-mutation
56
     */
57
    private $extensionsInitialized = false;
58
59
    /**
60
     * @var PrioritizedList<BlockStartParserInterface>
61
     *
62
     * @psalm-readonly
63
     */
64
    private $blockStartParsers;
65
66
    /**
67
     * @var PrioritizedList<InlineParserInterface>
68
     *
69
     * @psalm-readonly
70
     */
71
    private $inlineParsers;
72
73
    /**
74
     * @var array<string, PrioritizedList<InlineParserInterface>>
0 ignored issues
show
Documentation Bug introduced by Colin O'Dell
The doc comment array<string, Prioritize...InlineParserInterface>> at position 4 could not be parsed: Expected '>' at position 4, but found 'PrioritizedList'.
Loading history...
75
     *
76
     * @psalm-readonly-allow-private-mutation
77
     */
78
    private $inlineParsersByCharacter = [];
79
80
    /**
81
     * @var DelimiterProcessorCollection
82
     *
83
     * @psalm-readonly
84
     */
85
    private $delimiterProcessors;
86
87
    /**
88
     * @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...
89
     *
90
     * @psalm-readonly-allow-private-mutation
91
     */
92
    private $renderersByClass = [];
93
94
    /**
95
     * @var PrioritizedList<ListenerData>
96
     *
97
     * @psalm-readonly-allow-private-mutation
98
     */
99
    private $listenerData;
100
101
    /** @var EventDispatcherInterface|null */
102
    private $eventDispatcher;
103
104
    /**
105
     * @var Configuration
106
     *
107
     * @psalm-readonly
108
     */
109
    private $config;
110
111
    /**
112
     * @var string
113
     *
114
     * @psalm-readonly-allow-private-mutation
115
     */
116
    private $inlineParserCharacterRegex;
117
118
    /**
119
     * @param array<string, mixed> $config
120
     */
121 3012
    public function __construct(array $config = [])
122
    {
123 3012
        $this->config = new Configuration($config);
124
125 3012
        $this->blockStartParsers   = new PrioritizedList();
126 3012
        $this->inlineParsers       = new PrioritizedList();
127 3012
        $this->listenerData        = new PrioritizedList();
128 3012
        $this->delimiterProcessors = new DelimiterProcessorCollection();
129 3012
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 2901
    public function mergeConfig(array $config = []): void
135
    {
136 2901
        $this->assertUninitialized('Failed to modify configuration.');
137
138 2898
        $this->config->merge($config);
139 2898
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144 18
    public function setConfig(array $config = []): void
145
    {
146 18
        $this->assertUninitialized('Failed to modify configuration.');
147
148 15
        $this->config->replace($config);
149 15
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 2907
    public function getConfig(?string $key = null, $default = null)
155
    {
156 2907
        return $this->config->get($key, $default);
157
    }
158
159 2889
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
160
    {
161 2889
        $this->assertUninitialized('Failed to add block start parser.');
162
163 2886
        $this->blockStartParsers->add($parser, $priority);
164 2886
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
165
166 2886
        return $this;
167
    }
168
169 2901
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
170
    {
171 2901
        $this->assertUninitialized('Failed to add inline parser.');
172
173 2898
        $this->inlineParsers->add($parser, $priority);
174 2898
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
175
176 2898
        foreach ($parser->getCharacters() as $character) {
177 2895
            if (! isset($this->inlineParsersByCharacter[$character])) {
178 2895
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
179
            }
180
181 2895
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
182
        }
183
184 2898
        return $this;
185
    }
186
187 2892
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
188
    {
189 2892
        $this->assertUninitialized('Failed to add delimiter processor.');
190 2889
        $this->delimiterProcessors->add($processor);
191 2889
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
192
193 2889
        return $this;
194
    }
195
196 2910
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
197
    {
198 2910
        $this->assertUninitialized('Failed to add renderer.');
199
200 2907
        if (! isset($this->renderersByClass[$nodeClass])) {
201 2907
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
202
        }
203
204 2907
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
205 2907
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
206
207 2907
        return $this;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 2271
    public function getBlockStartParsers(): iterable
214
    {
215 2271
        if (! $this->extensionsInitialized) {
216 33
            $this->initializeExtensions();
217
        }
218
219 2271
        return $this->blockStartParsers->getIterator();
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 2598
    public function getInlineParsersForCharacter(string $character): iterable
226
    {
227 2598
        if (! $this->extensionsInitialized) {
228 24
            $this->initializeExtensions();
229
        }
230
231 2598
        if (! isset($this->inlineParsersByCharacter[$character])) {
232 2454
            return [];
233
        }
234
235 1491
        return $this->inlineParsersByCharacter[$character]->getIterator();
236
    }
237
238 2583
    public function getDelimiterProcessors(): DelimiterProcessorCollection
239
    {
240 2583
        if (! $this->extensionsInitialized) {
241 6
            $this->initializeExtensions();
242
        }
243
244 2583
        return $this->delimiterProcessors;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 2895
    public function getRenderersForClass(string $nodeClass): iterable
251
    {
252 2895
        if (! $this->extensionsInitialized) {
253 33
            $this->initializeExtensions();
254
        }
255
256
        // If renderers are defined for this specific class, return them immediately
257 2895
        if (isset($this->renderersByClass[$nodeClass])) {
258 2886
            return $this->renderersByClass[$nodeClass];
259
        }
260
261
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
262 60
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
263 54
            if (! isset($this->renderersByClass[$parent])) {
264 6
                continue;
265
            }
266
267
            // "Cache" this result to avoid future loops
268 51
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
269
        }
270
271 9
        return [];
272
    }
273
274
    /**
275
     * Get all registered extensions
276
     *
277
     * @return ExtensionInterface[]
278
     */
279 12
    public function getExtensions(): iterable
280
    {
281 12
        return $this->extensions;
282
    }
283
284
    /**
285
     * Add a single extension
286
     *
287
     * @return $this
288
     */
289 2904
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
290
    {
291 2904
        $this->assertUninitialized('Failed to add extension.');
292
293 2901
        $this->extensions[]              = $extension;
294 2901
        $this->uninitializedExtensions[] = $extension;
295
296 2901
        return $this;
297
    }
298
299 2982
    private function initializeExtensions(): void
300
    {
301
        // Ask all extensions to register their components
302 2982
        while (\count($this->uninitializedExtensions) > 0) {
303 2883
            foreach ($this->uninitializedExtensions as $i => $extension) {
304 2883
                $extension->register($this);
305 2883
                unset($this->uninitializedExtensions[$i]);
306
            }
307
        }
308
309 2979
        $this->extensionsInitialized = true;
310
311
        // Lastly, let's build a regex which matches non-inline characters
312
        // This will enable a huge performance boost with inline parsing
313 2979
        $this->buildInlineParserCharacterRegex();
314 2979
    }
315
316 2952
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
317
    {
318 2952
        if ($object instanceof EnvironmentAwareInterface) {
319 2895
            $object->setEnvironment($this);
320
        }
321
322 2952
        if ($object instanceof ConfigurationAwareInterface) {
323 2895
            $object->setConfiguration($this->config);
324
        }
325 2952
    }
326
327 2892
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
328
    {
329 2892
        $environment = new static();
330 2892
        $environment->addExtension(new CommonMarkCoreExtension());
331 2892
        $environment->mergeConfig([
332 964
            'renderer' => [
333 1928
                'block_separator' => "\n",
334
                'inner_separator' => "\n",
335
                'soft_break'      => "\n",
336
            ],
337 2892
            'html_input'         => HtmlFilter::ALLOW,
338
            'allow_unsafe_links' => true,
339
            'max_nesting_level'  => \INF,
340
        ]);
341
342 2892
        return $environment;
343
    }
344
345 150
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
346
    {
347 150
        $environment = self::createCommonMarkEnvironment();
348 150
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
349
350 150
        return $environment;
351
    }
352
353 2442
    public function getInlineParserCharacterRegex(): string
354
    {
355 2442
        return $this->inlineParserCharacterRegex;
356
    }
357
358 609
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
359
    {
360 609
        $this->assertUninitialized('Failed to add event listener.');
361
362 606
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
363
364 606
        if (\is_object($listener)) {
365 531
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
366 162
        } elseif (\is_array($listener) && \is_object($listener[0])) {
367 162
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
368
        }
369
370 606
        return $this;
371
    }
372
373
    /**
374
     * {@inheritDoc}
375
     */
376 2892
    public function dispatch(object $event)
377
    {
378 2892
        if (! $this->extensionsInitialized) {
379 2886
            $this->initializeExtensions();
380
        }
381
382 2889
        if ($this->eventDispatcher !== null) {
383 3
            return $this->eventDispatcher->dispatch($event);
384
        }
385
386 2886
        foreach ($this->getListenersForEvent($event) as $listener) {
387 600
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
388 3
                return $event;
389
            }
390
391 600
            $listener($event);
392
        }
393
394 2880
        return $event;
395
    }
396
397 3
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
398
    {
399 3
        $this->eventDispatcher = $dispatcher;
400 3
    }
401
402
    /**
403
     * {@inheritDoc}
404
     *
405
     * @return iterable<callable>
406
     */
407 2886
    public function getListenersForEvent(object $event): iterable
408
    {
409 2886
        foreach ($this->listenerData as $listenerData) {
410
            \assert($listenerData instanceof ListenerData);
411
412 600
            if (! \is_a($event, $listenerData->getEvent())) {
413 591
                continue;
414
            }
415
416 400
            yield function (object $event) use ($listenerData) {
417 600
                if (! $this->extensionsInitialized) {
418
                    $this->initializeExtensions();
419
                }
420
421 600
                return \call_user_func($listenerData->getListener(), $event);
422 600
            };
423
        }
424 2880
    }
425
426 2979
    private function buildInlineParserCharacterRegex(): void
427
    {
428 2979
        $chars = \array_unique(\array_merge(
429 2979
            \array_keys($this->inlineParsersByCharacter),
430 2979
            $this->delimiterProcessors->getDelimiterCharacters()
431
        ));
432
433 2979
        if (\count($chars) === 0) {
434
            // If no special inline characters exist then parse the whole line
435 81
            $this->inlineParserCharacterRegex = '/^.+$/';
436
        } else {
437
            // Match any character which inline parsers are not interested in
438 2898
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/';
439
440
            // Only add the u modifier (which slows down performance) if we have a multi-byte UTF-8 character in our regex
441 2898
            if (\strlen($this->inlineParserCharacterRegex) > \mb_strlen($this->inlineParserCharacterRegex)) {
442 54
                $this->inlineParserCharacterRegex .= 'u';
443
            }
444
        }
445 2979
    }
446
447
    /**
448
     * @throws \RuntimeException
449
     */
450 3000
    private function assertUninitialized(string $message): void
451
    {
452 3000
        if ($this->extensionsInitialized) {
453 24
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
454
        }
455 2976
    }
456
}
457