Completed
Push — master ( 9292e6...a84270 )
by Colin
03:12 queued 02:10
created

Environment::addInlineRenderer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.8333
c 0
b 0
f 0
cc 2
nc 2
nop 3
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\Environment;
16
17
use League\CommonMark\Configuration\Configuration;
18
use League\CommonMark\Configuration\ConfigurationAwareInterface;
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\CommonMark\CommonMarkCoreExtension;
23
use League\CommonMark\Extension\ExtensionInterface;
24
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
25
use League\CommonMark\Parser\Block\BlockStartParserInterface;
26
use League\CommonMark\Parser\Inline\InlineParserInterface;
27
use League\CommonMark\Renderer\NodeRendererInterface;
28
use League\CommonMark\Util\HtmlFilter;
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<BlockStartParserInterface>
50
     */
51
    private $blockStartParsers;
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<NodeRendererInterface>>
70
     */
71
    private $renderersByClass = [];
72
73
    /**
74
     * @var array<string, PrioritizedList<callable>>
75
     */
76
    private $listeners = [];
77
78
    /**
79
     * @var Configuration
80
     */
81
    private $config;
82
83
    /**
84
     * @var string
85
     */
86
    private $inlineParserCharacterRegex;
87
88
    /**
89
     * @param array<string, mixed> $config
90
     */
91 2616
    public function __construct(array $config = [])
92
    {
93 2616
        $this->config = new Configuration($config);
94
95 2616
        $this->blockStartParsers = new PrioritizedList();
96 2616
        $this->inlineParsers = new PrioritizedList();
97 2616
        $this->delimiterProcessors = new DelimiterProcessorCollection();
98 2616
    }
99
100 2514
    public function mergeConfig(array $config = []): void
101
    {
102 2514
        $this->assertUninitialized('Failed to modify configuration.');
103
104 2511
        $this->config->merge($config);
105 2511
    }
106
107 6
    public function setConfig(array $config = []): void
108
    {
109 6
        $this->assertUninitialized('Failed to modify configuration.');
110
111 3
        $this->config->replace($config);
112 3
    }
113
114 2520
    public function getConfig($key = null, $default = null)
115
    {
116 2520
        return $this->config->get($key, $default);
117
    }
118
119 2502
    public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
120
    {
121 2502
        $this->assertUninitialized('Failed to add block start parser.');
122
123 2499
        $this->blockStartParsers->add($parser, $priority);
124 2499
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
0 ignored issues
show
Documentation introduced by
$parser is of type object<League\CommonMark...ckStartParserInterface>, but the function expects a object<League\CommonMark\Environment\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
125
126 2499
        return $this;
127
    }
128
129 2508
    public function addInlineParser(InlineParserInterface $parser, int $priority = 0): ConfigurableEnvironmentInterface
130
    {
131 2508
        $this->assertUninitialized('Failed to add inline parser.');
132
133 2505
        $this->inlineParsers->add($parser, $priority);
134 2505
        $this->injectEnvironmentAndConfigurationIfNeeded($parser);
0 ignored issues
show
Documentation introduced by
$parser is of type object<League\CommonMark...\InlineParserInterface>, but the function expects a object<League\CommonMark\Environment\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
135
136 2505
        foreach ($parser->getCharacters() as $character) {
137 2502
            if (!isset($this->inlineParsersByCharacter[$character])) {
138 2502
                $this->inlineParsersByCharacter[$character] = new PrioritizedList();
139
            }
140
141 2502
            $this->inlineParsersByCharacter[$character]->add($parser, $priority);
142
        }
143
144 2505
        return $this;
145
    }
146
147 2505
    public function addDelimiterProcessor(DelimiterProcessorInterface $processor): ConfigurableEnvironmentInterface
148
    {
149 2505
        $this->assertUninitialized('Failed to add delimiter processor.');
150 2502
        $this->delimiterProcessors->add($processor);
151 2502
        $this->injectEnvironmentAndConfigurationIfNeeded($processor);
0 ignored issues
show
Documentation introduced by
$processor is of type object<League\CommonMark...iterProcessorInterface>, but the function expects a object<League\CommonMark\Environment\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
152
153 2502
        return $this;
154
    }
155
156 2523
    public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface
157
    {
158 2523
        $this->assertUninitialized('Failed to add renderer.');
159
160 2520
        if (!isset($this->renderersByClass[$nodeClass])) {
161 2520
            $this->renderersByClass[$nodeClass] = new PrioritizedList();
162
        }
163
164 2520
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
165 2520
        $this->injectEnvironmentAndConfigurationIfNeeded($renderer);
0 ignored issues
show
Documentation introduced by
$renderer is of type object<League\CommonMark...\NodeRendererInterface>, but the function expects a object<League\CommonMark\Environment\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
166
167 2520
        return $this;
168
    }
169
170 2142
    public function getBlockStartParsers(): iterable
171
    {
172 2142
        if (!$this->extensionsInitialized) {
173 33
            $this->initializeExtensions();
174
        }
175
176 2142
        return $this->blockStartParsers->getIterator();
177
    }
178
179 2220
    public function getInlineParsersForCharacter(string $character): iterable
180
    {
181 2220
        if (!$this->extensionsInitialized) {
182 18
            $this->initializeExtensions();
183
        }
184
185 2220
        if (!isset($this->inlineParsersByCharacter[$character])) {
186 2082
            return [];
187
        }
188
189 1131
        return $this->inlineParsersByCharacter[$character]->getIterator();
190
    }
191
192 2211
    public function getDelimiterProcessors(): DelimiterProcessorCollection
193
    {
194 2211
        if (!$this->extensionsInitialized) {
195 6
            $this->initializeExtensions();
196
        }
197
198 2211
        return $this->delimiterProcessors;
199
    }
