Test Setup Failed
Pull Request — latest (#3)
by Mark
34:22
created

Environment::getExtensions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file was originally 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 UnicornFail\Emoji\Environment;
18
19
use League\Configuration\Configuration;
20
use League\Configuration\ConfigurationAwareInterface;
21
use League\Configuration\ConfigurationBuilderInterface;
22
use League\Configuration\ConfigurationInterface;
23
use League\Configuration\Exception\InvalidConfigurationException;
24
use Nette\Schema\Expect;
25
use Psr\EventDispatcher\EventDispatcherInterface;
26
use Psr\EventDispatcher\StoppableEventInterface;
27
use UnicornFail\Emoji\Dataset\RuntimeDataset;
28
use UnicornFail\Emoji\Emojibase\EmojibaseDatasetInterface;
29
use UnicornFail\Emoji\Emojibase\EmojibaseShortcodeInterface;
30
use UnicornFail\Emoji\Event\ListenerData;
31
use UnicornFail\Emoji\Extension\ConfigurableExtensionInterface;
32
use UnicornFail\Emoji\Extension\EmojiCoreExtension;
33
use UnicornFail\Emoji\Extension\ExtensionInterface;
34
use UnicornFail\Emoji\Parser\EmojiParser;
35
use UnicornFail\Emoji\Parser\EmojiParserInterface;
36
use UnicornFail\Emoji\Parser\Lexer;
37
use UnicornFail\Emoji\Renderer\NodeRendererInterface;
38
use UnicornFail\Emoji\Util\PrioritizedList;
39
40
final class Environment implements EnvironmentBuilderInterface
41
{
42
    /** @var ConfigurationBuilderInterface */
43
    private $config;
44
45
    /** @var ?RuntimeDataset */
46
    private $dataset;
47
48
    /** @var ?EventDispatcherInterface */
49
    private $eventDispatcher;
50
51
    /**
52
     * @var ExtensionInterface[]
53
     *
54
     * @psalm-readonly-allow-private-mutation
55
     */
56
    private $extensions = [];
57
58
    /**
59
     * @var bool
60
     *
61
     * @psalm-readonly-allow-private-mutation
62
     */
63
    private $extensionsInitialized = false;
64
65
    /**
66
     * @var bool
67
     *
68
     * @psalm-readonly-allow-private-mutation
69
     */
70
    private $initialized = false;
71
72
    /**
73
     * @var ?PrioritizedList<ListenerData>
74
     *
75
     * @psalm-readonly-allow-private-mutation
76
     */
77
    private $listenerData;
78
79
    /** @var ?EmojiParserInterface */
80
    private $parser;
81
82
    /**
83
     * @var array<string, PrioritizedList<NodeRendererInterface>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, Prioritize...NodeRendererInterface>> at position 4 could not be parsed: Expected '>' at position 4, but found 'PrioritizedList'.
Loading history...
84
     *
85
     * @psalm-readonly-allow-private-mutation
86
     */
87
    private $renderersByClass = [];
88
89
    /**
90
     * @var ExtensionInterface[]
91
     *
92
     * @psalm-readonly-allow-private-mutation
93
     */
94
    private $uninitializedExtensions = [];
95
96
    /** @var RuntimeDataset */
97
    private $locale;
98
99
    /**
100
     * @param array<string, mixed> $config
101
     */
102
    public function __construct(array $config = [])
103
    {
104
        $this->config = self::createDefaultConfiguration();
105
        $this->config->merge($config);
106
        $this->locale = new RuntimeDataset($this->config);
107
    }
108
109
    /**
110
     * @param array<string, mixed> $configuration
111
     */
112
    public static function create(array $configuration = []): self
113
    {
114
        $environment = new self($configuration);
115
116
        foreach (self::defaultExtensions() as $extension) {
117
            $environment->addExtension($extension);
118
        }
119
120
        return $environment;
121
    }
122
123
    public static function createDefaultConfiguration(): ConfigurationBuilderInterface
124
    {
125
        return new Configuration([
126
            'allow_unsafe_links' => Expect::bool(true),
127
128
            'convert' => Expect::anyOf(false, Expect::structure([
129
                'emoticons'     => Expect::anyOf(false, ...Lexer::TYPES)->default(Lexer::UNICODE)->nullable(),
130
                'html_entities' => Expect::anyOf(false, ...Lexer::TYPES)->default(Lexer::UNICODE)->nullable(),
131
                'shortcodes'    => Expect::anyOf(false, ...Lexer::TYPES)->default(Lexer::UNICODE)->nullable(),
132
                'unicodes'      => Expect::anyOf(false, ...Lexer::TYPES)->default(Lexer::UNICODE)->nullable(),
133
            ]))->nullable(),
134
135
            'exclude' => Expect::structure([
136
                'shortcodes' => Expect::arrayOf('string')
137
                    ->default([])
138
                    ->before('\UnicornFail\Emoji\Util\Normalize::shortcodes'),
139
            ]),
140
141
            'locale' => Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_LOCALES)
142
                ->default('en')
143
                ->before('\UnicornFail\Emoji\Dataset\Locale'),
144
145
            'native' => Expect::bool()->nullable(),
146
147
            'presentation' => Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_PRESENTATIONS)
148
                ->default(EmojibaseDatasetInterface::EMOJI),
149
150
            'preset' => Expect::arrayOf('string')
151
                ->default(EmojibaseShortcodeInterface::DEFAULT_PRESETS)
152
                ->mergeDefaults(false)
153
                ->before('\UnicornFail\Emoji\Environment\Environment::normalizePresets'),
154
        ]);
155
    }
