Environment   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 48
eloc 118
c 1
b 0
f 0
dl 0
loc 377
ccs 131
cts 131
cp 1
rs 8.5599

23 Methods

Rating   Name   Duplication   Size   Complexity  
A addEventListener() 0 17 5
A initializeExtensions() 0 15 3
A addExtension() 0 8 1
A addDelimiterProcessor() 0 7 1
A dispatch() 0 14 4
A createGFMEnvironment() 0 6 1
A getInlineParserCharacterRegex() 0 3 1
A assertUninitialized() 0 4 2
A buildInlineParserCharacterRegex() 0 13 2
A mergeConfig() 0 5 1
A addBlockStartParser() 0 8 1
A __construct() 0 7 1
A addInlineParser() 0 16 3
A getInlineParsersForCharacter() 0 11 3
A getDelimiterProcessors() 0 7 2
A getBlockStartParsers() 0 7 2
A getConfig() 0 3 1
A getExtensions() 0 3 1
A addRenderer() 0 12 2
A createCommonMarkEnvironment() 0 16 1
A injectEnvironmentAndConfigurationIfNeeded() 0 8 3
A setConfig() 0 5 1
A getRenderersForClass() 0 22 6

How to fix   Complexity   

Complex Class

Complex classes like Environment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Environment, and based on these observations, apply Extract Interface, too.

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\AbstractEvent;
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
33
final class Environment implements ConfigurableEnvironmentInterface
34
{
35
    /**
36
     * @var ExtensionInterface[]
37
     *
38
     * @psalm-readonly-allow-private-mutation
39
     */
40
    private $extensions = [];
41
42
    /**
43
     * @var ExtensionInterface[]
44
     *
45
     * @psalm-readonly-allow-private-mutation
46
     */
47
    private $uninitializedExtensions = [];
48
49
    /**
50
     * @var bool
51
     *
52
     * @psalm-readonly-allow-private-mutation
53
     */
54
    private $extensionsInitialized = false;
55
56
    /**
57
     * @var PrioritizedList<BlockStartParserInterface>
58
     *
59
     * @psalm-readonly
60
     */
61
    private $blockStartParsers;
62
63
    /**
64
     * @var PrioritizedList<InlineParserInterface>
65
     *
66
     * @psalm-readonly
67
     */
68
    private $inlineParsers;
69
70
    /**
71
     * @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...
72
     *
73
     * @psalm-readonly-allow-private-mutation
74
     */
75
    private $inlineParsersByCharacter = [];
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 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...
86
     *
87
     * @psalm-readonly-allow-private-mutation
88
     */
89
    private $renderersByClass = [];
90
91
    /**
92
     * @var array<string, PrioritizedList<callable>>
0 ignored issues
show
Documentation Bug introduced by Colin O'Dell
The doc comment array<string, PrioritizedList<callable>> at position 4 could not be parsed: Expected '>' at position 4, but found 'PrioritizedList'.
Loading history...
93
     *
94
     * @psalm-readonly-allow-private-mutation
95
     */
96
    private $listeners = [];
97
98
    /**
99
     * @var Configuration
100
     *
101
     * @psalm-readonly
102
     */
103
    private $config;
104
105
    /**
106
     * @var string
107
     *
108
     * @psalm-readonly-allow-private-mutation
109
     */
110
    private $inlineParserCharacterRegex;
111
112
    /**
113
     * @param array<string, mixed> $config
114
     */
115 2952
    public function __construct(array $config = [])
116
    {
117 2952
        $this->config = new Configuration($config);
118
119 2952
        $this->blockStartParsers   = new PrioritizedList();
120 2952
        $this->inlineParsers       = new PrioritizedList();
121 2952
        $this->delimiterProcessors = new DelimiterProcessorCollection();
122 2952
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 2850
    public function mergeConfig(array $config = []): void
128
    {
129 2850
        $this->assertUninitialized('Failed to modify configuration.');
130
131 2847
        $this->config->merge($config);
132 2847
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 18
    public function setConfig(array $config = []): void
138
    {
139 18
        $this->assertUninitialized('Failed to modify configuration.');
140
141 15
        $this->config->replace($config);
142 15
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147 2856
    public function getConfig($key = null, $default = null)
148
    {
149 2856
        return $this->config->get($key, $default);
150
    }
151
152 2838
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
153
    {
154 2838
        $this->assertUninitialized('Failed to add block start parser.');
155
156 2835
        $this->blockStartParsers->add($parser, $priority);
157 2835
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
158
159 2835
        return $this;
160
    }
161
162 2844
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
163
    {
164 2844
        $this->assertUninitialized('Failed to add inline parser.');
165
166 2841
        $this->inlineParsers->add($parser, $priority);
167 2841
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
168
169 2841
        foreach ($parser->getCharacters() as $character) {
170 2838
            if (! isset($this->inlineParsersByCharacter[$character])) {
171 2838
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
172
            }
173
174 2838
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
175
        }
176
177 2841
        return $this;
178
    }
179
180 2841
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
181
    {
182 2841
        $this->assertUninitialized('Failed to add delimiter processor.');
183 2838
        $this->delimiterProcessors->add($processor);
184 2838
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
185
186 2838
        return $this;
187
    }
188
189 2859
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
190
    {
191 2859
        $this->assertUninitialized('Failed to add renderer.');
192
193 2856
        if (! isset($this->renderersByClass[$nodeClass])) {
194 2856
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
195
        }
196
197 2856
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
198 2856
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
199
200 2856
        return $this;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 2229
    public function getBlockStartParsers(): iterable
207
    {
208 2229
        if (! $this->extensionsInitialized) {
209 33
            $this->initializeExtensions();
210
        }
211
212 2229
        return $this->blockStartParsers->getIterator();
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218 2550
    public function getInlineParsersForCharacter(string $character): iterable
219
    {
220 2550
        if (! $this->extensionsInitialized) {
221 18
            $this->initializeExtensions();
222
        }
223
224 2550
        if (! isset($this->inlineParsersByCharacter[$character])) {
225 2412
            return [];
226
        }
227
228 1458
        return $this->inlineParsersByCharacter[$character]->getIterator();
229
    }
230
231 2541
    public function getDelimiterProcessors(): DelimiterProcessorCollection
232
    {
233 2541
        if (! $this->extensionsInitialized) {
234 6
            $this->initializeExtensions();
235
        }
236
237 2541
        return $this->delimiterProcessors;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 2847
    public function getRenderersForClass(string $nodeClass): iterable
244
    {
245 2847
        if (! $this->extensionsInitialized) {
246 30
            $this->initializeExtensions();
247
        }
248
249
        // If renderers are defined for this specific class, return them immediately
250 2847
        if (isset($this->renderersByClass[$nodeClass])) {
251 2838
            return $this->renderersByClass[$nodeClass];
252
        }
253
254
        /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
255 60
        while (\class_exists($parent = $parent ?? $nodeClass) && $parent = \get_parent_class($parent)) {
256 54
            if (! isset($this->renderersByClass[$parent])) {
257 6
                continue;
258
            }
259
260
            // "Cache" this result to avoid future loops
261 51
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
262
        }
263
264 9
        return [];
265
    }
266
267
    /**
268
     * Get all registered extensions
269
     *
270
     * @return ExtensionInterface[]
271
     */
272 12
    public function getExtensions(): iterable
273
    {
274 12
        return $this->extensions;
275
    }
276
277
    /**
278
     * Add a single extension
279
     *
280
     * @return $this
281
     */
282 2853
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
283
    {
284 2853
        $this->assertUninitialized('Failed to add extension.');
285
286 2850
        $this->extensions[]              = $extension;
287 2850
        $this->uninitializedExtensions[] = $extension;
288
289 2850
        return $this;
290
    }
291
292 2922
    private function initializeExtensions(): void
293
    {
294
        // Ask all extensions to register their components
295 2922
        while (\count($this->uninitializedExtensions) > 0) {
296 2832
            foreach ($this->uninitializedExtensions as $i => $extension) {
297 2832
                $extension->register($this);
298 2832
                unset($this->uninitializedExtensions[$i]);
299
            }
300
        }
301
302 2919
        $this->extensionsInitialized = true;
303
304
        // Lastly, let's build a regex which matches non-inline characters
305
        // This will enable a huge performance boost with inline parsing
306 2919
        $this->buildInlineParserCharacterRegex();
307 2919
    }
308
309 2892
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
310
    {
311 2892
        if ($object instanceof EnvironmentAwareInterface) {
312 2844
            $object->setEnvironment($this);
313
        }
314
315 2892
        if ($object instanceof ConfigurationAwareInterface) {
316 2844
            $object->setConfiguration($this->config);
317
        }
318 2892
    }
319
320 2841
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
321
    {
322 2841
        $environment = new static();
323 2841
        $environment->addExtension(new CommonMarkCoreExtension());
324 2841
        $environment->mergeConfig([
325 947
            'renderer' => [
326 1894
                'block_separator' => "\n",
327
                'inner_separator' => "\n",
328
                'soft_break'      => "\n",
329
            ],
330 2841
            'html_input'         => HtmlFilter::ALLOW,
331
            'allow_unsafe_links' => true,
332
            'max_nesting_level'  => \INF,
333
        ]);
334
335 2841
        return $environment;
336
    }
337
338 150
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
339
    {
340 150
        $environment = self::createCommonMarkEnvironment();
341 150
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
342
343 150
        return $environment;
344
    }
345
346 2400
    public function getInlineParserCharacterRegex(): string
347
    {
348 2400
        return $this->inlineParserCharacterRegex;
349
    }
350
351 567
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
352
    {
353 567
        $this->assertUninitialized('Failed to add event listener.');
354
355 564
        if (! isset($this->listeners[$eventClass])) {
356 564
            $this->listeners[$eventClass] = new PrioritizedList();
357
        }
358
359 564
        $this->listeners[$eventClass]->add($listener, $priority);
360
361 564
        if (\is_object($listener)) {
362 504
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
363 147
        } elseif (\is_array($listener) && \is_object($listener[0])) {
364 147
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
365
        }
366
367 564
        return $this;
368
    }
369
370 2835
    public function dispatch(AbstractEvent $event): void
371
    {
372 2835
        if (! $this->extensionsInitialized) {
373 2835
            $this->initializeExtensions();
374
        }
375
376 2832
        $type = \get_class($event);
377
378 2832
        foreach ($this->listeners[$type] ?? [] as $listener) {
379 561
            if ($event->isPropagationStopped()) {
380 3
                return;
381
            }
382
383 561
            $listener($event);
384
        }
385 2829
    }
386
387 2919
    private function buildInlineParserCharacterRegex(): void
388
    {
389 2919
        $chars = \array_unique(\array_merge(
390 2919
            \array_keys($this->inlineParsersByCharacter),
391 2919
            $this->delimiterProcessors->getDelimiterCharacters()
392
        ));
393
394 2919
        if (\count($chars) === 0) {
395
            // If no special inline characters exist then parse the whole line
396 78
            $this->inlineParserCharacterRegex = '/^.+$/u';
397
        } else {
398
            // Match any character which inline parsers are not interested in
399 2841
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/u';
400
        }
401 2919
    }
402
403
    /**
404
     * @throws \RuntimeException
405
     */
406 2940
    private function assertUninitialized(string $message): void
407
    {
408 2940
        if ($this->extensionsInitialized) {
409 24
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
410
        }
411 2916
    }
412
}
413