Completed
Push — master ( be9d1e...248da8 )
by Colin
01:11
created

Environment::getRenderersByClass()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 8
cts 8
cp 1
rs 9.6666
c 0
b 0
f 0
cc 4
nc 4
nop 2
crap 4
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 2619
    public function __construct(array $config = [])
94
    {
95 2619
        $this->config = new Configuration($config);
96
97 2619
        $this->blockParsers = new PrioritizedList();
98 2619
        $this->inlineParsers = new PrioritizedList();
99 2619
        $this->delimiterProcessors = new DelimiterProcessorCollection();
100 2619
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 2478
    public function mergeConfig(array $config = [])
106
    {
107 2478
        $this->assertUninitialized('Failed to modify configuration.');
108
109 2475
        $this->config->merge($config);
110 2475
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 6
    public function setConfig(array $config = [])
116
    {
117 6
        $this->assertUninitialized('Failed to modify configuration.');
118
119 3
        $this->config->replace($config);
120 3
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125 2481
    public function getConfig($key = null, $default = null)
126
    {
127 2481
        return $this->config->get($key, $default);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 2475
    public function addBlockParser(BlockParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
134
    {
135 2475
        $this->assertUninitialized('Failed to add block parser.');
136
137 2472
        $this->blockParsers->add($parser, $priority);
138 2472
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
139
140 2472
        return $this;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 2478
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
147
    {
148 2478
        $this->assertUninitialized('Failed to add inline parser.');
149
150 2475
        $this->inlineParsers->add($parser, $priority);
151 2475
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
152
153 2475
        foreach ($parser->getCharacters() as $character) {
154 2472
            if (!isset($this->inlineParsersByCharacter[$character])) {
155 2472
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
156
            }
157
158 2472
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
159
        }
160
161 2475
        return $this;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 2475
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
168
    {
169 2475
        $this->assertUninitialized('Failed to add delimiter processor.');
170 2472
        $this->delimiterProcessors->add($processor);
171 2472
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
172
173 2472
        return $this;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 2481
    public function addBlockRenderer($blockClass, BlockRendererInterface $blockRenderer, int $priority = 0): ConfigurableEnvironmentInterface
180
    {
181 2481
        $this->assertUninitialized('Failed to add block renderer.');
182
183 2478
        if (!isset($this->blockRenderersByClass[$blockClass])) {
184 2478
            $this->blockRenderersByClass[$blockClass] = new PrioritizedList();
185
        }
186
187 2478
        $this->blockRenderersByClass[$blockClass]->add($blockRenderer, $priority);
188 2478
        $this->injectEnvironmentAndConfigurationIfNeeded($blockRenderer);
189
190 2478
        return $this;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 2481
    public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
197
    {
198 2481
        $this->assertUninitialized('Failed to add inline renderer.');
199
200 2478
        if (!isset($this->inlineRenderersByClass[$inlineClass])) {
201 2478
            $this->inlineRenderersByClass[$inlineClass] = new PrioritizedList();
202
        }
203
204 2478
        $this->inlineRenderersByClass[$inlineClass]->add($renderer, $priority);
205 2478
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
206
207 2478
        return $this;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 2493
    public function getBlockParsers(): iterable
214
    {
215 2493
        if (!$this->extensionsInitialized) {
216 36
            $this->initializeExtensions();
217
        }
218
219 2493
        return $this->blockParsers->getIterator();
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 2418
    public function getInlineParsersForCharacter(string $character): iterable
226
    {
227 2418
        if (!$this->extensionsInitialized) {
228 18
            $this->initializeExtensions();
229
        }
230
231 2418
        if (!isset($this->inlineParsersByCharacter[$character])) {
232 2244
            return [];
233
        }
234
235 1368
        return $this->inlineParsersByCharacter[$character]->getIterator();
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241 2421
    public function getDelimiterProcessors(): DelimiterProcessorCollection
242
    {
243 2421
        if (!$this->extensionsInitialized) {
244 6
            $this->initializeExtensions();
245
        }
246
247 2421
        return $this->delimiterProcessors;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 2463
    public function getBlockRenderersForClass(string $blockClass): iterable
254
    {
255 2463
        if (!$this->extensionsInitialized) {
256 15
            $this->initializeExtensions();
257
        }
258
259 2463
        return $this->getRenderersByClass($this->blockRenderersByClass, $blockClass);
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265 2175
    public function getInlineRenderersForClass(string $inlineClass): iterable
266
    {
267 2175
        if (!$this->extensionsInitialized) {
268 18
            $this->initializeExtensions();
269
        }
270
271 2175
        return $this->getRenderersByClass($this->inlineRenderersByClass, $inlineClass);
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
     * @param ExtensionInterface $extension
288
     *
289
     * @return $this
290
     */
291 2481
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
292
    {
293 2481
        $this->assertUninitialized('Failed to add extension.');
294
295 2478
        $this->extensions[] = $extension;
296 2478
        $this->uninitializedExtensions[] = $extension;
297
298 2478
        return $this;
299
    }
300
301 2559
    private function initializeExtensions()
302
    {
303
        // Ask all extensions to register their components
304 2559
        while (!empty($this->uninitializedExtensions)) {
305 2466
            foreach ($this->uninitializedExtensions as $i => $extension) {
306 2466
                $extension->register($this);
307 2466
                unset($this->uninitializedExtensions[$i]);
308
            }
309
        }
310
311 2559
        $this->extensionsInitialized = true;
312
313
        // Lastly, let's build a regex which matches non-inline characters
314
        // This will enable a huge performance boost with inline parsing
315 2559
        $this->buildInlineParserCharacterRegex();
316 2559
    }
317
318 2529
    private function injectEnvironmentAndConfigurationIfNeeded($object)
319
    {
320 2529
        if ($object instanceof EnvironmentAwareInterface) {
321 2481
            $object->setEnvironment($this);
322
        }
323
324 2529
        if ($object instanceof ConfigurationAwareInterface) {
325 2481
            $object->setConfiguration($this->config);
326
        }
327 2529
    }
328
329 2469
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
330
    {
331 2469
        $environment = new static();
332 2469
        $environment->addExtension(new CommonMarkCoreExtension());
333 2469
        $environment->mergeConfig([
334 1646
            'renderer' => [
335 823
                'block_separator' => "\n",
336
                'inner_separator' => "\n",
337
                'soft_break'      => "\n",
338
            ],
339 2469
            'html_input'         => self::HTML_INPUT_ALLOW,
340
            'allow_unsafe_links' => true,
341
            'max_nesting_level'  => \INF,
342
        ]);
343
344 2469
        return $environment;
345
    }
346
347 72
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
348
    {
349 72
        $environment = self::createCommonMarkEnvironment();
350 72
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
351
352 72
        return $environment;
353
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358 2229
    public function getInlineParserCharacterRegex(): string
359
    {
360 2229
        return $this->inlineParserCharacterRegex;
361
    }
362
363
    /**
364
     * {@inheritdoc}
365
     */
366 222
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
367
    {
368 222
        $this->assertUninitialized('Failed to add event listener.');
369
370 222
        if (!isset($this->listeners[$eventClass])) {
371 222
            $this->listeners[$eventClass] = new PrioritizedList();
372
        }
373
374 222
        $this->listeners[$eventClass]->add($listener, $priority);
375
376 222
        if (\is_object($listener)) {
377 222
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
378
        }
379
380 222
        return $this;
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386 2466
    public function dispatch(AbstractEvent $event): void
387
    {
388 2466
        if (!$this->extensionsInitialized) {
389 2466
            $this->initializeExtensions();
390
        }
391
392 2466
        $type = \get_class($event);
393
394 2466
        foreach ($this->listeners[$type] ?? [] as $listener) {
395 219
            if ($event->isPropagationStopped()) {
396 3
                return;
397
            }
398
399 219
            $listener($event);
400
        }
401 2463
    }
402
403 2559
    private function buildInlineParserCharacterRegex()
404
    {
405 2559
        $chars = \array_unique(\array_merge(
406 2559
            \array_keys($this->inlineParsersByCharacter),
407 2559
            $this->delimiterProcessors->getDelimiterCharacters()
408
        ));
409
410 2559
        if (empty($chars)) {
411
            // If no special inline characters exist then parse the whole line
412 81
            $this->inlineParserCharacterRegex = '/^.+$/u';
413
        } else {
414
            // Match any character which inline parsers are not interested in
415 2478
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/u';
416
        }
417 2559
    }
418
419
    /**
420
     * @param string $message
421
     *
422
     * @throws \RuntimeException
423
     */
424 2571
    private function assertUninitialized(string $message)
425
    {
426 2571
        if ($this->extensionsInitialized) {
427 24
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
428
        }
429 2547
    }
430
431
    /**
432
     * @param array<string, PrioritizedList> $list
433
     * @param string                         $class
434
     *
435
     * @return iterable
436
     */
437 2481
    private function getRenderersByClass(array &$list, string $class): iterable
438
    {
439
        // If renderers are defined for this specific class, return them immediately
440 2481
        if (isset($list[$class])) {
441 2466
            return $list[$class];
442
        }
443
444 21
        while ($parent = \get_parent_class($parent ?? $class)) {
445 12
            if (!isset($list[$parent])) {
446 12
                continue;
447
            }
448
449
            // "Cache" this result to avoid future loops
450 6
            return $list[$class] = $list[$parent];
451
        }
452
453 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...
454
    }
455
}
456