156
157
    /**
158
     * @return ExtensionInterface[]
159
     */
160
    protected static function defaultExtensions(): iterable
161
    {
162
        return [new EmojiCoreExtension()];
163
    }
164
165
    /**
166
     * @param string|string[] $presets
167
     *
168
     * @return string[]
169
     */
170
    public static function normalizePresets($presets): array
171
    {
172
        // Presets.
173
        $normalized = [];
174
        foreach ((array) $presets as $preset) {
175
            if (isset(EmojibaseShortcodeInterface::PRESET_ALIASES[$preset])) {
176
                $normalized[] = EmojibaseShortcodeInterface::PRESET_ALIASES[$preset];
177
            } elseif (isset(EmojibaseShortcodeInterface::PRESETS[$preset])) {
178
                $normalized[] = EmojibaseShortcodeInterface::PRESETS[$preset];
179
            } else {
180
                throw InvalidConfigurationException::forConfigOption(
181
                    'preset',
182
                    $preset,
183
                    \sprintf(
184
                        'Accepted values are: %s.',
185
                        \implode(
186
                            ', ',
187
                            \array_map(
188
                                static function ($s) {
189
                                    return \sprintf('"%s"', $s);
190
                                },
191
                                EmojibaseShortcodeInterface::SUPPORTED_PRESETS
192
                            )
193
                        )
194
                    )
195
                );
196
            }
197
        }
198
199
        return \array_values(\array_unique($normalized));
200
    }
201
202
    public function addEventListener(
203
        string $eventClass,
204
        callable $listener,
205
        int $priority = 0
206
    ): EnvironmentBuilderInterface {
207
        $this->assertUninitialized('Failed to add event listener.');
208
209
        if ($this->listenerData === null) {
210
            /** @var PrioritizedList<ListenerData> $listenerData */
211
            $listenerData       = new PrioritizedList();
212
            $this->listenerData = $listenerData;
213
        }
214
215
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
216
217
        $object = \is_array($listener)
218
            ? $listener[0]
219
            : $listener;
220
221
        if ($object instanceof EnvironmentAwareInterface) {
222
            $object->setEnvironment($this);
223
        }
224
225
        if ($object instanceof ConfigurationAwareInterface) {
226
            $object->setConfiguration($this->getConfiguration());
227
        }
228
229
        return $this;
230
    }
231
232
    public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
233
    {
234
        $this->assertUninitialized('Failed to add extension.');
235
236
        $this->extensions[]              = $extension;
237
        $this->uninitializedExtensions[] = $extension;
238
239
        $config = $this->getConfiguration();
240
241
        if ($extension instanceof ConfigurableExtensionInterface) {
242
            $extension->configureSchema($config);
243
        }
244
245
        if ($extension instanceof EnvironmentAwareInterface) {
246
            $extension->setEnvironment($this);
247
        }
248
249
        if ($extension instanceof ConfigurationAwareInterface) {
250
            $extension->setConfiguration($config);
251
        }
252
253
        return $this;
254
    }
255
256
    public function addRenderer(
257
        string $nodeClass,
258
        NodeRendererInterface $renderer,
259
        int $priority = 0
260
    ): EnvironmentBuilderInterface {
261
        $this->assertUninitialized('Failed to add renderer.');
262
263
        if (! isset($this->renderersByClass[$nodeClass])) {
264
            /** @var PrioritizedList<NodeRendererInterface> $renderers */
265
            $renderers = new PrioritizedList();
266
267
            $this->renderersByClass[$nodeClass] = $renderers;
268
        }
269
270
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
271
272
        if ($renderer instanceof EnvironmentAwareInterface) {
273
            $renderer->setEnvironment($this);
274
        }
275
276
        if ($renderer instanceof ConfigurationAwareInterface) {
277
            $renderer->setConfiguration($this->getConfiguration());
278
        }
279
280
        return $this;
281
    }
