Completed
Push — 1.6 ( bb055d )
by Colin
01:28
created

Environment::getDelimiterProcessors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark;
16
17
use League\CommonMark\Block\Parser\BlockParserInterface;
18
use League\CommonMark\Block\Renderer\BlockRendererInterface;
19
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
20
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
21
use League\CommonMark\Event\AbstractEvent;
22
use League\CommonMark\Extension\CommonMarkCoreExtension;
23
use League\CommonMark\Extension\ExtensionInterface;
24
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
25
use League\CommonMark\Inline\Parser\InlineParserInterface;
26
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
27
use League\CommonMark\Util\Configuration;
28
use League\CommonMark\Util\ConfigurationAwareInterface;
29
use League\CommonMark\Util\PrioritizedList;
30
31
final class Environment implements ConfigurableEnvironmentInterface
32
{
33
    /**
34
     * @var ExtensionInterface[]
35
     */
36
    private $extensions = [];
37
38
    /**
39
     * @var ExtensionInterface[]
40
     */
41
    private $uninitializedExtensions = [];
42
43
    /**
44
     * @var bool
45
     */
46
    private $extensionsInitialized = false;
47
48
    /**
49
     * @var PrioritizedList<BlockParserInterface>
50
     */
51
    private $blockParsers;
52
53
    /**
54
     * @var PrioritizedList<InlineParserInterface>
55
     */
56
    private $inlineParsers;
57
58
    /**
59
     * @var array<string, PrioritizedList<InlineParserInterface>>
60
     */
61
    private $inlineParsersByCharacter = [];
62
63
    /**
64
     * @var DelimiterProcessorCollection
65
     */
66
    private $delimiterProcessors;
67
68
    /**
69
     * @var array<string, PrioritizedList<BlockRendererInterface>>
70
     */
71
    private $blockRenderersByClass = [];
72
73
    /**
74
     * @var array<string, PrioritizedList<InlineRendererInterface>>
75
     */
76
    private $inlineRenderersByClass = [];
77
78
    /**
79
     * @var array<string, PrioritizedList<callable>>
80
     */
81
    private $listeners = [];
82
83
    /**
84
     * @var Configuration
85
     */
86
    private $config;
87
88
    /**
89
     * @var string
90
     */
91
    private $inlineParserCharacterRegex;
92
93
    /**
94
     * @param array<string, mixed> $config
95
     */
96 3093
    public function __construct(array $config = [])
97
    {
98 3093
        $this->config = new Configuration($config);
99
100 3093
        $this->blockParsers = new PrioritizedList();
101 3093
        $this->inlineParsers = new PrioritizedList();
102 3093
        $this->delimiterProcessors = new DelimiterProcessorCollection();
103 3093
    }
104
105 2937
    public function mergeConfig(array $config = [])
106
    {
107 2937
        if (\func_num_args() === 0) {
108
            @\trigger_error('Calling Environment::mergeConfig() without any parameters is deprecated in league/commonmark 1.6 and will not be allowed in 2.0', \E_USER_DEPRECATED);
109
        }
110
111 2937
        $this->assertUninitialized('Failed to modify configuration.');
112
113 2934
        $this->config->merge($config);
114 2934
    }
115
116 24
    public function setConfig(array $config = [])
117
    {
118 24
        @\trigger_error('The Environment::setConfig() method is deprecated in league/commonmark 1.6 and will be removed in 2.0. Use mergeConfig() instead.', \E_USER_DEPRECATED);
119
120 24
        $this->assertUninitialized('Failed to modify configuration.');
121
122 21
        $this->config->replace($config);
123 21
    }
124
125 2940
    public function getConfig($key = null, $default = null)
126
    {
127 2940
        return $this->config->get($key, $default);
128
    }
129
130 2919
    public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
131
    {
132 2919
        $this->assertUninitialized('Failed to add block parser.');
133
134 2916
        $this->blockParsers->add($parser, $priority);
135 2916
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
136
137 2916
        return $this;
138
    }
139
140 2928
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
141
    {
142 2928
        $this->assertUninitialized('Failed to add inline parser.');
143
144 2925
        $this->inlineParsers->add($parser, $priority);
145 2925
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
146
147 2925
        foreach ($parser->getCharacters() as $character) {
148 2922
            if (!isset($this->inlineParsersByCharacter[$character])) {
149 2922
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
150
            }
151
152 2922
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
153
        }
154
155 2925
        return $this;
156
    }
157
158 2919
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
159
    {
160 2919
        $this->assertUninitialized('Failed to add delimiter processor.');
161 2916
        $this->delimiterProcessors->add($processor);
162 2916
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
163
164 2916
        return $this;
165
    }
166
167 2925
    public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface
168
    {
169 2925
        $this->assertUninitialized('Failed to add block renderer.');
170
171 2922
        if (!isset($this->blockRenderersByClass[$blockClass])) {
172 2922
            $this->blockRenderersByClass[$blockClass] = new PrioritizedList();
173
        }
174
175 2922
        $this->blockRenderersByClass[$blockClass]->add($blockRenderer, $priority);
176 2922
        $this->injectEnvironmentAndConfigurationIfNeeded($blockRenderer);
177
178 2922
        return $this;
179
    }
180
181 2925
    public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
182
    {
183 2925
        $this->assertUninitialized('Failed to add inline renderer.');
184
185 2922
        if (!isset($this->inlineRenderersByClass[$inlineClass])) {
186 2922
            $this->inlineRenderersByClass[$inlineClass] = new PrioritizedList();
187
        }
188
189 2922
        $this->inlineRenderersByClass[$inlineClass]->add($renderer, $priority);
190 2922
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
191
192 2922
        return $this;
193
    }
194
195 2937
    public function getBlockParsers(): iterable
196
    {
197 2937
        if (!$this->extensionsInitialized) {
198 36
            $this->initializeExtensions();
199
        }
200
201 2937
        return $this->blockParsers->getIterator();
202
    }
203
204 2868
    public function getInlineParsersForCharacter(string $character): iterable
205
    {
206 2868
        if (!$this->extensionsInitialized) {
207 24
            $this->initializeExtensions();
208
        }
209
210 2868
        if (!isset($this->inlineParsersByCharacter[$character])) {
211 2688
            return [];
212
        }
213
214 1695
        return $this->inlineParsersByCharacter[$character]->getIterator();
215
    }
216
217 2865
    public function getDelimiterProcessors(): DelimiterProcessorCollection
218
    {
219 2865
        if (!$this->extensionsInitialized) {
220 6
            $this->initializeExtensions();
221
        }
222
223 2865
        return $this->delimiterProcessors;
224
    }
225
226 2907
    public function getBlockRenderersForClass(string $blockClass): iterable
227
    {
228 2907
        if (!$this->extensionsInitialized) {
229 15
            $this->initializeExtensions();
230
        }
231
232 2907
        return $this->getRenderersByClass($this->blockRenderersByClass, $blockClass, BlockRendererInterface::class);
233
    }
234
235 2613
    public function getInlineRenderersForClass(string $inlineClass): iterable
236
    {
237 2613
        if (!$this->extensionsInitialized) {
238 18
            $this->initializeExtensions();
239
        }
240
241 2613
        return $this->getRenderersByClass($this->inlineRenderersByClass, $inlineClass, InlineRendererInterface::class);
242
    }
243
244
    /**
245
     * Get all registered extensions
246
     *
247
     * @return ExtensionInterface[]
248
     */
249 18
    public function getExtensions(): iterable
250
    {
251 18
        return $this->extensions;
252
    }
253
254
    /**
255
     * Add a single extension
256
     *
257
     * @param ExtensionInterface $extension
258
     *
259
     * @return $this
260
     */
261 2940
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
262
    {
263 2940
        $this->assertUninitialized('Failed to add extension.');
264
265 2937
        $this->extensions[] = $extension;
266 2937
        $this->uninitializedExtensions[] = $extension;
267
268 2937
        return $this;
269
    }
270
271 3012
    private function initializeExtensions(): void
272
    {
273
        // Ask all extensions to register their components
274 3012
        while (!empty($this->uninitializedExtensions)) {
275 2910
            foreach ($this->uninitializedExtensions as $i => $extension) {
276 2910
                $extension->register($this);
277 2910
                unset($this->uninitializedExtensions[$i]);
278
            }
279
        }
280
281 3006
        $this->extensionsInitialized = true;
282
283
        // Lastly, let's build a regex which matches non-inline characters
284
        // This will enable a huge performance boost with inline parsing
285 3006
        $this->buildInlineParserCharacterRegex();
286 3006
    }
287
288
    /**
289
     * @param object $object
290
     */
291 2979
    private function injectEnvironmentAndConfigurationIfNeeded($object): void
292
    {
293 2979
        if ($object instanceof EnvironmentAwareInterface) {
294 2925
            $object->setEnvironment($this);
295
        }
296
297 2979
        if ($object instanceof ConfigurationAwareInterface) {
298 2925
            $object->setConfiguration($this->config);
299
        }
300 2979
    }
301
302 2928
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
303
    {
304 2928
        $environment = new static();
305 2928
        $environment->addExtension(new CommonMarkCoreExtension());
306 2928
        $environment->mergeConfig([
307 976
            'renderer' => [
308 1952
                'block_separator' => "\n",
309
                'inner_separator' => "\n",
310
                'soft_break'      => "\n",
311
            ],
312 2928
            'html_input'         => self::HTML_INPUT_ALLOW,
313
            'allow_unsafe_links' => true,
314
            'max_nesting_level'  => \PHP_INT_MAX,
315
        ]);
316
317 2928
        return $environment;
318
    }
319
320 141
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
321
    {
322 141
        $environment = self::createCommonMarkEnvironment();
323 141
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
324
325 141
        return $environment;
326
    }
327
328 2673
    public function getInlineParserCharacterRegex(): string
329
    {
330 2673
        return $this->inlineParserCharacterRegex;
331
    }
332
333 537
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
334
    {
335 537
        $this->assertUninitialized('Failed to add event listener.');
336
337 534
        if (!isset($this->listeners[$eventClass])) {
338 534
            $this->listeners[$eventClass] = new PrioritizedList();
339
        }
340
341 534
        $this->listeners[$eventClass]->add($listener, $priority);
342
343 534
        if (\is_object($listener)) {
344 507
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
345 105
        } elseif (\is_array($listener) && \is_object($listener[0])) {
346 105
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
347
        }
348
349 534
        return $this;
350
    }
351
352 2913
    public function dispatch(AbstractEvent $event): void
353
    {
354 2913
        if (!$this->extensionsInitialized) {
355 2913
            $this->initializeExtensions();
356
        }
357
358 2907
        $type = \get_class($event);
359
360 2907
        foreach ($this->listeners[$type] ?? [] as $listener) {
361 531
            if ($event->isPropagationStopped()) {
362 3
                return;
363
            }
364
365 531
            $listener($event);
366
        }
367 2904
    }
368
369 3006
    private function buildInlineParserCharacterRegex(): void
370
    {
371 3006
        $chars = \array_unique(\array_merge(
372 3006
            \array_keys($this->inlineParsersByCharacter),
373 3006
            $this->delimiterProcessors->getDelimiterCharacters()
374
        ));
375
376 3006
        if (empty($chars)) {
377
            // If no special inline characters exist then parse the whole line
378 84
            $this->inlineParserCharacterRegex = '/^.+$/';
379
        } else {
380
            // Match any character which inline parsers are not interested in
381 2922
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/';
382
383
            // Only add the u modifier (which slows down performance) if we have a multi-byte UTF-8 character in our regex
384 2922
            if (\strlen($this->inlineParserCharacterRegex) > \mb_strlen($this->inlineParserCharacterRegex)) {
385 54
                $this->inlineParserCharacterRegex .= 'u';
386
            }
387
        }
388 3006
    }
389
390
    /**
391
     * @param string $message
392
     *
393
     * @throws \RuntimeException
394
     */
395 3039
    private function assertUninitialized(string $message): void
396
    {
397 3039
        if ($this->extensionsInitialized) {
398 27
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
399
        }
400 3012
    }
401
402
    /**
403
     * @param array<string, PrioritizedList> $list
404
     * @param string                         $class
405
     * @param string                         $type
406
     *
407
     * @return iterable
408
     *
409
     * @phpstan-template T
410
     *
411
     * @phpstan-param array<string, PrioritizedList<T>> $list
412
     * @phpstan-param string                            $class
413
     * @phpstan-param class-string<T>                   $type
414
     *
415
     * @phpstan-return iterable<T>
416
     */
417 2925
    private function getRenderersByClass(array &$list, string $class, string $type): iterable
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
418
    {
419
        // If renderers are defined for this specific class, return them immediately
420 2925
        if (isset($list[$class])) {
421 2910
            return $list[$class];
422
        }
423
424 75
        while (\class_exists($parent = $parent ?? $class) && $parent = \get_parent_class($parent)) {
425 66
            if (!isset($list[$parent])) {
426 12
                continue;
427
            }
428
429
            // "Cache" this result to avoid future loops
430 60
            return $list[$class] = $list[$parent];
431
        }
432
433 15
        return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type documented by League\CommonMark\Environment::getRenderersByClass of type League\CommonMark\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
434
    }
435
}
436