Test Setup Failed
Pull Request — latest (#3)
by Mark
33:50
created

Environment::initializeConfiguration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 47
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

426
            \array_map(static function (/** @scrutinizer ignore-unused */ string $conversionType) use ($conversionTypes, $default): Schema {

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

Loading history...
427
                return Expect::anyOf(false, ...$conversionTypes)->default($default)->nullable();
428
            }, EmojiConverterInterface::TYPES)
429
        ))->castTo('array');
430
431
        $this->config->addSchema('convert', Expect::anyOf($structuredConversionTypes, ...$conversionTypes)
432
            ->default(\array_fill_keys(EmojiConverterInterface::TYPES, $default))
433
            ->before('\UnicornFail\Emoji\Environment\Environment::normalizeConvert'));
434
435
        $this->config->addSchema('exclude', Expect::structure([
436
            'shortcodes' => Expect::arrayOf('string')
437
                ->default([])
438
                ->before('\UnicornFail\Emoji\Util\Normalize::shortcodes'),
439
        ])->castTo('array'));
440
441
        $this->config->addSchema('locale', Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_LOCALES)
442
            ->default('en')
443
            ->before('\UnicornFail\Emoji\Environment\Environment::normalizeLocale'));
444
445
        $this->config->addSchema('native', Expect::bool()->nullable());
446
447
        $this->config->addSchema('presentation', Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_PRESENTATIONS)
448
            ->default(EmojibaseDatasetInterface::EMOJI));
449
450
        $this->config->addSchema('preset', Expect::arrayOf('string')
451
            ->default(EmojibaseShortcodeInterface::DEFAULT_PRESETS)
452
            ->mergeDefaults(false)
453
            ->before('\UnicornFail\Emoji\Environment\Environment::normalizePresets'));
454
    }
455
456
    protected function initializeExtensions(): void
457
    {
458
        if ($this->extensionsInitialized) {
459
            return;
460
        }
461
462
        // Ask all extensions to register their components.
463
        while (\count($this->uninitializedExtensions) > 0) {
464
            foreach ($this->uninitializedExtensions as $i => $extension) {
465
                $extension->register($this);
466
                unset($this->uninitializedExtensions[$i]);
467
            }
468
        }
469
470
        $this->extensionsInitialized = true;
471
    }
472
473
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
474
    {
475
        $this->eventDispatcher = $dispatcher;
476
    }
477
}
478