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

Environment::dispatch()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 17
rs 9.6111
cc 5
nc 4
nop 1
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\Extension\TypedExtensionInterface;
35
use UnicornFail\Emoji\Parser\EmojiParser;
36
use UnicornFail\Emoji\Parser\EmojiParserInterface;
37
use UnicornFail\Emoji\Parser\Lexer;
38
use UnicornFail\Emoji\Renderer\NodeRendererInterface;
39
use UnicornFail\Emoji\Util\PrioritizedList;
40
41
final class Environment implements EnvironmentBuilderInterface
42
{
43
    /** @var ConfigurationBuilderInterface */
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 = self::createDefaultConfiguration();
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 [
132
            'emoticons'     => $value,
133
            'html_entities' => $value,
134
            'shortcodes'    => $value,
135
            'unicodes'      => $value,
136
        ];
137
    }
138
139
    public static function createDefaultConfiguration(): ConfigurationBuilderInterface
140
    {
141
        return new Configuration([
142
            'allow_unsafe_links' => Expect::bool(true),
143
144
            'exclude' => Expect::structure([
145
                'shortcodes' => Expect::arrayOf('string')
146
                    ->default([])
147
                    ->before('\UnicornFail\Emoji\Util\Normalize::shortcodes'),
148
            ])->castTo('array'),
149
150
            'locale' => Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_LOCALES)
151
                ->default('en')
152
                ->before('\UnicornFail\Emoji\Environment\Environment::normalizeLocale'),
153
154
            'native' => Expect::bool()->nullable(),
155
156
            'presentation' => Expect::anyOf(...EmojibaseDatasetInterface::SUPPORTED_PRESENTATIONS)
157
                ->default(EmojibaseDatasetInterface::EMOJI),
158
159
            'preset' => Expect::arrayOf('string')
160
                ->default(EmojibaseShortcodeInterface::DEFAULT_PRESETS)
161
                ->mergeDefaults(false)
162
                ->before('\UnicornFail\Emoji\Environment\Environment::normalizePresets'),
163
        ]);
164
    }
165
166
    /**
167
     * @return ExtensionInterface[]
168
     */
169
    protected static function defaultExtensions(): iterable
170
    {
171
        return [new EmojiCoreExtension()];
172
    }
173
174
    public static function normalizeLocale(string $locale): string
175
    {
176
        // Immediately return if locale is an exact match.
177
        if (\in_array($locale, EmojibaseDatasetInterface::SUPPORTED_LOCALES, true)) {
178
            return $locale;
179
        }
180
181
        // Immediately return if this local has already been normalized.
182
        /** @var string[] $normalized */
183
        static $normalized = [];
184
        if (isset($normalized[$locale])) {
185
            return $normalized[$locale];
186
        }
187
188
        $original              = $locale;
189
        $normalized[$original] = '';
190
191
        // Otherwise, see if it just needs some TLC.
192
        $locale = \strtolower($locale);
193
        $locale = \preg_replace('/[^a-z]/', '-', $locale) ?? $locale;
194
        foreach ([$locale, \current(\explode('-', $locale, 2))] as $locale) {
195
            if (\in_array($locale, EmojibaseDatasetInterface::SUPPORTED_LOCALES, true)) {
196
                $normalized[$original] = $locale;
197
                break;
198
            }
199
        }
200
201
        return $normalized[$original];
202
    }
203
204
    /**
205
     * @param string|string[] $presets
206
     *
207
     * @return string[]
208
     */
209
    public static function normalizePresets($presets): array
210
    {
211
        // Presets.
212
        $normalized = [];
213
        foreach ((array) $presets as $preset) {
214
            if (isset(EmojibaseShortcodeInterface::PRESET_ALIASES[$preset])) {
215
                $normalized[] = EmojibaseShortcodeInterface::PRESET_ALIASES[$preset];
216
            } elseif (isset(EmojibaseShortcodeInterface::PRESETS[$preset])) {
217
                $normalized[] = EmojibaseShortcodeInterface::PRESETS[$preset];
218
            } else {
219
                throw InvalidConfigurationException::forConfigOption(
220
                    'preset',
221
                    $preset,
222
                    \sprintf(
223
                        'Accepted values are: %s.',
224
                        \implode(
225
                            ', ',
226
                            \array_map(
227
                                static function ($s) {
228
                                    return \sprintf('"%s"', $s);
229
                                },
230
                                EmojibaseShortcodeInterface::SUPPORTED_PRESETS
231
                            )
232
                        )
233
                    )
234
                );
235
            }
236
        }
237
238
        return \array_values(\array_unique($normalized));
239
    }