282
283
    /**
284
     * @throws \RuntimeException
285
     */
286
    protected function assertUninitialized(string $message): void
287
    {
288
        if ($this->initialized) {
289
            throw new \RuntimeException(\sprintf('%s The Environment has already been initialized.', $message));
290
        }
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296
    public function dispatch(object $event)
297
    {
298
        $this->initialize();
299
300
        if ($this->eventDispatcher !== null) {
301
            return $this->eventDispatcher->dispatch($event);
302
        }
303
304
        foreach ($this->getListenersForEvent($event) as $listener) {
305
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
306
                return $event;
307
            }
308
309
            $listener($event);
310
        }
311
312
        return $event;
313
    }
314
315
    /**
316
     * @return ConfigurationBuilderInterface
317
     */
318
    public function getConfiguration(): ConfigurationInterface
319
    {
320
        return $this->config;
321
    }
322
323
    public function getRuntimeDataset(): RuntimeDataset
324
    {
325
        if ($this->dataset === null) {
326
            $this->dataset = new RuntimeDataset($this->getConfiguration());
327
        }
328
329
        return $this->dataset;
330
    }
331
332
    /**
333
     * {@inheritDoc}
334
     *
335
     * @return ExtensionInterface[]
336
     */
337
    public function getExtensions(): iterable
338
    {
339
        return $this->extensions;
340
    }
341
342
    /**
343
     * {@inheritDoc}
344
     *
345
     * @return iterable<callable>
346
     */
347
    public function getListenersForEvent(object $event): iterable
348
    {
349
        if ($this->listenerData === null) {
350
            /** @var PrioritizedList<ListenerData> $listenerData */
351
            $listenerData       = new PrioritizedList();
352
            $this->listenerData = $listenerData;
353
        }
354
355
        /** @var ListenerData $listenerData */
356
        foreach ($this->listenerData as $listenerData) {
357
            if (! \is_a($event, $listenerData->getEvent())) {
358
                continue;
359
            }
360
361
            yield function (object $event) use ($listenerData): void {
362
                $this->initialize();
363
364
                \call_user_func($listenerData->getListener(), $event);
365
            };
366
        }
367
    }
368
369
    public function getParser(): EmojiParserInterface
370
    {
371
        if ($this->parser === null) {
372
            $this->parser = new EmojiParser($this);
373
        }
374
375
        return $this->parser;
376
    }
377
378
    /**
379
     * {@inheritDoc}
380
     *
381
     * @return PrioritizedList<NodeRendererInterface>
382
     */
383
    public function getRenderersForClass(string $nodeClass): iterable
384
    {
385
        $this->initialize();
386
387
        // If renderers are defined for this specific class, return them immediately
388
        if (isset($this->renderersByClass[$nodeClass])) {
389
            return $this->renderersByClass[$nodeClass];
390
        }
391
392
        while (\class_exists($parent = (string) ($parent ?? $nodeClass)) && ($parent = \get_parent_class($parent))) {
393
            if (! isset($this->renderersByClass[$parent])) {
394
                continue;
395
            }
396
397
            // "Cache" this result to avoid future loops
398
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
399
        }
400
401
        /** @var PrioritizedList<NodeRendererInterface> $renderers */
402
        $renderers = new PrioritizedList();
403
404
        return $renderers;
405
    }
406
407
    protected function initialize(): void
408
    {
409
        if ($this->initialized) {
410
            return;
411
        }
412
413
        $this->initializeExtensions();
414
415
        $this->initialized = true;
416
    }
417
418
    protected function initializeExtensions(): void
419
    {
420
        if ($this->extensionsInitialized) {
421
            return;
422
        }
423
424
        // Ask all extensions to register their components.
425
        while (\count($this->uninitializedExtensions) > 0) {
426
            foreach ($this->uninitializedExtensions as $i => $extension) {
427
                $extension->register($this);
428
                unset($this->uninitializedExtensions[$i]);
429
            }
430
        }
431
432
        $this->extensionsInitialized = true;
433
    }
434
435
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
436
    {
437
        $this->eventDispatcher = $dispatcher;
438
    }
439
}
440