Skip to content

Commit 88757ed

Browse files
committed
Extract the EmojiTransliterator to a dedicated package
Due to the size of the data needed to transliterate emojis, using a dedicated package allows to include the package in the project only when needed. In Symfony 6.x, the symfony/intl component depends on this component for BC reasons. Root projects can implement a replace hack in their composer.json to skip this package if they don't need it. This dependency will be removed in Symfony 7.0.
1 parent 3040d43 commit 88757ed

File tree

171 files changed

+479
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

171 files changed

+479
-178
lines changed

.github/workflows/package-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121

2222
- name: Find packages
2323
id: find-packages
24-
run: echo "packages=$(php .github/get-modified-packages.php $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/Intl/Resources/emoji |jq -R -s -c 'split("\n")[:-1]') $(git diff --name-only origin/${{ github.base_ref }} HEAD | grep src/ | jq -R -s -c 'split("\n")[:-1]'))" >> $GITHUB_OUTPUT
24+
run: echo "packages=$(php .github/get-modified-packages.php $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/EmojiTransliterator/Resources/emoji |jq -R -s -c 'split("\n")[:-1]') $(git diff --name-only origin/${{ github.base_ref }} HEAD | grep src/ | jq -R -s -c 'split("\n")[:-1]'))" >> $GITHUB_OUTPUT
2525

2626
- name: Verify meta files are correct
2727
run: |

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
echo SYMFONY_DEPRECATIONS_HELPER=weak >> $GITHUB_ENV
9494
cp composer.json composer.json.orig
9595
echo -e '{\n"require":{'"$(grep phpunit-bridge composer.json)"'"php":"*"},"minimum-stability":"dev"}' > composer.json
96-
php .github/build-packages.php HEAD^ $SYMFONY_VERSION $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/Intl/Resources/emoji)
96+
php .github/build-packages.php HEAD^ $SYMFONY_VERSION $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/EmojiTransliterator/Resources/emoji)
9797
mv composer.json composer.json.phpunit
9898
mv composer.json.orig composer.json
9999
fi

.php-cs-fixer.dist.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
'Symfony/Bundle/FrameworkBundle/Resources/views/Form',
5151
// explicit trigger_error tests
5252
'Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/',
53+
'Symfony/Component/EmojiTransliterator/Resources/data/',
5354
'Symfony/Component/Intl/Resources/data/',
5455
])
5556
// Support for older PHPunit version

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"symfony/doctrine-bridge": "self.version",
7070
"symfony/dom-crawler": "self.version",
7171
"symfony/dotenv": "self.version",
72+
"symfony/emoji-transliterator": "self.version",
7273
"symfony/error-handler": "self.version",
7374
"symfony/event-dispatcher": "self.version",
7475
"symfony/expression-language": "self.version",