240
241
    public function addEventListener(
242
        string $eventClass,
243
        callable $listener,
244
        int $priority = 0
245
    ): EnvironmentBuilderInterface {
246
        $this->assertUninitialized('Failed to add event listener.');
247
248
        if ($this->listenerData === null) {
249
            /** @var PrioritizedList<ListenerData> $listenerData */
250
            $listenerData       = new PrioritizedList();
251
            $this->listenerData = $listenerData;
252
        }
253
254
        $this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
255
256
        $object = \is_array($listener)
257
            ? $listener[0]
258
            : $listener;
259
260
        if ($object instanceof EnvironmentAwareInterface) {
261
            $object->setEnvironment($this);
262
        }
263
264
        if ($object instanceof ConfigurationAwareInterface) {
265
            $object->setConfiguration($this->getConfiguration());
266
        }
267
268
        return $this;
269
    }
270
271
    public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
272
    {
273
        $this->assertUninitialized('Failed to add extension.');
274
275
        $this->extensions[]              = $extension;
276
        $this->uninitializedExtensions[] = $extension;
277
278
        $config = $this->getConfiguration();
279
280
        if ($extension instanceof ConfigurableExtensionInterface) {
281
            $extension->configureSchema($config);
282
        }
283
284
        return $this;
285
    }
286
287
    public function addRenderer(
288
        string $nodeClass,
289
        NodeRendererInterface $renderer,
290
        int $priority = 0
291
    ): EnvironmentBuilderInterface {
292
        $this->assertUninitialized('Failed to add renderer.');
293
294
        if (! isset($this->renderersByClass[$nodeClass])) {
295
            /** @var PrioritizedList<NodeRendererInterface> $renderers */
296
            $renderers = new PrioritizedList();
297
298
            $this->renderersByClass[$nodeClass] = $renderers;
299
        }
300
301
        $this->renderersByClass[$nodeClass]->add($renderer, $priority);
302
303
        if ($renderer instanceof EnvironmentAwareInterface) {
304
            $renderer->setEnvironment($this);
305
        }
306
307
        if ($renderer instanceof ConfigurationAwareInterface) {
308
            $renderer->setConfiguration($this->getConfiguration());
309
        }
310
311
        return $this;
312
    }
313
314
    /**
315
     * @throws \RuntimeException
316
     */
317
    protected function assertUninitialized(string $message): void
318
    {
319
        if ($this->initialized) {
320
            throw new \RuntimeException(\sprintf('%s The Environment has already been initialized.', $message));
321
        }
322
    }
323
324
    /**
325
     * {@inheritDoc}
326
     */
327
    public function dispatch(object $event)
328
    {
329
        $this->initialize();
330
331
        if ($this->eventDispatcher !== null) {
332
            return $this->eventDispatcher->dispatch($event);
333
        }
334
335
        foreach ($this->getListenersForEvent($event) as $listener) {
336
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
337
                return $event;
338
            }
339
340
            $listener($event);
341
        }
342
343
        return $event;
344
    }
345
346
    /**
347
     * @return ConfigurationBuilderInterface
348
     */
349
    public function getConfiguration(): ConfigurationInterface
350
    {
351
        return $this->config;
352
    }
353
354
    public function getRuntimeDataset(string $index = 'hexcode'): RuntimeDataset
355
    {
356
        if ($this->dataset === null) {
357
            $this->dataset = new RuntimeDataset($this->getConfiguration());
358
        }
359
360
        return $this->dataset->indexBy($index);
361
    }
362
363
    /**
364
     * {@inheritDoc}
365
     *
366
     * @return ExtensionInterface[]
367
     */