200
201 2514
    public function getRenderersForClass(string $nodeClass): iterable
202
    {
203 2514
        if (!$this->extensionsInitialized) {
204 30
            $this->initializeExtensions();
205
        }
206
207
        // If renderers are defined for this specific class, return them immediately
208 2514
        if (isset($this->renderersByClass[$nodeClass])) {
209 2505
            return $this->renderersByClass[$nodeClass];
210
        }
211
212 42
        while ($parent = \get_parent_class($parent ?? $nodeClass)) {
213 36
            if (!isset($this->renderersByClass[$parent])) {
214 6
                continue;
215
            }
216
217
            // "Cache" this result to avoid future loops
218 33
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
219
        }
220
221 9
        return [];
222
    }
223
224
    /**
225
     * Get all registered extensions
226
     *
227
     * @return ExtensionInterface[]
228
     */
229 12
    public function getExtensions(): iterable
230
    {
231 12
        return $this->extensions;
232
    }
233
234
    /**
235
     * Add a single extension
236
     *
237
     * @param ExtensionInterface $extension
238
     *
239
     * @return $this
240
     */
241 2517
    public function addExtension(ExtensionInterface $extension): ConfigurableEnvironmentInterface
242
    {
243 2517
        $this->assertUninitialized('Failed to add extension.');
244
245 2514
        $this->extensions[] = $extension;
246 2514
        $this->uninitializedExtensions[] = $extension;
247
248 2514
        return $this;
249
    }
250
251 2586
    private function initializeExtensions(): void
252
    {
253
        // Ask all extensions to register their components
254 2586
        while (!empty($this->uninitializedExtensions)) {
255 2496
            foreach ($this->uninitializedExtensions as $i => $extension) {
256 2496
                $extension->register($this);
257 2496
                unset($this->uninitializedExtensions[$i]);
258
            }
259
        }
260
261 2586
        $this->extensionsInitialized = true;
262
263
        // Lastly, let's build a regex which matches non-inline characters
264
        // This will enable a huge performance boost with inline parsing
265 2586
        $this->buildInlineParserCharacterRegex();
266 2586
    }
267
268 2556
    private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
269
    {
270 2556
        if ($object instanceof EnvironmentAwareInterface) {
271 2508
            $object->setEnvironment($this);
272
        }
273
274 2556
        if ($object instanceof ConfigurationAwareInterface) {
275 2508
            $object->setConfiguration($this->config);
276
        }
277 2556
    }
278
279 2505
    public static function createCommonMarkEnvironment(): ConfigurableEnvironmentInterface
280
    {
281 2505
        $environment = new static();
282 2505
        $environment->addExtension(new CommonMarkCoreExtension());
283 2505
        $environment->mergeConfig([
284 835
            'renderer' => [
285 1670
                'block_separator' => "\n",
286
                'inner_separator' => "\n",
287
                'soft_break'      => "\n",
288
            ],
289 2505
            'html_input'         => HtmlFilter::ALLOW,
290
            'allow_unsafe_links' => true,
291
            'max_nesting_level'  => \INF,
292
        ]);
293
294 2505
        return $environment;
295
    }
296
297 72
    public static function createGFMEnvironment(): ConfigurableEnvironmentInterface
298
    {
299 72
        $environment = self::createCommonMarkEnvironment();
300 72
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
301
302 72
        return $environment;
303
    }
304
305 2070
    public function getInlineParserCharacterRegex(): string
306
    {
307 2070
        return $this->inlineParserCharacterRegex;
308
    }
309
310 261
    public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
311
    {
312 261
        $this->assertUninitialized('Failed to add event listener.');
313
314 258
        if (!isset($this->listeners[$eventClass])) {
315 258
            $this->listeners[$eventClass] = new PrioritizedList();
316
        }
317
318 258
        $this->listeners[$eventClass]->add($listener, $priority);
319
320 258
        if (\is_object($listener)) {
321 258
            $this->injectEnvironmentAndConfigurationIfNeeded($listener);
322 36
        } elseif (\is_array($listener) && \is_object($listener[0])) {
323 36
            $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
324
        }
325
326 258
        return $this;
327
    }
328
329 2499
    public function dispatch(AbstractEvent $event): void
330
    {
331 2499
        if (!$this->extensionsInitialized) {
332 2499
            $this->initializeExtensions();
333
        }
334
335 2499
        $type = \get_class($event);
336
337 2499
        foreach ($this->listeners[$type] ?? [] as $listener) {
338 255
            if ($event->isPropagationStopped()) {
339 3
                return;
340
            }
341
342 255
            $listener($event);
343
        }
344 2496
    }
345
346 2586
    private function buildInlineParserCharacterRegex(): void
347
    {
348 2586
        $chars = \array_unique(\array_merge(
349 2586
            \array_keys($this->inlineParsersByCharacter),
350 2586
            $this->delimiterProcessors->getDelimiterCharacters()
351
        ));
352
353 2586
        if (empty($chars)) {
354
            // If no special inline characters exist then parse the whole line
355 78
            $this->inlineParserCharacterRegex = '/^.+$/u';
356
        } else {
357
            // Match any character which inline parsers are not interested in
358 2508
            $this->inlineParserCharacterRegex = '/^[^' . \preg_quote(\implode('', $chars), '/') . ']+/u';
359
        }
360 2586
    }
361
362
    /**
363
     * @param string $message
364
     *
365
     * @throws \RuntimeException
366
     */
367 2604
    private function assertUninitialized(string $message): void
368
    {
369 2604
        if ($this->extensionsInitialized) {
370 24
            throw new \RuntimeException($message . ' Extensions have already been initialized.');
371
        }
372 2580
    }
373
}
374