psalm.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<directory name="src/Symfony/*/*/Tests" />
1515
<directory name="src/Symfony/*/*/*/Tests" />
1616
<directory name="src/Symfony/*/*/*/*/Tests" />
17-
<directory name="src/Symfony/Component/Intl/Resources/emoji/" />
17+
<directory name="src/Symfony/Component/EmojiTransliterator/Resources/emoji/" />
1818
<directory name="vendor" />
1919
</ignoreFiles>
2020
</projectFiles>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
5+
/Resources/emoji export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 8 additions & 0 deletions
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\EmojiTransliterator;
13+
14+
// TODO add a composer requirement in 7.0 instead
15+
if (!class_exists(\Transliterator::class)) {
16+
throw new \LogicException(sprintf('You cannot use the "%s\EmojiTransliterator" class as the "intl" extension is not installed. See https://php.net/intl.', __NAMESPACE__));
17+
} else {
18+
/**
19+
* @internal
20+
*/
21+
trait EmojiTransliteratorTrait
22+
{
23+
private array $map;
24+
private \Transliterator $transliterator;
25+
26+
public static function create(string $id, int $direction = self::FORWARD): self
27+
{
28+
$id = strtolower($id);
29+
30+
if (!isset(self::REVERSEABLE_IDS[$id]) && !str_starts_with($id, 'emoji-')) {
31+
$id = 'emoji-'.$id;
32+
}
33+
34+
if (self::REVERSE === $direction) {
35+
if (!isset(self::REVERSEABLE_IDS[$id])) {
36+
// Create a failing reverse-transliterator to populate intl_get_error_*()
37+
\Transliterator::createFromRules('A > B')->createInverse();
38+
39+
throw new \IntlException(intl_get_error_message(), intl_get_error_code());
40+
}
41+
$id = self::REVERSEABLE_IDS[$id];
42+
}
43+
44+
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/', $id) || !is_file(__DIR__."/Resources/data/{$id}.php")) {
45+
\Transliterator::create($id); // Populate intl_get_error_*()
46+
47+
throw new \IntlException(intl_get_error_message(), intl_get_error_code());
48+
}
49+
50+
static $maps;
51+
52+
// TODO switch the code to use self::class instead of static::class once the class is actually final in Symfony 7.0
53+
// Create an instance of \Transliterator with a custom id; that's the only way
54+
if (\PHP_VERSION_ID >= 80200) {
55+
static $newInstance;
56+
$instance = ($newInstance ??= (new \ReflectionClass(static::class))->newInstanceWithoutConstructor(...))();
57+
$instance->id = $id;
58+
} else {
59+
$instance = unserialize(sprintf('O:%d:"%s":1:{s:2:"id";s:%d:"%s";}', \strlen(static::class), static::class, \strlen($id), $id));
60+
}
61+
62+
$instance->map = $maps[$id] ??= require __DIR__."/Resources/data/{$id}.php";
63+
64+
return $instance;
65+
}
66+
67+
public function createInverse(): self
68+
{
69+
return self::create($this->id, self::REVERSE);
70+
}
71+
72+
public function getErrorCode(): int|false
73+
{
74+
return $this->transliterator?->getErrorCode() ?? 0;
75+
}
76+
77+
public function getErrorMessage(): string|false
78+
{
79+
return $this->transliterator?->getErrorMessage() ?? false;
80+
}
81+
82+
public static function listIDs(): array
83+
{
84+
static $ids = [];
85+
86+
if ($ids) {
87+
return $ids;
88+
}
89+
90+
foreach (scandir(__DIR__.'/Resources/data/') as $file) {
91+
if (str_ends_with($file, '.php')) {
92+
$ids[] = substr($file, 0, -4);
93+
}
94+
}
95+
96+
return $ids;
97+
}
98+
99+
public function transliterate(string $string, int $start = 0, int $end = -1): string|false
100+
{
101+
$quickCheck = ':' === array_key_first($this->map)[0] ? ':' : self::QUICK_CHECK;
102+
103+
if (0 === $start && -1 === $end && preg_match('//u', $string)) {
104+
return \strlen($string) === strcspn($string, $quickCheck) ? $string : strtr($string, $this->map);
105+
}
106+
107+
// Here we rely on intl to validate the $string, $start and $end arguments
108+
// and to slice the string. Slicing is done by replacing the part if $string
109+
// between $start and $end by a unique cookie that can be reliably used to
110+
// identify which part of $string should be transliterated.
111+
112+
static $cookie;
113+
static $transliterator;
114+
115+
$cookie ??= hash('xxh128', random_bytes(8));
116+
$this->transliterator ??= clone $transliterator ??= \Transliterator::createFromRules('[:any:]* > '.$cookie);
117+
118+
if (false === $result = $this->transliterator->transliterate($string, $start, $end)) {
119+
return false;
120+
}
121+
122+
$parts = explode($cookie, $result);
123+
$start = \strlen($parts[0]);
124+
$length = -\strlen($parts[1]) ?: null;
125+
$string = substr($string, $start, $length);
126+
127+
return $parts[0].(\strlen($string) === strcspn($string, $quickCheck) ? $string : strtr($string, $this->map)).$parts[1];
128+
}
129+
}
130+
}
131+
132+
if (\PHP_VERSION_ID >= 80200) {
133+
/**
134+
* @final since 6.3
135+
*/
136+
class EmojiTransliterator extends \Transliterator
137+
{
138+
use EmojiTransliteratorTrait;
139+
140+
private const QUICK_CHECK = "\xA9\xAE\xE2\xE3\xF0";
141+
private const REVERSEABLE_IDS = [
142+
'emoji-github' => 'github-emoji',
143+
'emoji-slack' => 'slack-emoji',
144+
'github-emoji' => 'emoji-github',
145+
'slack-emoji' => 'emoji-slack',
146+
];
147+
148+
public readonly string $id;
149+
}
150+
} else {
151+
/**
152+
* @final since 6.3
153+
*/
154+
class EmojiTransliterator extends \Transliterator
155+
{
156+
use EmojiTransliteratorTrait;
157+
158+
private const QUICK_CHECK = "\xA9\xAE\xE2\xE3\xF0";
159+
private const REVERSEABLE_IDS = [
160+
'emoji-github' => 'github-emoji',
161+
'emoji-slack' => 'slack-emoji',
162+
'github-emoji' => 'emoji-github',
163+
'slack-emoji' => 'emoji-slack',
164+
];
165+
}
166+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2023-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

0 commit comments

Comments
 (0)