368
    public function getExtensions(): iterable
369
    {
370
        return $this->extensions;
371
    }
372
373
    /**
374
     * {@inheritDoc}
375
     *
376
     * @return iterable<callable>
377
     */
378
    public function getListenersForEvent(object $event): iterable
379
    {
380
        if ($this->listenerData === null) {
381
            /** @var PrioritizedList<ListenerData> $listenerData */
382
            $listenerData       = new PrioritizedList();
383
            $this->listenerData = $listenerData;
384
        }
385
386
        /** @var ListenerData $listenerData */
387
        foreach ($this->listenerData as $listenerData) {
388
            if (! \is_a($event, $listenerData->getEvent())) {
389
                continue;
390
            }
391
392
            yield function (object $event) use ($listenerData): void {
393
                $this->initialize();
394
395
                \call_user_func($listenerData->getListener(), $event);
396
            };
397
        }
398
    }
399
400
    public function getParser(): EmojiParserInterface
401
    {
402
        if ($this->parser === null) {
403
            $this->parser = new EmojiParser($this);
404
        }
405
406
        return $this->parser;
407
    }
408
409
    /**
410
     * {@inheritDoc}
411
     *
412
     * @return PrioritizedList<NodeRendererInterface>
413
     */
414
    public function getRenderersForClass(string $nodeClass): iterable
415
    {
416
        $this->initialize();
417
418
        // If renderers are defined for this specific class, return them immediately
419
        if (isset($this->renderersByClass[$nodeClass])) {
420
            return $this->renderersByClass[$nodeClass];
421
        }
422
423
        while (\class_exists($parent = (string) ($parent ?? $nodeClass)) && ($parent = \get_parent_class($parent))) {
424
            if (! isset($this->renderersByClass[$parent])) {
425
                continue;
426
            }
427
428
            // "Cache" this result to avoid future loops
429
            return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
430
        }
431
432
        /** @var PrioritizedList<NodeRendererInterface> $renderers */
433
        $renderers = new PrioritizedList();
434
435
        return $renderers;
436
    }
437
438
    protected function initialize(): void
439
    {
440
        if ($this->initialized) {
441
            return;
442
        }
443
444
        $default = Lexer::UNICODE;
445
446
        /** @var string[] $types */
447
        $types = (array) Lexer::TYPES;
448
449
        foreach ($this->extensions as $extension) {
450
            if ($extension instanceof TypedExtensionInterface) {
451
                $types = \array_unique(\array_merge($types, $extension->registerEmojiTypes()));
452
            }
453
        }
454
455
        $this->config->addSchema('convert', Expect::anyOf(
456
            Expect::structure([
457
                'emoticons'     => Expect::anyOf(false, ...$types)->default($default)->nullable(),
458
                'html_entities' => Expect::anyOf(false, ...$types)->default($default)->nullable(),
459
                'shortcodes'    => Expect::anyOf(false, ...$types)->default($default)->nullable(),
460
                'unicodes'      => Expect::anyOf(false, ...$types)->default($default)->nullable(),
461
            ])->castTo('array'),
462
            ...$types
463
        )->default([
464
            'emoticons'     => $default,
465
            'html_entities' => $default,
466
            'shortcodes'    => $default,
467
            'unicodes'      => $default,
468
        ])->before('\UnicornFail\Emoji\Environment\Environment::normalizeConvert'));
469
470
        $this->initializeExtensions();
471
472
        $this->initialized = true;
473
    }
474
475
    protected function initializeExtensions(): void
476
    {
477
        if ($this->extensionsInitialized) {
478
            return;
479
        }
480
481
        // Ask all extensions to register their components.
482
        while (\count($this->uninitializedExtensions) > 0) {
483
            foreach ($this->uninitializedExtensions as $i => $extension) {
484
                $extension->register($this);
485
                unset($this->uninitializedExtensions[$i]);
486
            }
487
        }
488
489
        $this->extensionsInitialized = true;
490
    }
491
492
    public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
493
    {
494
        $this->eventDispatcher = $dispatcher;
495
    }
496
}
497